prompt.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. from typing import Dict, Any, List, Optional, Union
  2. import logging
  3. from rich.console import Console
  4. from rich.prompt import Prompt, Confirm, IntPrompt, FloatPrompt
  5. from rich.table import Table
  6. from rich.panel import Panel
  7. from rich.text import Text
  8. from rich.markdown import Markdown
  9. from rich import box
  10. import re
  11. logger = logging.getLogger('boilerplates')
  12. class PromptHandler:
  13. """Advanced prompt handler with Rich UI for complex variable group logic."""
  14. def __init__(self, variable_groups: Dict[str, Any], resolved_defaults: Dict[str, Any] = None):
  15. """Initialize the prompt handler.
  16. Args:
  17. variable_groups: Dictionary of variable groups from VariableManager
  18. resolved_defaults: Pre-resolved default values with priority handling
  19. """
  20. self.variable_groups = variable_groups
  21. self.resolved_defaults = resolved_defaults or {}
  22. self.console = Console()
  23. self.final_values = {}
  24. def __call__(self) -> Dict[str, Any]:
  25. """Execute the complex prompting logic and return final variable values."""
  26. logger.debug(f"Starting advanced prompt handler with {len(self.variable_groups)} variable groups")
  27. # Process each variable group with the complex logic
  28. for group_name, group_data in self.variable_groups.items():
  29. self._process_variable_group(group_name, group_data)
  30. self._show_summary()
  31. return self.final_values
  32. def _process_variable_group(self, group_name: str, group_data: Dict[str, Any]):
  33. """Process a single variable group with complex prompting logic.
  34. Logic flow:
  35. 1. Check if group has variables with no default values → always prompt
  36. 2. If group is not enabled → ask user if they want to enable it
  37. 3. If group is enabled → prompt for variables without values
  38. 4. Ask if user wants to change existing variable values
  39. """
  40. variables = group_data.get('vars', {})
  41. if not variables:
  42. return
  43. # Show group header
  44. self.console.print(f"[bold cyan]{group_name.title()} Variables[/bold cyan]")
  45. self.console.print()
  46. # Step 1: Check for variables with no default values (always prompt)
  47. vars_without_defaults = self._get_variables_without_defaults(variables)
  48. # Step 2: Determine if group should be enabled
  49. group_enabled = self._determine_group_enabled_status(group_name, variables, vars_without_defaults)
  50. # Always set default values for variables in this group, even if user doesn't want to configure them
  51. vars_with_defaults = self._get_variables_with_defaults(variables)
  52. for var_name in vars_with_defaults:
  53. default_value = self.resolved_defaults.get(var_name)
  54. self.final_values[var_name] = default_value
  55. # When group is not enabled
  56. if not group_enabled:
  57. return
  58. # Step 3: Prompt for required variables (those without defaults)
  59. if vars_without_defaults:
  60. for var_name in vars_without_defaults:
  61. var_data = variables[var_name]
  62. value = self._prompt_for_variable(var_name, var_data, required=True)
  63. self.final_values[var_name] = value
  64. # Step 4: Handle variables with defaults - ask if user wants to change them
  65. vars_with_defaults = self._get_variables_with_defaults(variables)
  66. if vars_with_defaults:
  67. self._handle_variables_with_defaults(group_name, vars_with_defaults, variables)
  68. self.console.print() # Add spacing between groups
  69. def _get_variables_without_defaults(self, variables: Dict[str, Any]) -> List[str]:
  70. """Get list of variable names that have no default values."""
  71. return [
  72. var_name for var_name, var_data in variables.items()
  73. if var_name not in self.resolved_defaults or self.resolved_defaults[var_name] is None
  74. ]
  75. def _get_variables_with_defaults(self, variables: Dict[str, Any]) -> List[str]:
  76. """Get list of variable names that have default values."""
  77. return [
  78. var_name for var_name, var_data in variables.items()
  79. if var_name in self.resolved_defaults and self.resolved_defaults[var_name] is not None
  80. ]
  81. def _determine_group_enabled_status(self, group_name: str, variables: Dict[str, Any], vars_without_defaults: List[str]) -> bool:
  82. """Determine if a variable group should be enabled based on complex logic."""
  83. # If there are required variables (no defaults), group must be enabled
  84. if vars_without_defaults:
  85. logger.debug(f"Group {group_name} has required variables, enabling automatically")
  86. return True
  87. # Check if group is enabled by default values or should ask user
  88. vars_with_defaults = self._get_variables_with_defaults(variables)
  89. if not vars_with_defaults:
  90. logger.debug(f"Group {group_name} has no variables with defaults, skipping")
  91. return False
  92. # Show preview of what this group would configure
  93. self._show_group_preview(group_name, vars_with_defaults)
  94. # Ask user if they want to enable this optional group
  95. try:
  96. return Confirm.ask(
  97. f"[yellow]Do you want to configure {group_name} variables?[/yellow]",
  98. default=False
  99. )
  100. except (EOFError, KeyboardInterrupt):
  101. # For optional group configuration, gracefully handle interruption
  102. logger.debug(f"User interrupted prompt for group {group_name}, defaulting to disabled")
  103. return False
  104. def _show_group_preview(self, group_name: str, vars_with_defaults: List[str]):
  105. """Show a preview of variables that would be configured in this group."""
  106. if not vars_with_defaults:
  107. return
  108. table = Table(title=f"Variables in {group_name}", box=box.SIMPLE)
  109. table.add_column("Variable", style="cyan")
  110. table.add_column("Default Value", style="green")
  111. for var_name in vars_with_defaults:
  112. default_value = self.resolved_defaults.get(var_name, "None")
  113. table.add_row(var_name, str(default_value))
  114. self.console.print(table)
  115. def _handle_variables_with_defaults(self, group_name: str, vars_with_defaults: List[str], variables: Dict[str, Any]):
  116. """Handle variables that have default values."""
  117. # Ask if user wants to customize any of these values (defaults already set earlier)
  118. try:
  119. want_to_customize = Confirm.ask(f"[yellow]Do you want to customize any {group_name} variables?[/yellow]", default=False)
  120. except (EOFError, KeyboardInterrupt):
  121. logger.debug(f"User interrupted customization prompt for group {group_name}, using defaults")
  122. return
  123. if want_to_customize:
  124. for var_name in vars_with_defaults:
  125. var_data = variables[var_name]
  126. current_value = self.final_values[var_name]
  127. self.console.print(f"\n[dim]Current value for [bold]{var_name}[/bold]: {current_value}[/dim]")
  128. try:
  129. change_variable = Confirm.ask(f"Change [bold]{var_name}[/bold]?", default=False)
  130. except (EOFError, KeyboardInterrupt):
  131. logger.debug(f"User interrupted change prompt for variable {var_name}, keeping current value")
  132. continue
  133. if change_variable:
  134. new_value = self._prompt_for_variable(var_name, var_data, required=False, current_value=current_value)
  135. self.final_values[var_name] = new_value
  136. def _prompt_for_variable(self, var_name: str, var_data: Dict[str, Any], required: bool = False, current_value: Any = None) -> Any:
  137. """Prompt user for a single variable with type validation."""
  138. var_type = var_data.get('type', 'string')
  139. description = var_data.get('description', '')
  140. options = var_data.get('options', [])
  141. # Build prompt message
  142. prompt_parts = [f"[bold]{var_name}[/bold]"]
  143. if required:
  144. prompt_parts.append("[red](Required)[/red]")
  145. if description:
  146. prompt_parts.append(f"[dim]{description}[/dim]")
  147. prompt_message = " ".join(prompt_parts)
  148. # Add type information if not string
  149. if var_type != 'string':
  150. prompt_message += f" [dim]({var_type})[/dim]"
  151. # Handle different variable types
  152. try:
  153. if var_type == 'boolean':
  154. return self._prompt_boolean(prompt_message, current_value)
  155. elif var_type == 'integer':
  156. return self._prompt_integer(prompt_message, current_value)
  157. elif var_type == 'float':
  158. return self._prompt_float(prompt_message, current_value)
  159. elif var_type == 'choice' and options:
  160. return self._prompt_choice(prompt_message, options, current_value)
  161. elif var_type == 'list':
  162. return self._prompt_list(prompt_message, current_value)
  163. else: # string or unknown type
  164. return self._prompt_string(prompt_message, current_value, required)
  165. except KeyboardInterrupt:
  166. # Let KeyboardInterrupt propagate up to be handled at module level
  167. raise
  168. except Exception as e:
  169. logger.error(f"Error prompting for variable {var_name}: {e}")
  170. self.console.print(f"[red]Error getting input for {var_name}. Using default string prompt.[/red]")
  171. return self._prompt_string(prompt_message, current_value, required)
  172. def _prompt_string(self, prompt_message: str, current_value: Any = None, required: bool = False) -> str:
  173. """Prompt for string input with validation."""
  174. default_val = str(current_value) if current_value is not None else None
  175. while True:
  176. try:
  177. value = Prompt.ask(prompt_message, default=default_val)
  178. if required and not value.strip():
  179. self.console.print("[red]This field is required and cannot be empty[/red]")
  180. continue
  181. return value.strip()
  182. except (EOFError, KeyboardInterrupt):
  183. # Let KeyboardInterrupt propagate up for proper cancellation
  184. raise KeyboardInterrupt("Template generation cancelled by user")
  185. def _prompt_boolean(self, prompt_message: str, current_value: Any = None) -> bool:
  186. """Prompt for boolean input."""
  187. default_val = bool(current_value) if current_value is not None else None
  188. try:
  189. return Confirm.ask(prompt_message, default=default_val)
  190. except (EOFError, KeyboardInterrupt):
  191. raise KeyboardInterrupt("Template generation cancelled by user")
  192. def _prompt_integer(self, prompt_message: str, current_value: Any = None) -> int:
  193. """Prompt for integer input with validation."""
  194. default_val = int(current_value) if current_value is not None else None
  195. while True:
  196. try:
  197. return IntPrompt.ask(prompt_message, default=default_val)
  198. except ValueError:
  199. self.console.print("[red]Please enter a valid integer[/red]")
  200. except (EOFError, KeyboardInterrupt):
  201. raise KeyboardInterrupt("Template generation cancelled by user")
  202. def _prompt_float(self, prompt_message: str, current_value: Any = None) -> float:
  203. """Prompt for float input with validation."""
  204. default_val = float(current_value) if current_value is not None else None
  205. while True:
  206. try:
  207. return FloatPrompt.ask(prompt_message, default=default_val)
  208. except ValueError:
  209. self.console.print("[red]Please enter a valid number[/red]")
  210. except (EOFError, KeyboardInterrupt):
  211. raise KeyboardInterrupt("Template generation cancelled by user")
  212. def _prompt_choice(self, prompt_message: str, options: List[Any], current_value: Any = None) -> Any:
  213. """Prompt for choice from a list of options."""
  214. # Show available options
  215. self.console.print(f"\n[dim]Available options:[/dim]")
  216. for i, option in enumerate(options, 1):
  217. marker = "→" if option == current_value else " "
  218. self.console.print(f" {marker} {i}. {option}")
  219. while True:
  220. try:
  221. choice = Prompt.ask(f"{prompt_message} (1-{len(options)})")
  222. try:
  223. choice_idx = int(choice) - 1
  224. if 0 <= choice_idx < len(options):
  225. return options[choice_idx]
  226. else:
  227. self.console.print(f"[red]Please enter a number between 1 and {len(options)}[/red]")
  228. except ValueError:
  229. # Try to match by string value
  230. matching_options = [opt for opt in options if str(opt).lower() == choice.lower()]
  231. if matching_options:
  232. return matching_options[0]
  233. self.console.print(f"[red]Please enter a valid option number (1-{len(options)}) or exact option name[/red]")
  234. except (EOFError, KeyboardInterrupt):
  235. raise KeyboardInterrupt("Template generation cancelled by user")
  236. def _prompt_list(self, prompt_message: str, current_value: Any = None) -> List[str]:
  237. """Prompt for list input (comma-separated values)."""
  238. current_str = ""
  239. if current_value and isinstance(current_value, list):
  240. current_str = ", ".join(str(item) for item in current_value)
  241. elif current_value:
  242. current_str = str(current_value)
  243. self.console.print(f"[dim]Enter values separated by commas[/dim]")
  244. try:
  245. value = Prompt.ask(prompt_message, default=current_str)
  246. if not value.strip():
  247. return []
  248. # Split by comma and clean up
  249. items = [item.strip() for item in value.split(',') if item.strip()]
  250. return items
  251. except (EOFError, KeyboardInterrupt):
  252. raise KeyboardInterrupt("Template generation cancelled by user")
  253. def _show_summary(self):
  254. """Display a summary of all configured variables."""
  255. if not self.final_values:
  256. self.console.print("[yellow]No variables were configured.[/yellow]")
  257. return
  258. table = Table(box=box.SIMPLE_HEAVY)
  259. table.add_column("Variable", style="cyan", min_width=20)
  260. table.add_column("Value", style="green")
  261. table.add_column("Type", style="dim")
  262. for var_name, value in self.final_values.items():
  263. var_type = type(value).__name__
  264. # Format value for display
  265. if isinstance(value, list):
  266. display_value = ", ".join(str(item) for item in value)
  267. else:
  268. display_value = str(value)
  269. table.add_row(var_name, display_value, var_type)
  270. self.console.print(table)
  271. self.console.print()
  272. # Ask user if they want to proceed with template generation
  273. if not Confirm.ask("[bold]Proceed with template generation?[/bold]", default=True):
  274. raise KeyboardInterrupt("Template generation cancelled by user")