massgen 0.0.3__py3-none-any.whl → 0.1.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.

Potentially problematic release.


This version of massgen might be problematic. Click here for more details.

Files changed (268) hide show
  1. massgen/__init__.py +142 -8
  2. massgen/adapters/__init__.py +29 -0
  3. massgen/adapters/ag2_adapter.py +483 -0
  4. massgen/adapters/base.py +183 -0
  5. massgen/adapters/tests/__init__.py +0 -0
  6. massgen/adapters/tests/test_ag2_adapter.py +439 -0
  7. massgen/adapters/tests/test_agent_adapter.py +128 -0
  8. massgen/adapters/utils/__init__.py +2 -0
  9. massgen/adapters/utils/ag2_utils.py +236 -0
  10. massgen/adapters/utils/tests/__init__.py +0 -0
  11. massgen/adapters/utils/tests/test_ag2_utils.py +138 -0
  12. massgen/agent_config.py +329 -55
  13. massgen/api_params_handler/__init__.py +10 -0
  14. massgen/api_params_handler/_api_params_handler_base.py +99 -0
  15. massgen/api_params_handler/_chat_completions_api_params_handler.py +176 -0
  16. massgen/api_params_handler/_claude_api_params_handler.py +113 -0
  17. massgen/api_params_handler/_response_api_params_handler.py +130 -0
  18. massgen/backend/__init__.py +39 -4
  19. massgen/backend/azure_openai.py +385 -0
  20. massgen/backend/base.py +341 -69
  21. massgen/backend/base_with_mcp.py +1102 -0
  22. massgen/backend/capabilities.py +386 -0
  23. massgen/backend/chat_completions.py +577 -130
  24. massgen/backend/claude.py +1033 -537
  25. massgen/backend/claude_code.py +1203 -0
  26. massgen/backend/cli_base.py +209 -0
  27. massgen/backend/docs/BACKEND_ARCHITECTURE.md +126 -0
  28. massgen/backend/{CLAUDE_API_RESEARCH.md → docs/CLAUDE_API_RESEARCH.md} +18 -18
  29. massgen/backend/{GEMINI_API_DOCUMENTATION.md → docs/GEMINI_API_DOCUMENTATION.md} +9 -9
  30. massgen/backend/docs/Gemini MCP Integration Analysis.md +1050 -0
  31. massgen/backend/docs/MCP_IMPLEMENTATION_CLAUDE_BACKEND.md +177 -0
  32. massgen/backend/docs/MCP_INTEGRATION_RESPONSE_BACKEND.md +352 -0
  33. massgen/backend/docs/OPENAI_GPT5_MODELS.md +211 -0
  34. massgen/backend/{OPENAI_RESPONSES_API_FORMAT.md → docs/OPENAI_RESPONSE_API_TOOL_CALLS.md} +3 -3
  35. massgen/backend/docs/OPENAI_response_streaming.md +20654 -0
  36. massgen/backend/docs/inference_backend.md +257 -0
  37. massgen/backend/docs/permissions_and_context_files.md +1085 -0
  38. massgen/backend/external.py +126 -0
  39. massgen/backend/gemini.py +1850 -241
  40. massgen/backend/grok.py +40 -156
  41. massgen/backend/inference.py +156 -0
  42. massgen/backend/lmstudio.py +171 -0
  43. massgen/backend/response.py +1095 -322
  44. massgen/chat_agent.py +131 -113
  45. massgen/cli.py +1560 -275
  46. massgen/config_builder.py +2396 -0
  47. massgen/configs/BACKEND_CONFIGURATION.md +458 -0
  48. massgen/configs/README.md +559 -216
  49. massgen/configs/ag2/ag2_case_study.yaml +27 -0
  50. massgen/configs/ag2/ag2_coder.yaml +34 -0
  51. massgen/configs/ag2/ag2_coder_case_study.yaml +36 -0
  52. massgen/configs/ag2/ag2_gemini.yaml +27 -0
  53. massgen/configs/ag2/ag2_groupchat.yaml +108 -0
  54. massgen/configs/ag2/ag2_groupchat_gpt.yaml +118 -0
  55. massgen/configs/ag2/ag2_single_agent.yaml +21 -0
  56. massgen/configs/basic/multi/fast_timeout_example.yaml +37 -0
  57. massgen/configs/basic/multi/gemini_4o_claude.yaml +31 -0
  58. massgen/configs/basic/multi/gemini_gpt5nano_claude.yaml +36 -0
  59. massgen/configs/{gemini_4o_claude.yaml → basic/multi/geminicode_4o_claude.yaml} +3 -3
  60. massgen/configs/basic/multi/geminicode_gpt5nano_claude.yaml +36 -0
  61. massgen/configs/basic/multi/glm_gemini_claude.yaml +25 -0
  62. massgen/configs/basic/multi/gpt4o_audio_generation.yaml +30 -0
  63. massgen/configs/basic/multi/gpt4o_image_generation.yaml +31 -0
  64. massgen/configs/basic/multi/gpt5nano_glm_qwen.yaml +26 -0
  65. massgen/configs/basic/multi/gpt5nano_image_understanding.yaml +26 -0
  66. massgen/configs/{three_agents_default.yaml → basic/multi/three_agents_default.yaml} +8 -4
  67. massgen/configs/basic/multi/three_agents_opensource.yaml +27 -0
  68. massgen/configs/basic/multi/three_agents_vllm.yaml +20 -0
  69. massgen/configs/basic/multi/two_agents_gemini.yaml +19 -0
  70. massgen/configs/{two_agents.yaml → basic/multi/two_agents_gpt5.yaml} +14 -6
  71. massgen/configs/basic/multi/two_agents_opensource_lmstudio.yaml +31 -0
  72. massgen/configs/basic/multi/two_qwen_vllm_sglang.yaml +28 -0
  73. massgen/configs/{single_agent.yaml → basic/single/single_agent.yaml} +1 -1
  74. massgen/configs/{single_flash2.5.yaml → basic/single/single_flash2.5.yaml} +1 -2
  75. massgen/configs/basic/single/single_gemini2.5pro.yaml +16 -0
  76. massgen/configs/basic/single/single_gpt4o_audio_generation.yaml +22 -0
  77. massgen/configs/basic/single/single_gpt4o_image_generation.yaml +22 -0
  78. massgen/configs/basic/single/single_gpt4o_video_generation.yaml +24 -0
  79. massgen/configs/basic/single/single_gpt5nano.yaml +20 -0
  80. massgen/configs/basic/single/single_gpt5nano_file_search.yaml +18 -0
  81. massgen/configs/basic/single/single_gpt5nano_image_understanding.yaml +17 -0
  82. massgen/configs/basic/single/single_gptoss120b.yaml +15 -0
  83. massgen/configs/basic/single/single_openrouter_audio_understanding.yaml +15 -0
  84. massgen/configs/basic/single/single_qwen_video_understanding.yaml +15 -0
  85. massgen/configs/debug/code_execution/command_filtering_blacklist.yaml +29 -0
  86. massgen/configs/debug/code_execution/command_filtering_whitelist.yaml +28 -0
  87. massgen/configs/debug/code_execution/docker_verification.yaml +29 -0
  88. massgen/configs/debug/skip_coordination_test.yaml +27 -0
  89. massgen/configs/debug/test_sdk_migration.yaml +17 -0
  90. massgen/configs/docs/DISCORD_MCP_SETUP.md +208 -0
  91. massgen/configs/docs/TWITTER_MCP_ENESCINAR_SETUP.md +82 -0
  92. massgen/configs/providers/azure/azure_openai_multi.yaml +21 -0
  93. massgen/configs/providers/azure/azure_openai_single.yaml +19 -0
  94. massgen/configs/providers/claude/claude.yaml +14 -0
  95. massgen/configs/providers/gemini/gemini_gpt5nano.yaml +28 -0
  96. massgen/configs/providers/local/lmstudio.yaml +11 -0
  97. massgen/configs/providers/openai/gpt5.yaml +46 -0
  98. massgen/configs/providers/openai/gpt5_nano.yaml +46 -0
  99. massgen/configs/providers/others/grok_single_agent.yaml +19 -0
  100. massgen/configs/providers/others/zai_coding_team.yaml +108 -0
  101. massgen/configs/providers/others/zai_glm45.yaml +12 -0
  102. massgen/configs/{creative_team.yaml → teams/creative/creative_team.yaml} +16 -6
  103. massgen/configs/{travel_planning.yaml → teams/creative/travel_planning.yaml} +16 -6
  104. massgen/configs/{news_analysis.yaml → teams/research/news_analysis.yaml} +16 -6
  105. massgen/configs/{research_team.yaml → teams/research/research_team.yaml} +15 -7
  106. massgen/configs/{technical_analysis.yaml → teams/research/technical_analysis.yaml} +16 -6
  107. massgen/configs/tools/code-execution/basic_command_execution.yaml +25 -0
  108. massgen/configs/tools/code-execution/code_execution_use_case_simple.yaml +41 -0
  109. massgen/configs/tools/code-execution/docker_claude_code.yaml +32 -0
  110. massgen/configs/tools/code-execution/docker_multi_agent.yaml +32 -0
  111. massgen/configs/tools/code-execution/docker_simple.yaml +29 -0
  112. massgen/configs/tools/code-execution/docker_with_resource_limits.yaml +32 -0
  113. massgen/configs/tools/code-execution/multi_agent_playwright_automation.yaml +57 -0
  114. massgen/configs/tools/filesystem/cc_gpt5_gemini_filesystem.yaml +34 -0
  115. massgen/configs/tools/filesystem/claude_code_context_sharing.yaml +68 -0
  116. massgen/configs/tools/filesystem/claude_code_flash2.5.yaml +43 -0
  117. massgen/configs/tools/filesystem/claude_code_flash2.5_gptoss.yaml +49 -0
  118. massgen/configs/tools/filesystem/claude_code_gpt5nano.yaml +31 -0
  119. massgen/configs/tools/filesystem/claude_code_single.yaml +40 -0
  120. massgen/configs/tools/filesystem/fs_permissions_test.yaml +87 -0
  121. massgen/configs/tools/filesystem/gemini_gemini_workspace_cleanup.yaml +54 -0
  122. massgen/configs/tools/filesystem/gemini_gpt5_filesystem_casestudy.yaml +30 -0
  123. massgen/configs/tools/filesystem/gemini_gpt5nano_file_context_path.yaml +43 -0
  124. massgen/configs/tools/filesystem/gemini_gpt5nano_protected_paths.yaml +45 -0
  125. massgen/configs/tools/filesystem/gpt5mini_cc_fs_context_path.yaml +31 -0
  126. massgen/configs/tools/filesystem/grok4_gpt5_gemini_filesystem.yaml +32 -0
  127. massgen/configs/tools/filesystem/multiturn/grok4_gpt5_claude_code_filesystem_multiturn.yaml +58 -0
  128. massgen/configs/tools/filesystem/multiturn/grok4_gpt5_gemini_filesystem_multiturn.yaml +58 -0
  129. massgen/configs/tools/filesystem/multiturn/two_claude_code_filesystem_multiturn.yaml +47 -0
  130. massgen/configs/tools/filesystem/multiturn/two_gemini_flash_filesystem_multiturn.yaml +48 -0
  131. massgen/configs/tools/mcp/claude_code_discord_mcp_example.yaml +27 -0
  132. massgen/configs/tools/mcp/claude_code_simple_mcp.yaml +35 -0
  133. massgen/configs/tools/mcp/claude_code_twitter_mcp_example.yaml +32 -0
  134. massgen/configs/tools/mcp/claude_mcp_example.yaml +24 -0
  135. massgen/configs/tools/mcp/claude_mcp_test.yaml +27 -0
  136. massgen/configs/tools/mcp/five_agents_travel_mcp_test.yaml +157 -0
  137. massgen/configs/tools/mcp/five_agents_weather_mcp_test.yaml +103 -0
  138. massgen/configs/tools/mcp/gemini_mcp_example.yaml +24 -0
  139. massgen/configs/tools/mcp/gemini_mcp_filesystem_test.yaml +23 -0
  140. massgen/configs/tools/mcp/gemini_mcp_filesystem_test_sharing.yaml +23 -0
  141. massgen/configs/tools/mcp/gemini_mcp_filesystem_test_single_agent.yaml +17 -0
  142. massgen/configs/tools/mcp/gemini_mcp_filesystem_test_with_claude_code.yaml +24 -0
  143. massgen/configs/tools/mcp/gemini_mcp_test.yaml +27 -0
  144. massgen/configs/tools/mcp/gemini_notion_mcp.yaml +52 -0
  145. massgen/configs/tools/mcp/gpt5_nano_mcp_example.yaml +24 -0
  146. massgen/configs/tools/mcp/gpt5_nano_mcp_test.yaml +27 -0
  147. massgen/configs/tools/mcp/gpt5mini_claude_code_discord_mcp_example.yaml +38 -0
  148. massgen/configs/tools/mcp/gpt_oss_mcp_example.yaml +25 -0
  149. massgen/configs/tools/mcp/gpt_oss_mcp_test.yaml +28 -0
  150. massgen/configs/tools/mcp/grok3_mini_mcp_example.yaml +24 -0
  151. massgen/configs/tools/mcp/grok3_mini_mcp_test.yaml +27 -0
  152. massgen/configs/tools/mcp/multimcp_gemini.yaml +111 -0
  153. massgen/configs/tools/mcp/qwen_api_mcp_example.yaml +25 -0
  154. massgen/configs/tools/mcp/qwen_api_mcp_test.yaml +28 -0
  155. massgen/configs/tools/mcp/qwen_local_mcp_example.yaml +24 -0
  156. massgen/configs/tools/mcp/qwen_local_mcp_test.yaml +27 -0
  157. massgen/configs/tools/planning/five_agents_discord_mcp_planning_mode.yaml +140 -0
  158. massgen/configs/tools/planning/five_agents_filesystem_mcp_planning_mode.yaml +151 -0
  159. massgen/configs/tools/planning/five_agents_notion_mcp_planning_mode.yaml +151 -0
  160. massgen/configs/tools/planning/five_agents_twitter_mcp_planning_mode.yaml +155 -0
  161. massgen/configs/tools/planning/gpt5_mini_case_study_mcp_planning_mode.yaml +73 -0
  162. massgen/configs/tools/web-search/claude_streamable_http_test.yaml +43 -0
  163. massgen/configs/tools/web-search/gemini_streamable_http_test.yaml +43 -0
  164. massgen/configs/tools/web-search/gpt5_mini_streamable_http_test.yaml +43 -0
  165. massgen/configs/tools/web-search/gpt_oss_streamable_http_test.yaml +44 -0
  166. massgen/configs/tools/web-search/grok3_mini_streamable_http_test.yaml +43 -0
  167. massgen/configs/tools/web-search/qwen_api_streamable_http_test.yaml +44 -0
  168. massgen/configs/tools/web-search/qwen_local_streamable_http_test.yaml +43 -0
  169. massgen/coordination_tracker.py +708 -0
  170. massgen/docker/README.md +462 -0
  171. massgen/filesystem_manager/__init__.py +21 -0
  172. massgen/filesystem_manager/_base.py +9 -0
  173. massgen/filesystem_manager/_code_execution_server.py +545 -0
  174. massgen/filesystem_manager/_docker_manager.py +477 -0
  175. massgen/filesystem_manager/_file_operation_tracker.py +248 -0
  176. massgen/filesystem_manager/_filesystem_manager.py +813 -0
  177. massgen/filesystem_manager/_path_permission_manager.py +1261 -0
  178. massgen/filesystem_manager/_workspace_tools_server.py +1815 -0
  179. massgen/formatter/__init__.py +10 -0
  180. massgen/formatter/_chat_completions_formatter.py +284 -0
  181. massgen/formatter/_claude_formatter.py +235 -0
  182. massgen/formatter/_formatter_base.py +156 -0
  183. massgen/formatter/_response_formatter.py +263 -0
  184. massgen/frontend/__init__.py +1 -2
  185. massgen/frontend/coordination_ui.py +471 -286
  186. massgen/frontend/displays/base_display.py +56 -11
  187. massgen/frontend/displays/create_coordination_table.py +1956 -0
  188. massgen/frontend/displays/rich_terminal_display.py +1259 -619
  189. massgen/frontend/displays/simple_display.py +9 -4
  190. massgen/frontend/displays/terminal_display.py +27 -68
  191. massgen/logger_config.py +681 -0
  192. massgen/mcp_tools/README.md +232 -0
  193. massgen/mcp_tools/__init__.py +105 -0
  194. massgen/mcp_tools/backend_utils.py +1035 -0
  195. massgen/mcp_tools/circuit_breaker.py +195 -0
  196. massgen/mcp_tools/client.py +894 -0
  197. massgen/mcp_tools/config_validator.py +138 -0
  198. massgen/mcp_tools/docs/circuit_breaker.md +646 -0
  199. massgen/mcp_tools/docs/client.md +950 -0
  200. massgen/mcp_tools/docs/config_validator.md +478 -0
  201. massgen/mcp_tools/docs/exceptions.md +1165 -0
  202. massgen/mcp_tools/docs/security.md +854 -0
  203. massgen/mcp_tools/exceptions.py +338 -0
  204. massgen/mcp_tools/hooks.py +212 -0
  205. massgen/mcp_tools/security.py +780 -0
  206. massgen/message_templates.py +342 -64
  207. massgen/orchestrator.py +1515 -241
  208. massgen/stream_chunk/__init__.py +35 -0
  209. massgen/stream_chunk/base.py +92 -0
  210. massgen/stream_chunk/multimodal.py +237 -0
  211. massgen/stream_chunk/text.py +162 -0
  212. massgen/tests/mcp_test_server.py +150 -0
  213. massgen/tests/multi_turn_conversation_design.md +0 -8
  214. massgen/tests/test_azure_openai_backend.py +156 -0
  215. massgen/tests/test_backend_capabilities.py +262 -0
  216. massgen/tests/test_backend_event_loop_all.py +179 -0
  217. massgen/tests/test_chat_completions_refactor.py +142 -0
  218. massgen/tests/test_claude_backend.py +15 -28
  219. massgen/tests/test_claude_code.py +268 -0
  220. massgen/tests/test_claude_code_context_sharing.py +233 -0
  221. massgen/tests/test_claude_code_orchestrator.py +175 -0
  222. massgen/tests/test_cli_backends.py +180 -0
  223. massgen/tests/test_code_execution.py +679 -0
  224. massgen/tests/test_external_agent_backend.py +134 -0
  225. massgen/tests/test_final_presentation_fallback.py +237 -0
  226. massgen/tests/test_gemini_planning_mode.py +351 -0
  227. massgen/tests/test_grok_backend.py +7 -10
  228. massgen/tests/test_http_mcp_server.py +42 -0
  229. massgen/tests/test_integration_simple.py +198 -0
  230. massgen/tests/test_mcp_blocking.py +125 -0
  231. massgen/tests/test_message_context_building.py +29 -47
  232. massgen/tests/test_orchestrator_final_presentation.py +48 -0
  233. massgen/tests/test_path_permission_manager.py +2087 -0
  234. massgen/tests/test_rich_terminal_display.py +14 -13
  235. massgen/tests/test_timeout.py +133 -0
  236. massgen/tests/test_v3_3agents.py +11 -12
  237. massgen/tests/test_v3_simple.py +8 -13
  238. massgen/tests/test_v3_three_agents.py +11 -18
  239. massgen/tests/test_v3_two_agents.py +8 -13
  240. massgen/token_manager/__init__.py +7 -0
  241. massgen/token_manager/token_manager.py +400 -0
  242. massgen/utils.py +52 -16
  243. massgen/v1/agent.py +45 -91
  244. massgen/v1/agents.py +18 -53
  245. massgen/v1/backends/gemini.py +50 -153
  246. massgen/v1/backends/grok.py +21 -54
  247. massgen/v1/backends/oai.py +39 -111
  248. massgen/v1/cli.py +36 -93
  249. massgen/v1/config.py +8 -12
  250. massgen/v1/logging.py +43 -127
  251. massgen/v1/main.py +18 -32
  252. massgen/v1/orchestrator.py +68 -209
  253. massgen/v1/streaming_display.py +62 -163
  254. massgen/v1/tools.py +8 -12
  255. massgen/v1/types.py +9 -23
  256. massgen/v1/utils.py +5 -23
  257. massgen-0.1.0.dist-info/METADATA +1245 -0
  258. massgen-0.1.0.dist-info/RECORD +273 -0
  259. massgen-0.1.0.dist-info/entry_points.txt +2 -0
  260. massgen/frontend/logging/__init__.py +0 -9
  261. massgen/frontend/logging/realtime_logger.py +0 -197
  262. massgen-0.0.3.dist-info/METADATA +0 -568
  263. massgen-0.0.3.dist-info/RECORD +0 -76
  264. massgen-0.0.3.dist-info/entry_points.txt +0 -2
  265. /massgen/backend/{Function calling openai responses.md → docs/Function calling openai responses.md} +0 -0
  266. {massgen-0.0.3.dist-info → massgen-0.1.0.dist-info}/WHEEL +0 -0
  267. {massgen-0.0.3.dist-info → massgen-0.1.0.dist-info}/licenses/LICENSE +0 -0
  268. {massgen-0.0.3.dist-info → massgen-0.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,2396 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ MassGen Interactive Configuration Builder
5
+
6
+ A user-friendly CLI tool to create MassGen configuration files without
7
+ manually writing YAML. Guides users through agent selection, tool configuration,
8
+ and workspace setup.
9
+
10
+ Usage:
11
+ python -m massgen.config_builder
12
+ python -m massgen.cli --build-config
13
+ """
14
+
15
+ import os
16
+ from pathlib import Path
17
+ from typing import Dict, List, Optional, Tuple
18
+
19
+ import questionary
20
+ import yaml
21
+ from dotenv import load_dotenv
22
+ from rich.console import Console
23
+ from rich.panel import Panel
24
+ from rich.prompt import Confirm, Prompt
25
+ from rich.table import Table
26
+ from rich.theme import Theme
27
+
28
+ from massgen.backend.capabilities import BACKEND_CAPABILITIES, get_capabilities
29
+
30
+ # Load environment variables
31
+ load_dotenv()
32
+
33
+ # Custom theme for the CLI - using colors that work on both light and dark backgrounds
34
+ custom_theme = Theme(
35
+ {
36
+ "info": "#4A90E2", # Medium blue - matches system status colors
37
+ "warning": "#CC6600", # Orange-brown - works on both light and dark
38
+ "error": "#CC0000 bold", # Deep red - strong contrast
39
+ "success": "#00AA44 bold", # Deep green - visible on both
40
+ "prompt": "#6633CC bold", # Purple - good on both backgrounds
41
+ },
42
+ )
43
+
44
+ console = Console(theme=custom_theme)
45
+
46
+
47
+ class ConfigBuilder:
48
+ """Interactive configuration builder for MassGen."""
49
+
50
+ @property
51
+ def PROVIDERS(self) -> Dict[str, Dict]:
52
+ """Generate provider configurations from the capabilities registry (single source of truth).
53
+
54
+ This dynamically builds the PROVIDERS dict from massgen/backend/capabilities.py,
55
+ ensuring consistency between config builder, documentation, and backend implementations.
56
+ """
57
+ providers = {}
58
+
59
+ for backend_type, caps in BACKEND_CAPABILITIES.items():
60
+ # Build supports list, handling filesystem specially
61
+ supports = list(caps.supported_capabilities)
62
+
63
+ # Add "filesystem" to supports for ANY backend that supports it (native or MCP)
64
+ if caps.filesystem_support in ["native", "mcp"]:
65
+ supports = [s if s != "filesystem_native" else "filesystem" for s in supports]
66
+ if "filesystem" not in supports:
67
+ supports.append("filesystem")
68
+
69
+ providers[backend_type] = {
70
+ "name": caps.provider_name,
71
+ "type": caps.backend_type,
72
+ "env_var": caps.env_var,
73
+ "models": caps.models,
74
+ "supports": supports,
75
+ }
76
+
77
+ return providers
78
+
79
+ # Use case templates - all use cases support all agent types
80
+ USE_CASES = {
81
+ "custom": {
82
+ "name": "Custom Configuration",
83
+ "description": "Full flexibility - choose any agents, tools, and settings",
84
+ "recommended_agents": 1,
85
+ "recommended_tools": [],
86
+ "agent_types": "all",
87
+ "notes": "Choose any combination of agents and tools",
88
+ "info": None, # No auto-configuration - skip preset panel
89
+ },
90
+ "coding": {
91
+ "name": "Filesystem + Code Execution",
92
+ "description": "Generate, test, and modify code with file operations",
93
+ "recommended_agents": 2,
94
+ "recommended_tools": ["code_execution", "filesystem"],
95
+ "agent_types": "all",
96
+ "notes": "Claude Code recommended for best filesystem support",
97
+ "info": """[bold cyan]Features auto-configured for this preset:[/bold cyan]
98
+
99
+ [green]✓[/green] [bold]Filesystem Access[/bold]
100
+ • File read/write operations in isolated workspace
101
+ • Native filesystem (Claude Code) or MCP filesystem (other backends)
102
+
103
+ [green]✓[/green] [bold]Code Execution[/bold]
104
+ • OpenAI: Code Interpreter
105
+ • Claude/Gemini: Native code execution
106
+ • Isolated execution environment
107
+
108
+ [dim]Use this for:[/dim] Code generation, refactoring, testing, or any task requiring file operations.""",
109
+ },
110
+ "coding_docker": {
111
+ "name": "Filesystem + Code Execution (Docker)",
112
+ "description": "Secure isolated code execution in Docker containers (requires setup)",
113
+ "recommended_agents": 2,
114
+ "recommended_tools": ["code_execution", "filesystem"],
115
+ "agent_types": "all",
116
+ "notes": "⚠️ SETUP REQUIRED: Docker Engine 28+, Python docker library, and image build (see massgen/docker/README.md)",
117
+ "info": """[bold cyan]Features auto-configured for this preset:[/bold cyan]
118
+
119
+ [green]✓[/green] [bold]Filesystem Access[/bold]
120
+ • File read/write operations
121
+
122
+ [green]✓[/green] [bold]Code Execution[/bold]
123
+ • OpenAI: Code Interpreter
124
+ • Claude/Gemini: Native code execution
125
+
126
+ [green]✓[/green] [bold]Docker Isolation[/bold]
127
+ • Fully isolated container execution via MCP
128
+ • Persistent package installations across turns
129
+ • Network and resource controls
130
+
131
+ [yellow]⚠️ Requires Docker setup:[/yellow] Docker Engine 28.0.0+, docker Python library, and massgen-executor image
132
+ [dim]Use this for:[/dim] Secure code execution when you need full isolation and persistent dependencies.""",
133
+ },
134
+ "qa": {
135
+ "name": "Simple Q&A",
136
+ "description": "Basic question answering with multiple perspectives",
137
+ "recommended_agents": 3,
138
+ "recommended_tools": [],
139
+ "agent_types": "all",
140
+ "notes": "Multiple agents provide diverse perspectives and cross-verification",
141
+ "info": None, # No special features - skip preset panel
142
+ },
143
+ "research": {
144
+ "name": "Research & Analysis",
145
+ "description": "Multi-agent research with web search",
146
+ "recommended_agents": 3,
147
+ "recommended_tools": ["web_search"],
148
+ "agent_types": "all",
149
+ "notes": "Works best with web search enabled for current information",
150
+ "info": """[bold cyan]Features auto-configured for this preset:[/bold cyan]
151
+
152
+ [green]✓[/green] [bold]Web Search[/bold]
153
+ • Real-time internet search for current information
154
+ • Fact-checking and source verification
155
+ • Available for: OpenAI, Claude, Gemini, Grok
156
+
157
+ [green]✓[/green] [bold]Multi-Agent Collaboration[/bold]
158
+ • 3 agents recommended for diverse perspectives
159
+ • Cross-verification of facts and sources
160
+
161
+ [dim]Use this for:[/dim] Research queries, current events, fact-checking, comparative analysis.""",
162
+ },
163
+ "data_analysis": {
164
+ "name": "Data Analysis",
165
+ "description": "Analyze data with code execution and visualizations",
166
+ "recommended_agents": 2,
167
+ "recommended_tools": ["code_execution", "filesystem", "image_understanding"],
168
+ "agent_types": "all",
169
+ "notes": "Code execution helps with data processing and visualization",
170
+ "info": """[bold cyan]Features auto-configured for this preset:[/bold cyan]
171
+
172
+ [green]✓[/green] [bold]Filesystem Access[/bold]
173
+ • Read/write data files (CSV, JSON, etc.)
174
+ • Save visualizations and reports
175
+
176
+ [green]✓[/green] [bold]Code Execution[/bold]
177
+ • Data processing and transformation
178
+ • Statistical analysis
179
+ • Visualization generation (matplotlib, seaborn, etc.)
180
+
181
+ [green]✓[/green] [bold]Image Understanding[/bold]
182
+ • Analyze charts, graphs, and visualizations
183
+ • Extract data from images and screenshots
184
+ • Available for: OpenAI, Claude Code, Gemini, Azure OpenAI
185
+
186
+ [dim]Use this for:[/dim] Data analysis, chart interpretation, statistical processing, visualization.""",
187
+ },
188
+ "multimodal": {
189
+ "name": "Multimodal Analysis",
190
+ "description": "Analyze images, audio, and video content",
191
+ "recommended_agents": 2,
192
+ "recommended_tools": ["image_understanding", "audio_understanding", "video_understanding"],
193
+ "agent_types": "all",
194
+ "notes": "Different backends support different modalities",
195
+ "info": """[bold cyan]Features auto-configured for this preset:[/bold cyan]
196
+
197
+ [green]✓[/green] [bold]Image Understanding[/bold]
198
+ • Analyze images, screenshots, charts
199
+ • OCR and text extraction
200
+ • Available for: OpenAI, Claude Code, Gemini, Azure OpenAI
201
+
202
+ [green]✓[/green] [bold]Audio Understanding[/bold] [dim](where supported)[/dim]
203
+ • Transcribe and analyze audio
204
+ • Available for: Claude, ChatCompletion
205
+
206
+ [green]✓[/green] [bold]Video Understanding[/bold] [dim](where supported)[/dim]
207
+ • Analyze video content
208
+ • Available for: Claude, ChatCompletion, OpenAI
209
+
210
+ [dim]Use this for:[/dim] Image analysis, screenshot interpretation, multimedia content analysis.""",
211
+ },
212
+ }
213
+
214
+ def __init__(self, default_mode: bool = False) -> None:
215
+ """Initialize the configuration builder with default config.
216
+
217
+ Args:
218
+ default_mode: If True, save config to ~/.config/massgen/config.yaml by default
219
+ """
220
+ self.config = {
221
+ "agents": [],
222
+ "ui": {
223
+ "display_type": "rich_terminal",
224
+ "logging_enabled": True,
225
+ },
226
+ }
227
+ self.orchestrator_config = {}
228
+ self.default_mode = default_mode
229
+
230
+ def show_banner(self) -> None:
231
+ """Display welcome banner using Rich Panel."""
232
+ # Clear screen
233
+ console.clear()
234
+
235
+ # ASCII art for multi-agent coordination
236
+ ascii_art = """[bold cyan]
237
+ ███╗ ███╗ █████╗ ███████╗███████╗ ██████╗ ███████╗███╗ ██╗
238
+ ████╗ ████║██╔══██╗██╔════╝██╔════╝██╔════╝ ██╔════╝████╗ ██║
239
+ ██╔████╔██║███████║███████╗███████╗██║ ███╗█████╗ ██╔██╗ ██║
240
+ ██║╚██╔╝██║██╔══██║╚════██║╚════██║██║ ██║██╔══╝ ██║╚██╗██║
241
+ ██║ ╚═╝ ██║██║ ██║███████║███████║╚██████╔╝███████╗██║ ╚████║
242
+ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝[/bold cyan]
243
+
244
+ [dim] 🤖 🤖 🤖 → 💬 collaborate → 🎯 winner → 📢 final[/dim]
245
+ """
246
+
247
+ banner_content = f"""{ascii_art}
248
+ [bold bright_cyan]Interactive Configuration Builder[/bold bright_cyan]
249
+ [dim]Create custom multi-agent configurations in minutes![/dim]"""
250
+
251
+ banner_panel = Panel(
252
+ banner_content,
253
+ border_style="bold cyan",
254
+ padding=(0, 2),
255
+ width=80,
256
+ )
257
+
258
+ console.print(banner_panel)
259
+ console.print()
260
+
261
+ def _calculate_visible_length(self, text: str) -> int:
262
+ """Calculate visible length of text, excluding Rich markup tags."""
263
+ import re
264
+
265
+ # Remove all Rich markup tags like [bold], [/bold], [dim cyan], etc.
266
+ visible_text = re.sub(r"\[/?[^\]]+\]", "", text)
267
+ return len(visible_text)
268
+
269
+ def _pad_with_markup(self, text: str, target_width: int) -> str:
270
+ """Pad text to target width, accounting for Rich markup."""
271
+ visible_len = self._calculate_visible_length(text)
272
+ padding_needed = target_width - visible_len
273
+ return text + (" " * padding_needed if padding_needed > 0 else "")
274
+
275
+ def _safe_prompt(self, prompt_func, error_msg: str = "Selection cancelled"):
276
+ """Wrapper for questionary prompts with graceful exit handling.
277
+
278
+ Args:
279
+ prompt_func: The questionary prompt function to call
280
+ error_msg: Error message to show if cancelled
281
+
282
+ Returns:
283
+ The result from the prompt, or raises KeyboardInterrupt if cancelled
284
+
285
+ Raises:
286
+ KeyboardInterrupt: If user cancels (Ctrl+C or returns None)
287
+ """
288
+ try:
289
+ result = prompt_func()
290
+ if result is None:
291
+ # User pressed Ctrl+C or Esc - treat as cancellation
292
+ raise KeyboardInterrupt
293
+ return result
294
+ except (KeyboardInterrupt, EOFError):
295
+ # Re-raise to be handled by caller
296
+ raise
297
+
298
+ def detect_api_keys(self) -> Dict[str, bool]:
299
+ """Detect available API keys from environment with error handling."""
300
+ api_keys = {}
301
+ try:
302
+ for provider_id, provider_info in self.PROVIDERS.items():
303
+ try:
304
+ # Claude Code is always available (works with CLI login or API key)
305
+ if provider_id == "claude_code":
306
+ api_keys[provider_id] = True
307
+ continue
308
+
309
+ env_var = provider_info.get("env_var")
310
+ if env_var:
311
+ api_keys[provider_id] = bool(os.getenv(env_var))
312
+ else:
313
+ api_keys[provider_id] = True # Local models don't need keys
314
+ except Exception as e:
315
+ console.print(f"[warning]⚠️ Could not check {provider_id}: {e}[/warning]")
316
+ api_keys[provider_id] = False
317
+ return api_keys
318
+ except Exception as e:
319
+ console.print(f"[error]❌ Error detecting API keys: {e}[/error]")
320
+ # Return empty dict to allow continue with manual input
321
+ return {provider_id: False for provider_id in self.PROVIDERS.keys()}
322
+
323
+ def interactive_api_key_setup(self) -> Dict[str, bool]:
324
+ """Interactive API key setup wizard.
325
+
326
+ Prompts user to enter API keys for providers and saves them to .env file.
327
+ Follows CLI tool patterns (AWS CLI, Stripe CLI) for API key management.
328
+
329
+ Returns:
330
+ Updated api_keys dict after setup
331
+ """
332
+ try:
333
+ console.print("\n[bold cyan]API Key Setup[/bold cyan]\n")
334
+ console.print("[dim]Configure API keys for cloud AI providers.[/dim]")
335
+ console.print("[dim](Alternatively, you can use local models like vLLM/Ollama - no keys needed)[/dim]\n")
336
+
337
+ # Collect API keys from user
338
+ collected_keys = {}
339
+
340
+ # Complete list of all API key providers (includes main backends + chatcompletion variants)
341
+ # This is the complete set from cli.py create_backend()
342
+ all_providers = [
343
+ # Main backends (high priority)
344
+ ("openai", "OpenAI", "OPENAI_API_KEY"),
345
+ ("anthropic", "Anthropic (Claude)", "ANTHROPIC_API_KEY"),
346
+ ("gemini", "Google Gemini", "GOOGLE_API_KEY"),
347
+ ("grok", "xAI (Grok)", "XAI_API_KEY"),
348
+ # Azure
349
+ ("azure_openai", "Azure OpenAI", "AZURE_OPENAI_API_KEY"),
350
+ # ChatCompletion providers
351
+ ("cerebras", "Cerebras AI", "CEREBRAS_API_KEY"),
352
+ ("together", "Together AI", "TOGETHER_API_KEY"),
353
+ ("fireworks", "Fireworks AI", "FIREWORKS_API_KEY"),
354
+ ("groq", "Groq", "GROQ_API_KEY"),
355
+ ("nebius", "Nebius AI Studio", "NEBIUS_API_KEY"),
356
+ ("openrouter", "OpenRouter", "OPENROUTER_API_KEY"),
357
+ ("zai", "ZAI (Zhipu.ai)", "ZAI_API_KEY"),
358
+ ("moonshot", "Kimi/Moonshot AI", "MOONSHOT_API_KEY"),
359
+ ("poe", "POE", "POE_API_KEY"),
360
+ ("qwen", "Qwen (Alibaba)", "QWEN_API_KEY"),
361
+ ]
362
+
363
+ # Create checkbox choices for provider selection (nothing pre-checked)
364
+ provider_choices = []
365
+ for provider_id, name, env_var in all_providers:
366
+ provider_choices.append(
367
+ questionary.Choice(
368
+ f"{name:<25} [{env_var}]",
369
+ value=(provider_id, name, env_var),
370
+ checked=False,
371
+ ),
372
+ )
373
+
374
+ console.print("[dim]Select which providers you want to configure (Space to toggle, Enter to confirm):[/dim]")
375
+ console.print("[dim]Or skip all to use local models (vLLM, Ollama, etc.)[/dim]\n")
376
+
377
+ selected_providers = questionary.checkbox(
378
+ "Select cloud providers to configure:",
379
+ choices=provider_choices,
380
+ style=questionary.Style(
381
+ [
382
+ ("selected", "fg:cyan"),
383
+ ("pointer", "fg:cyan bold"),
384
+ ("highlighted", "fg:cyan"),
385
+ ],
386
+ ),
387
+ use_arrow_keys=True,
388
+ ).ask()
389
+
390
+ if selected_providers is None:
391
+ raise KeyboardInterrupt
392
+
393
+ if not selected_providers:
394
+ console.print("\n[yellow]⚠️ No providers selected[/yellow]")
395
+ console.print("[dim]Skipping API key setup. You can use local models (vLLM, Ollama) without API keys.[/dim]\n")
396
+ return {}
397
+
398
+ # Now prompt for API keys only for selected providers
399
+ console.print(f"\n[cyan]Configuring {len(selected_providers)} provider(s)[/cyan]\n")
400
+
401
+ for provider_id, name, env_var in selected_providers:
402
+ # Prompt for API key (with password-style input)
403
+ console.print(f"[bold cyan]{name}[/bold cyan]")
404
+ console.print(f"[dim]Environment variable: {env_var}[/dim]")
405
+
406
+ api_key = Prompt.ask(
407
+ f"Enter your {name} API key",
408
+ password=True, # Hide input
409
+ )
410
+
411
+ if api_key is None:
412
+ raise KeyboardInterrupt
413
+
414
+ if api_key and api_key.strip():
415
+ collected_keys[env_var] = api_key.strip()
416
+ console.print(f"✅ {name} API key saved")
417
+ else:
418
+ console.print(f"[yellow]⚠️ Skipped {name} (empty input)[/yellow]")
419
+ console.print()
420
+
421
+ if not collected_keys:
422
+ console.print("[error]❌ No API keys were configured.[/error]")
423
+ console.print("[info]At least one API key is required to use MassGen.[/info]")
424
+ return {}
425
+
426
+ # Ask where to save
427
+ console.print("\n[bold cyan]Where to Save API Keys[/bold cyan]\n")
428
+ console.print("[dim]Choose where to save your API keys:[/dim]\n")
429
+ console.print(" [1] ~/.massgen/.env (recommended - available globally)")
430
+ console.print(" [2] ./.env (current directory only)")
431
+ console.print()
432
+
433
+ save_location = Prompt.ask(
434
+ "[prompt]Choose location[/prompt]",
435
+ choices=["1", "2"],
436
+ default="1",
437
+ )
438
+
439
+ if save_location is None:
440
+ raise KeyboardInterrupt
441
+
442
+ # Determine target path
443
+ if save_location == "1":
444
+ env_dir = Path.home() / ".massgen"
445
+ env_dir.mkdir(parents=True, exist_ok=True)
446
+ env_path = env_dir / ".env"
447
+ else:
448
+ env_path = Path(".env")
449
+
450
+ # Check if .env already exists
451
+ existing_content = {}
452
+ if env_path.exists():
453
+ console.print(f"\n[yellow]⚠️ {env_path} already exists[/yellow]")
454
+
455
+ # Parse existing .env file
456
+ try:
457
+ with open(env_path, "r") as f:
458
+ for line in f:
459
+ line = line.strip()
460
+ if line and not line.startswith("#") and "=" in line:
461
+ key, value = line.split("=", 1)
462
+ existing_content[key.strip()] = value.strip()
463
+ except Exception as e:
464
+ console.print(f"[warning]⚠️ Could not read existing .env: {e}[/warning]")
465
+
466
+ merge = Confirm.ask("Merge with existing keys (recommended)?", default=True)
467
+ if merge is None:
468
+ raise KeyboardInterrupt
469
+
470
+ if merge:
471
+ # Merge: existing keys + new keys (new keys overwrite)
472
+ existing_content.update(collected_keys)
473
+ collected_keys = existing_content
474
+ else:
475
+ # User chose to overwrite completely
476
+ pass
477
+
478
+ # Write .env file
479
+ try:
480
+ with open(env_path, "w") as f:
481
+ f.write("# MassGen API Keys\n")
482
+ f.write("# Generated by MassGen Interactive Setup\n\n")
483
+
484
+ for env_var, api_key in sorted(collected_keys.items()):
485
+ f.write(f"{env_var}={api_key}\n")
486
+
487
+ console.print(f"\n✅ [success]API keys saved to: {env_path.absolute()}[/success]")
488
+
489
+ # Security reminder
490
+ if env_path == Path(".env"):
491
+ console.print("\n[yellow]⚠️ Security reminder:[/yellow]")
492
+ console.print("[yellow] Add .env to your .gitignore to avoid committing API keys![/yellow]")
493
+
494
+ except Exception as e:
495
+ console.print(f"\n[error]❌ Failed to save .env file: {e}[/error]")
496
+ return {}
497
+
498
+ # Reload environment variables
499
+ console.print("\n[dim]Reloading environment variables...[/dim]")
500
+ load_dotenv(env_path, override=True)
501
+
502
+ # Re-detect API keys
503
+ console.print("[dim]Verifying API keys...[/dim]\n")
504
+ updated_api_keys = self.detect_api_keys()
505
+
506
+ # Show what was detected
507
+ available_count = sum(1 for has_key in updated_api_keys.values() if has_key)
508
+ console.print(f"[success]✅ {available_count} provider(s) available[/success]\n")
509
+
510
+ return updated_api_keys
511
+
512
+ except (KeyboardInterrupt, EOFError):
513
+ console.print("\n\n[yellow]API key setup cancelled[/yellow]\n")
514
+ return {}
515
+ except Exception as e:
516
+ console.print(f"\n[error]❌ Error during API key setup: {e}[/error]")
517
+ return {}
518
+
519
+ def show_available_providers(
520
+ self,
521
+ api_keys: Dict[str, bool],
522
+ ) -> None:
523
+ """Display providers in a clean Rich table."""
524
+ try:
525
+ # Create Rich table
526
+ table = Table(
527
+ title="[bold cyan]Available Providers[/bold cyan]",
528
+ show_header=True,
529
+ header_style="bold cyan",
530
+ border_style="cyan",
531
+ title_style="bold cyan",
532
+ expand=False, # Don't expand to full width
533
+ padding=(0, 1), # Padding around cells
534
+ )
535
+
536
+ # Add columns
537
+ table.add_column("", justify="center", width=3, no_wrap=True) # Status icon
538
+ table.add_column("Provider", style="bold", min_width=20)
539
+ table.add_column("Models", style="dim", min_width=25)
540
+ table.add_column("Capabilities", style="dim cyan", min_width=20)
541
+
542
+ # Add rows for each provider
543
+ for provider_id, provider_info in self.PROVIDERS.items():
544
+ try:
545
+ has_key = api_keys.get(provider_id, False)
546
+ status = "✅" if has_key else "❌"
547
+ name = provider_info.get("name", "Unknown")
548
+
549
+ # Models (first 2)
550
+ models = provider_info.get("models", [])
551
+ models_display = ", ".join(models[:2])
552
+ if len(models) > 2:
553
+ models_display += f" +{len(models)-2}"
554
+
555
+ # Capabilities (abbreviated, first 3)
556
+ caps = provider_info.get("supports", [])
557
+ cap_abbrev = {
558
+ "web_search": "web",
559
+ "code_execution": "code",
560
+ "filesystem": "files",
561
+ "image_understanding": "img",
562
+ "reasoning": "reason",
563
+ "mcp": "mcp",
564
+ "audio_understanding": "audio",
565
+ "video_understanding": "video",
566
+ }
567
+ caps_display = ", ".join([cap_abbrev.get(c, c[:4]) for c in caps[:3]])
568
+ if len(caps) > 3:
569
+ caps_display += f" +{len(caps)-3}"
570
+
571
+ # Add row
572
+ # Special handling for Claude Code - always available but show hint if no API key
573
+ if provider_id == "claude_code":
574
+ env_var = provider_info.get("env_var", "")
575
+ api_key_set = bool(os.getenv(env_var)) if env_var else False
576
+ if api_key_set:
577
+ table.add_row("✅", name, models_display, caps_display or "basic")
578
+ else:
579
+ name_with_hint = f"{name}\n[dim cyan]⚠️ Requires `claude login` (no API key found)[/dim cyan]"
580
+ table.add_row("✅", name_with_hint, models_display, caps_display or "basic")
581
+ elif has_key:
582
+ table.add_row(status, name, models_display, caps_display or "basic")
583
+ else:
584
+ # For missing keys, add env var hint
585
+ env_var = provider_info.get("env_var", "")
586
+ name_with_hint = f"{name}\n[yellow]Need: {env_var}[/yellow]"
587
+ table.add_row(status, name_with_hint, models_display, caps_display or "basic")
588
+
589
+ except Exception as e:
590
+ console.print(f"[warning]⚠️ Could not display {provider_id}: {e}[/warning]")
591
+
592
+ # Display the table
593
+ console.print(table)
594
+ console.print("\n💡 [dim]Tip: Set API keys in ~/.config/massgen/.env or ~/.massgen/.env[/dim]\n")
595
+
596
+ except Exception as e:
597
+ console.print(f"[error]❌ Error displaying providers: {e}[/error]")
598
+ console.print("[info]Continuing with setup...[/info]\n")
599
+
600
+ def select_use_case(self) -> str:
601
+ """Let user select a use case template with error handling."""
602
+ try:
603
+ # Step header
604
+ step_panel = Panel(
605
+ "[bold cyan]Step 1 of 4: Select Your Use Case[/bold cyan]\n\n[italic dim]All agent types are supported for every use case[/italic dim]",
606
+ border_style="cyan",
607
+ padding=(0, 2),
608
+ width=80,
609
+ )
610
+ console.print(step_panel)
611
+ console.print()
612
+
613
+ # Build choices for questionary - organized with tool hints
614
+ choices = []
615
+
616
+ # Define display with brief tool descriptions
617
+ display_info = [
618
+ ("custom", "⚙️", "Custom Configuration", "Choose your own tools"),
619
+ ("qa", "💬", "Simple Q&A", "Basic chat (no special tools)"),
620
+ ("research", "🔍", "Research & Analysis", "Web search enabled"),
621
+ ("coding", "💻", "Code & Files", "File ops + code execution"),
622
+ ("coding_docker", "🐳", "Code & Files (Docker)", "File ops + isolated Docker execution"),
623
+ ("data_analysis", "📊", "Data Analysis", "Files + code + image analysis"),
624
+ ("multimodal", "🎨", "Multimodal Analysis", "Images, audio, video understanding"),
625
+ ]
626
+
627
+ for use_case_id, emoji, name, tools_hint in display_info:
628
+ try:
629
+ use_case_info = self.USE_CASES.get(use_case_id)
630
+ if not use_case_info:
631
+ continue
632
+
633
+ # Show name with tools hint
634
+ display = f"{emoji} {name:<30} [{tools_hint}]"
635
+
636
+ choices.append(
637
+ questionary.Choice(
638
+ title=display,
639
+ value=use_case_id,
640
+ ),
641
+ )
642
+ except Exception as e:
643
+ console.print(f"[warning]⚠️ Could not display use case: {e}[/warning]")
644
+
645
+ # Add helpful context before the prompt
646
+ console.print("[dim]Choose a preset that matches your task. Each preset auto-configures tools and capabilities.[/dim]")
647
+ console.print("[dim]You can customize everything in later steps.[/dim]\n")
648
+
649
+ use_case_id = questionary.select(
650
+ "Select your use case:",
651
+ choices=choices,
652
+ style=questionary.Style(
653
+ [
654
+ ("selected", "fg:cyan bold"),
655
+ ("pointer", "fg:cyan bold"),
656
+ ("highlighted", "fg:cyan"),
657
+ ],
658
+ ),
659
+ use_arrow_keys=True,
660
+ ).ask()
661
+
662
+ if use_case_id is None:
663
+ raise KeyboardInterrupt # User cancelled, exit immediately
664
+
665
+ # Show selection with description
666
+ selected_info = self.USE_CASES[use_case_id]
667
+ console.print(f"\n✅ Selected: [green]{selected_info.get('name', use_case_id)}[/green]")
668
+ console.print(f" [dim]{selected_info.get('description', '')}[/dim]")
669
+ console.print(f" [dim cyan]→ Recommended: {selected_info.get('recommended_agents', 1)} agent(s)[/dim cyan]\n")
670
+
671
+ # Show preset information (only if there are special features)
672
+ use_case_details = self.USE_CASES[use_case_id]
673
+ if use_case_details.get("info"):
674
+ preset_panel = Panel(
675
+ use_case_details["info"],
676
+ border_style="cyan",
677
+ title="[bold]Preset Configuration[/bold]",
678
+ width=80,
679
+ padding=(1, 2),
680
+ )
681
+ console.print(preset_panel)
682
+ console.print()
683
+
684
+ return use_case_id
685
+ except (KeyboardInterrupt, EOFError):
686
+ raise # Re-raise to be handled by run()
687
+ except Exception as e:
688
+ console.print(f"[error]❌ Error selecting use case: {e}[/error]")
689
+ console.print("[info]Defaulting to 'qa' use case[/info]\n")
690
+ return "qa" # Safe default
691
+
692
+ def add_custom_mcp_server(self) -> Optional[Dict]:
693
+ """Interactive flow to configure a custom MCP server.
694
+
695
+ Returns:
696
+ MCP server configuration dict, or None if cancelled
697
+ """
698
+ try:
699
+ console.print("\n[bold cyan]Configure Custom MCP Server[/bold cyan]\n")
700
+
701
+ # Name
702
+ name = questionary.text(
703
+ "Server name (identifier):",
704
+ validate=lambda x: len(x) > 0,
705
+ ).ask()
706
+
707
+ if not name:
708
+ return None
709
+
710
+ # Type
711
+ server_type = questionary.select(
712
+ "Server type:",
713
+ choices=[
714
+ questionary.Choice("stdio (standard input/output)", value="stdio"),
715
+ questionary.Choice("sse (server-sent events)", value="sse"),
716
+ questionary.Choice("Custom type", value="custom"),
717
+ ],
718
+ default="stdio",
719
+ style=questionary.Style(
720
+ [
721
+ ("selected", "fg:cyan bold"),
722
+ ("pointer", "fg:cyan bold"),
723
+ ("highlighted", "fg:cyan"),
724
+ ],
725
+ ),
726
+ use_arrow_keys=True,
727
+ ).ask()
728
+
729
+ if server_type == "custom":
730
+ server_type = questionary.text("Enter custom type:").ask()
731
+
732
+ if not server_type:
733
+ server_type = "stdio"
734
+
735
+ # Command
736
+ command = questionary.text(
737
+ "Command:",
738
+ default="npx",
739
+ ).ask()
740
+
741
+ if not command:
742
+ command = "npx"
743
+
744
+ # Args
745
+ args_str = questionary.text(
746
+ "Arguments (space-separated, or empty for none):",
747
+ default="",
748
+ ).ask()
749
+
750
+ args = args_str.split() if args_str else []
751
+
752
+ # Environment variables
753
+ env_vars = {}
754
+ if questionary.confirm("Add environment variables?", default=False).ask():
755
+ console.print("\n[dim]Tip: Use ${VAR_NAME} to reference from .env file[/dim]\n")
756
+ while True:
757
+ var_name = questionary.text(
758
+ "Environment variable name (or press Enter to finish):",
759
+ ).ask()
760
+
761
+ if not var_name:
762
+ break
763
+
764
+ var_value = questionary.text(
765
+ f"Value for {var_name}:",
766
+ default=f"${{{var_name}}}",
767
+ ).ask()
768
+
769
+ if var_value:
770
+ env_vars[var_name] = var_value
771
+
772
+ # Build server config
773
+ mcp_server = {
774
+ "name": name,
775
+ "type": server_type,
776
+ "command": command,
777
+ "args": args,
778
+ }
779
+
780
+ if env_vars:
781
+ mcp_server["env"] = env_vars
782
+
783
+ console.print(f"\n✅ Custom MCP server configured: {name}\n")
784
+ return mcp_server
785
+
786
+ except (KeyboardInterrupt, EOFError):
787
+ console.print("\n[info]Cancelled custom MCP configuration[/info]")
788
+ return None
789
+ except Exception as e:
790
+ console.print(f"[error]❌ Error configuring custom MCP: {e}[/error]")
791
+ return None
792
+
793
+ def batch_create_agents(self, count: int, provider_id: str) -> List[Dict]:
794
+ """Create multiple agents with the same provider.
795
+
796
+ Args:
797
+ count: Number of agents to create
798
+ provider_id: Provider ID (e.g., 'openai', 'claude')
799
+
800
+ Returns:
801
+ List of agent configurations with default models
802
+ """
803
+ agents = []
804
+ provider_info = self.PROVIDERS.get(provider_id, {})
805
+
806
+ # Generate agent IDs like agent_a, agent_b, agent_c...
807
+ for i in range(count):
808
+ # Convert index to letter (0->a, 1->b, 2->c, etc.)
809
+ agent_letter = chr(ord("a") + i)
810
+
811
+ agent = {
812
+ "id": f"agent_{agent_letter}",
813
+ "backend": {
814
+ "type": provider_info.get("type", provider_id),
815
+ "model": provider_info.get("models", ["default"])[0], # Default to first model
816
+ },
817
+ }
818
+
819
+ # Add workspace for Claude Code (use numbers, not letters)
820
+ if provider_info.get("type") == "claude_code":
821
+ agent["backend"]["cwd"] = f"workspace{i + 1}"
822
+
823
+ agents.append(agent)
824
+
825
+ return agents
826
+
827
+ def clone_agent(self, source_agent: Dict, new_id: str) -> Dict:
828
+ """Clone an agent's configuration with a new ID.
829
+
830
+ Args:
831
+ source_agent: Agent to clone
832
+ new_id: New agent ID
833
+
834
+ Returns:
835
+ Cloned agent with updated ID and workspace (if applicable)
836
+ """
837
+ import copy
838
+
839
+ cloned = copy.deepcopy(source_agent)
840
+ cloned["id"] = new_id
841
+
842
+ # Update workspace for Claude Code agents to avoid conflicts
843
+ backend_type = cloned.get("backend", {}).get("type")
844
+ if backend_type == "claude_code" and "cwd" in cloned.get("backend", {}):
845
+ # Extract number from new_id (e.g., "agent_b" -> 2)
846
+ if "_" in new_id and len(new_id) > 0:
847
+ agent_letter = new_id.split("_")[-1]
848
+ if len(agent_letter) == 1 and agent_letter.isalpha():
849
+ agent_num = ord(agent_letter.lower()) - ord("a") + 1
850
+ cloned["backend"]["cwd"] = f"workspace{agent_num}"
851
+
852
+ return cloned
853
+
854
+ def modify_cloned_agent(self, agent: Dict, agent_num: int) -> Dict:
855
+ """Allow selective modification of a cloned agent.
856
+
857
+ Args:
858
+ agent: Cloned agent to modify
859
+ agent_num: Agent number (1-indexed)
860
+
861
+ Returns:
862
+ Modified agent configuration
863
+ """
864
+ try:
865
+ console.print(f"\n[bold cyan]Selective Modification: {agent['id']}[/bold cyan]")
866
+ console.print("[dim]Choose which settings to modify (or press Enter to keep all)[/dim]\n")
867
+
868
+ backend_type = agent.get("backend", {}).get("type")
869
+
870
+ # Find provider info
871
+ provider_info = None
872
+ for pid, pinfo in self.PROVIDERS.items():
873
+ if pinfo.get("type") == backend_type:
874
+ provider_info = pinfo
875
+ break
876
+
877
+ if not provider_info:
878
+ console.print("[warning]⚠️ Could not find provider info[/warning]")
879
+ return agent
880
+
881
+ # Ask what to modify
882
+ modify_choices = questionary.checkbox(
883
+ "What would you like to modify? (Space to select, Enter to confirm)",
884
+ choices=[
885
+ questionary.Choice("Model", value="model"),
886
+ questionary.Choice("Tools (web search, code execution)", value="tools"),
887
+ questionary.Choice("Filesystem settings", value="filesystem"),
888
+ questionary.Choice("MCP servers", value="mcp"),
889
+ ],
890
+ style=questionary.Style(
891
+ [
892
+ ("selected", "fg:cyan"),
893
+ ("pointer", "fg:cyan bold"),
894
+ ("highlighted", "fg:cyan"),
895
+ ],
896
+ ),
897
+ use_arrow_keys=True,
898
+ ).ask()
899
+
900
+ if not modify_choices:
901
+ console.print("✅ Keeping all cloned settings")
902
+ return agent
903
+
904
+ # Modify selected aspects
905
+ if "model" in modify_choices:
906
+ models = provider_info.get("models", [])
907
+ if models:
908
+ current_model = agent["backend"].get("model")
909
+ model_choices = [
910
+ questionary.Choice(
911
+ f"{model}" + (" (current)" if model == current_model else ""),
912
+ value=model,
913
+ )
914
+ for model in models
915
+ ]
916
+
917
+ selected_model = questionary.select(
918
+ f"Select model for {agent['id']}:",
919
+ choices=model_choices,
920
+ default=current_model,
921
+ style=questionary.Style(
922
+ [
923
+ ("selected", "fg:cyan bold"),
924
+ ("pointer", "fg:cyan bold"),
925
+ ("highlighted", "fg:cyan"),
926
+ ],
927
+ ),
928
+ use_arrow_keys=True,
929
+ ).ask()
930
+
931
+ if selected_model:
932
+ agent["backend"]["model"] = selected_model
933
+ console.print(f"✅ Model changed to: {selected_model}")
934
+
935
+ if "tools" in modify_choices:
936
+ supports = provider_info.get("supports", [])
937
+ builtin_tools = [s for s in supports if s in ["web_search", "code_execution", "bash"]]
938
+
939
+ if builtin_tools:
940
+ # Show current tools
941
+ current_tools = []
942
+ if agent["backend"].get("enable_web_search"):
943
+ current_tools.append("web_search")
944
+ if agent["backend"].get("enable_code_interpreter") or agent["backend"].get("enable_code_execution"):
945
+ current_tools.append("code_execution")
946
+
947
+ tool_choices = []
948
+ if "web_search" in builtin_tools:
949
+ tool_choices.append(
950
+ questionary.Choice("Web Search", value="web_search", checked="web_search" in current_tools),
951
+ )
952
+ if "code_execution" in builtin_tools:
953
+ tool_choices.append(
954
+ questionary.Choice("Code Execution", value="code_execution", checked="code_execution" in current_tools),
955
+ )
956
+ if "bash" in builtin_tools:
957
+ tool_choices.append(
958
+ questionary.Choice("Bash/Shell", value="bash", checked="bash" in current_tools),
959
+ )
960
+
961
+ if tool_choices:
962
+ selected_tools = questionary.checkbox(
963
+ "Enable built-in tools:",
964
+ choices=tool_choices,
965
+ style=questionary.Style(
966
+ [
967
+ ("selected", "fg:cyan"),
968
+ ("pointer", "fg:cyan bold"),
969
+ ("highlighted", "fg:cyan"),
970
+ ],
971
+ ),
972
+ use_arrow_keys=True,
973
+ ).ask()
974
+
975
+ # Clear existing tools
976
+ agent["backend"].pop("enable_web_search", None)
977
+ agent["backend"].pop("enable_code_interpreter", None)
978
+ agent["backend"].pop("enable_code_execution", None)
979
+
980
+ # Apply selected tools
981
+ if selected_tools:
982
+ if "web_search" in selected_tools:
983
+ if backend_type in ["openai", "claude", "gemini", "grok", "azure_openai"]:
984
+ agent["backend"]["enable_web_search"] = True
985
+
986
+ if "code_execution" in selected_tools:
987
+ if backend_type == "openai" or backend_type == "azure_openai":
988
+ agent["backend"]["enable_code_interpreter"] = True
989
+ elif backend_type in ["claude", "gemini"]:
990
+ agent["backend"]["enable_code_execution"] = True
991
+
992
+ console.print("✅ Tools updated")
993
+
994
+ if "filesystem" in modify_choices and "filesystem" in provider_info.get("supports", []):
995
+ enable_fs = questionary.confirm(
996
+ "Enable filesystem access?",
997
+ default=bool(agent["backend"].get("cwd")),
998
+ ).ask()
999
+
1000
+ if enable_fs:
1001
+ if backend_type == "claude_code":
1002
+ current_cwd = agent["backend"].get("cwd", f"workspace{agent_num}")
1003
+ custom_cwd = questionary.text(
1004
+ "Workspace directory:",
1005
+ default=current_cwd,
1006
+ ).ask()
1007
+ if custom_cwd:
1008
+ agent["backend"]["cwd"] = custom_cwd
1009
+ else:
1010
+ agent["backend"]["cwd"] = f"workspace{agent_num}"
1011
+ console.print(f"✅ Filesystem enabled: {agent['backend']['cwd']}")
1012
+ else:
1013
+ agent["backend"].pop("cwd", None)
1014
+ console.print("✅ Filesystem disabled")
1015
+
1016
+ if "mcp" in modify_choices and "mcp" in provider_info.get("supports", []):
1017
+ if questionary.confirm("Modify MCP servers?", default=False).ask():
1018
+ # Show current MCP servers
1019
+ current_mcps = agent["backend"].get("mcp_servers", [])
1020
+ if current_mcps:
1021
+ console.print(f"\n[dim]Current MCP servers: {len(current_mcps)}[/dim]")
1022
+ for mcp in current_mcps:
1023
+ console.print(f" • {mcp.get('name', 'unnamed')}")
1024
+
1025
+ if questionary.confirm("Replace with new MCP servers?", default=False).ask():
1026
+ mcp_servers = []
1027
+ while True:
1028
+ custom_server = self.add_custom_mcp_server()
1029
+ if custom_server:
1030
+ mcp_servers.append(custom_server)
1031
+ if not questionary.confirm("Add another MCP server?", default=False).ask():
1032
+ break
1033
+ else:
1034
+ break
1035
+
1036
+ if mcp_servers:
1037
+ agent["backend"]["mcp_servers"] = mcp_servers
1038
+ console.print(f"✅ MCP servers updated: {len(mcp_servers)} server(s)")
1039
+ else:
1040
+ agent["backend"].pop("mcp_servers", None)
1041
+ console.print("✅ MCP servers removed")
1042
+
1043
+ console.print(f"\n✅ [green]Agent {agent['id']} modified[/green]\n")
1044
+ return agent
1045
+
1046
+ except (KeyboardInterrupt, EOFError):
1047
+ raise
1048
+ except Exception as e:
1049
+ console.print(f"[error]❌ Error modifying agent: {e}[/error]")
1050
+ return agent
1051
+
1052
+ def apply_preset_to_agent(self, agent: Dict, use_case: str) -> Dict:
1053
+ """Auto-apply preset configuration to an agent.
1054
+
1055
+ Args:
1056
+ agent: Agent configuration dict
1057
+ use_case: Use case ID for preset configuration
1058
+
1059
+ Returns:
1060
+ Updated agent configuration with preset applied
1061
+ """
1062
+ if use_case == "custom":
1063
+ return agent
1064
+
1065
+ use_case_info = self.USE_CASES.get(use_case, {})
1066
+ recommended_tools = use_case_info.get("recommended_tools", [])
1067
+
1068
+ backend_type = agent.get("backend", {}).get("type")
1069
+ provider_info = None
1070
+
1071
+ # Find provider info
1072
+ for pid, pinfo in self.PROVIDERS.items():
1073
+ if pinfo.get("type") == backend_type:
1074
+ provider_info = pinfo
1075
+ break
1076
+
1077
+ if not provider_info:
1078
+ return agent
1079
+
1080
+ # Auto-enable filesystem if recommended
1081
+ if "filesystem" in recommended_tools and "filesystem" in provider_info.get("supports", []):
1082
+ if not agent["backend"].get("cwd"):
1083
+ agent["backend"]["cwd"] = "workspace"
1084
+
1085
+ # Auto-enable web search if recommended
1086
+ if "web_search" in recommended_tools:
1087
+ if backend_type in ["openai", "claude", "gemini", "grok", "azure_openai"]:
1088
+ agent["backend"]["enable_web_search"] = True
1089
+
1090
+ # Auto-enable code execution if recommended
1091
+ if "code_execution" in recommended_tools:
1092
+ if backend_type == "openai" or backend_type == "azure_openai":
1093
+ agent["backend"]["enable_code_interpreter"] = True
1094
+ elif backend_type in ["claude", "gemini"]:
1095
+ agent["backend"]["enable_code_execution"] = True
1096
+
1097
+ # Auto-enable Docker for Docker preset
1098
+ if use_case == "coding_docker" and agent["backend"].get("cwd"):
1099
+ agent["backend"]["enable_mcp_command_line"] = True
1100
+ agent["backend"]["command_line_execution_mode"] = "docker"
1101
+
1102
+ # Note: image_understanding, audio_understanding, video_understanding, and reasoning
1103
+ # are passive capabilities - they work automatically when the backend supports them
1104
+ # and when appropriate content (images/audio/video) is provided in messages.
1105
+ # No explicit backend configuration flags needed.
1106
+
1107
+ return agent
1108
+
1109
+ def customize_agent(self, agent: Dict, agent_num: int, total_agents: int, use_case: Optional[str] = None) -> Dict:
1110
+ """Customize a single agent with Panel UI.
1111
+
1112
+ Args:
1113
+ agent: Agent configuration dict
1114
+ agent_num: Agent number (1-indexed)
1115
+ total_agents: Total number of agents
1116
+ use_case: Use case ID for preset recommendations
1117
+
1118
+ Returns:
1119
+ Updated agent configuration
1120
+ """
1121
+ try:
1122
+ backend_type = agent.get("backend", {}).get("type")
1123
+ provider_info = None
1124
+
1125
+ # Find provider info
1126
+ for pid, pinfo in self.PROVIDERS.items():
1127
+ if pinfo.get("type") == backend_type:
1128
+ provider_info = pinfo
1129
+ break
1130
+
1131
+ if not provider_info:
1132
+ console.print(f"[warning]⚠️ Could not find provider for {backend_type}[/warning]")
1133
+ return agent
1134
+
1135
+ # Create Panel for this agent
1136
+ panel_content = []
1137
+ panel_content.append(f"[bold]Agent {agent_num} of {total_agents}: {agent['id']}[/bold]\n")
1138
+
1139
+ # Model selection
1140
+ models = provider_info.get("models", [])
1141
+ if models:
1142
+ current_model = agent["backend"].get("model")
1143
+ panel_content.append(f"[cyan]Current model:[/cyan] {current_model}")
1144
+
1145
+ console.print(Panel("\n".join(panel_content), border_style="cyan", width=80))
1146
+ console.print()
1147
+
1148
+ model_choices = [
1149
+ questionary.Choice(
1150
+ f"{model}" + (" (current)" if model == current_model else ""),
1151
+ value=model,
1152
+ )
1153
+ for model in models
1154
+ ]
1155
+
1156
+ selected_model = questionary.select(
1157
+ f"Select model for {agent['id']}:",
1158
+ choices=model_choices,
1159
+ default=current_model,
1160
+ style=questionary.Style(
1161
+ [
1162
+ ("selected", "fg:cyan bold"),
1163
+ ("pointer", "fg:cyan bold"),
1164
+ ("highlighted", "fg:cyan"),
1165
+ ],
1166
+ ),
1167
+ use_arrow_keys=True,
1168
+ ).ask()
1169
+
1170
+ if selected_model:
1171
+ agent["backend"]["model"] = selected_model
1172
+ console.print(f"\n✓ Model set to {selected_model}")
1173
+
1174
+ # Configure text verbosity for OpenAI models only
1175
+ if backend_type in ["openai", "azure_openai"]:
1176
+ console.print("\n[dim]Configure text verbosity:[/dim]")
1177
+ console.print("[dim] • low: Concise responses[/dim]")
1178
+ console.print("[dim] • medium: Balanced detail (recommended)[/dim]")
1179
+ console.print("[dim] • high: Detailed, verbose responses[/dim]\n")
1180
+
1181
+ verbosity_choice = questionary.select(
1182
+ "Text verbosity level:",
1183
+ choices=[
1184
+ questionary.Choice("Low (concise)", value="low"),
1185
+ questionary.Choice("Medium (recommended)", value="medium"),
1186
+ questionary.Choice("High (detailed)", value="high"),
1187
+ ],
1188
+ default="medium",
1189
+ style=questionary.Style(
1190
+ [
1191
+ ("selected", "fg:cyan bold"),
1192
+ ("pointer", "fg:cyan bold"),
1193
+ ("highlighted", "fg:cyan"),
1194
+ ],
1195
+ ),
1196
+ use_arrow_keys=True,
1197
+ ).ask()
1198
+
1199
+ agent["backend"]["text"] = {
1200
+ "verbosity": verbosity_choice if verbosity_choice else "medium",
1201
+ }
1202
+ console.print(f"✓ Text verbosity set to: {verbosity_choice if verbosity_choice else 'medium'}\n")
1203
+
1204
+ # Auto-add reasoning params for GPT-5 and o-series models
1205
+ if selected_model in ["gpt-5", "gpt-5-mini", "gpt-5-nano", "o4", "o4-mini"]:
1206
+ console.print("[dim]This model supports extended reasoning. Configure reasoning effort:[/dim]")
1207
+ console.print("[dim] • high: Maximum reasoning depth (slower, more thorough)[/dim]")
1208
+ console.print("[dim] • medium: Balanced reasoning (recommended)[/dim]")
1209
+ console.print("[dim] • low: Faster responses with basic reasoning[/dim]\n")
1210
+
1211
+ # Determine default based on model
1212
+ if selected_model in ["gpt-5", "o4"]:
1213
+ default_effort = "medium" # Changed from high to medium
1214
+ elif selected_model in ["gpt-5-mini", "o4-mini"]:
1215
+ default_effort = "medium"
1216
+ else: # gpt-5-nano
1217
+ default_effort = "low"
1218
+
1219
+ effort_choice = questionary.select(
1220
+ "Reasoning effort level:",
1221
+ choices=[
1222
+ questionary.Choice("High (maximum depth)", value="high"),
1223
+ questionary.Choice("Medium (balanced - recommended)", value="medium"),
1224
+ questionary.Choice("Low (faster)", value="low"),
1225
+ ],
1226
+ default=default_effort,
1227
+ style=questionary.Style(
1228
+ [
1229
+ ("selected", "fg:cyan bold"),
1230
+ ("pointer", "fg:cyan bold"),
1231
+ ("highlighted", "fg:cyan"),
1232
+ ],
1233
+ ),
1234
+ use_arrow_keys=True,
1235
+ ).ask()
1236
+
1237
+ agent["backend"]["reasoning"] = {
1238
+ "effort": effort_choice if effort_choice else default_effort,
1239
+ "summary": "auto",
1240
+ }
1241
+ console.print(f"✓ Reasoning effort set to: {effort_choice if effort_choice else default_effort}\n")
1242
+ else:
1243
+ console.print(Panel("\n".join(panel_content), border_style="cyan", width=80))
1244
+
1245
+ # Filesystem access (native or via MCP)
1246
+ if "filesystem" in provider_info.get("supports", []):
1247
+ console.print()
1248
+
1249
+ # Get filesystem support type from capabilities
1250
+ caps = get_capabilities(backend_type)
1251
+ fs_type = caps.filesystem_support if caps else "mcp"
1252
+
1253
+ # Claude Code ALWAYS has filesystem access (that's what makes it special!)
1254
+ if backend_type == "claude_code":
1255
+ # Filesystem is always enabled for Claude Code
1256
+ current_cwd = agent["backend"].get("cwd", "workspace")
1257
+ console.print("[dim]Claude Code has native filesystem access (always enabled)[/dim]")
1258
+ console.print(f"[dim]Current workspace: {current_cwd}[/dim]")
1259
+
1260
+ if questionary.confirm("Customize workspace directory?", default=False).ask():
1261
+ custom_cwd = questionary.text(
1262
+ "Enter workspace directory:",
1263
+ default=current_cwd,
1264
+ ).ask()
1265
+ if custom_cwd:
1266
+ agent["backend"]["cwd"] = custom_cwd
1267
+
1268
+ console.print(f"✅ Filesystem access: {agent['backend']['cwd']} (native)")
1269
+
1270
+ # Ask about Docker bash execution for Claude Code
1271
+ console.print()
1272
+ console.print("[dim]Claude Code bash execution mode:[/dim]")
1273
+ console.print("[dim] • local: Run bash commands directly on your machine (default)[/dim]")
1274
+ console.print("[dim] • docker: Run bash in isolated Docker container (requires Docker setup)[/dim]")
1275
+
1276
+ enable_docker = questionary.confirm(
1277
+ "Enable Docker bash execution? (requires Docker setup)",
1278
+ default=(use_case == "coding_docker"),
1279
+ ).ask()
1280
+
1281
+ if enable_docker:
1282
+ agent["backend"]["enable_mcp_command_line"] = True
1283
+ agent["backend"]["command_line_execution_mode"] = "docker"
1284
+ console.print("🐳 Docker bash execution enabled")
1285
+ else:
1286
+ console.print("💻 Local bash execution enabled (default)")
1287
+ else:
1288
+ # For non-Claude Code backends
1289
+ # Check if filesystem is recommended in the preset
1290
+ filesystem_recommended = False
1291
+ if use_case and use_case != "custom":
1292
+ use_case_info = self.USE_CASES.get(use_case, {})
1293
+ filesystem_recommended = "filesystem" in use_case_info.get("recommended_tools", [])
1294
+
1295
+ if fs_type == "native":
1296
+ console.print("[dim]This backend has native filesystem support[/dim]")
1297
+ else:
1298
+ console.print("[dim]This backend supports filesystem operations via MCP[/dim]")
1299
+
1300
+ if filesystem_recommended:
1301
+ console.print("[dim]💡 Filesystem access recommended for this preset[/dim]")
1302
+
1303
+ # Auto-enable for Docker preset
1304
+ enable_filesystem = filesystem_recommended
1305
+ if not filesystem_recommended:
1306
+ enable_filesystem = questionary.confirm("Enable filesystem access for this agent?", default=True).ask()
1307
+
1308
+ if enable_filesystem:
1309
+ # For MCP-based filesystem, set cwd parameter
1310
+ if not agent["backend"].get("cwd"):
1311
+ # Use agent index for workspace naming
1312
+ agent["backend"]["cwd"] = f"workspace{agent_num}"
1313
+
1314
+ console.print(f"✅ Filesystem access enabled (via MCP): {agent['backend']['cwd']}")
1315
+
1316
+ # Enable Docker execution mode for Docker preset
1317
+ if use_case == "coding_docker":
1318
+ agent["backend"]["enable_mcp_command_line"] = True
1319
+ agent["backend"]["command_line_execution_mode"] = "docker"
1320
+ console.print("🐳 Docker execution mode enabled for isolated code execution")
1321
+
1322
+ # Built-in tools (backend-specific capabilities)
1323
+ # Skip for Claude Code - bash is always available, already configured above
1324
+ if backend_type != "claude_code":
1325
+ supports = provider_info.get("supports", [])
1326
+ builtin_tools = [s for s in supports if s in ["web_search", "code_execution", "bash"]]
1327
+
1328
+ # Get recommended tools from use case
1329
+ recommended_tools = []
1330
+ if use_case:
1331
+ use_case_info = self.USE_CASES.get(use_case, {})
1332
+ recommended_tools = use_case_info.get("recommended_tools", [])
1333
+
1334
+ if builtin_tools:
1335
+ console.print()
1336
+
1337
+ # Show preset info if this is a preset use case
1338
+ if recommended_tools and use_case != "custom":
1339
+ console.print(f"[dim]💡 Preset recommendation: {', '.join(recommended_tools)}[/dim]")
1340
+
1341
+ tool_choices = []
1342
+
1343
+ if "web_search" in builtin_tools:
1344
+ tool_choices.append(questionary.Choice("Web Search", value="web_search", checked="web_search" in recommended_tools))
1345
+ if "code_execution" in builtin_tools:
1346
+ tool_choices.append(questionary.Choice("Code Execution", value="code_execution", checked="code_execution" in recommended_tools))
1347
+ if "bash" in builtin_tools:
1348
+ tool_choices.append(questionary.Choice("Bash/Shell", value="bash", checked="bash" in recommended_tools))
1349
+
1350
+ if tool_choices:
1351
+ selected_tools = questionary.checkbox(
1352
+ "Enable built-in tools for this agent (Space to select, Enter to confirm):",
1353
+ choices=tool_choices,
1354
+ style=questionary.Style(
1355
+ [
1356
+ ("selected", "fg:cyan"),
1357
+ ("pointer", "fg:cyan bold"),
1358
+ ("highlighted", "fg:cyan"),
1359
+ ],
1360
+ ),
1361
+ use_arrow_keys=True,
1362
+ ).ask()
1363
+
1364
+ if selected_tools:
1365
+ # Apply backend-specific configuration
1366
+ if "web_search" in selected_tools:
1367
+ if backend_type in ["openai", "claude", "gemini", "grok", "azure_openai"]:
1368
+ agent["backend"]["enable_web_search"] = True
1369
+
1370
+ if "code_execution" in selected_tools:
1371
+ if backend_type == "openai" or backend_type == "azure_openai":
1372
+ agent["backend"]["enable_code_interpreter"] = True
1373
+ elif backend_type in ["claude", "gemini"]:
1374
+ agent["backend"]["enable_code_execution"] = True
1375
+
1376
+ console.print(f"✅ Enabled {len(selected_tools)} built-in tool(s)")
1377
+
1378
+ # Multimodal capabilities (passive - no config needed)
1379
+ supports = provider_info.get("supports", [])
1380
+ multimodal_caps = [s for s in supports if s in ["image_understanding", "audio_understanding", "video_understanding", "reasoning"]]
1381
+
1382
+ # Show multimodal capabilities info (passive - no config needed)
1383
+ if multimodal_caps:
1384
+ console.print()
1385
+ console.print("[dim]📷 This backend also supports (no configuration needed):[/dim]")
1386
+ if "image_understanding" in multimodal_caps:
1387
+ console.print("[dim] • Image understanding (analyze images, charts, screenshots)[/dim]")
1388
+ if "audio_understanding" in multimodal_caps:
1389
+ console.print("[dim] • Audio understanding (transcribe and analyze audio)[/dim]")
1390
+ if "video_understanding" in multimodal_caps:
1391
+ console.print("[dim] • Video understanding (analyze video content)[/dim]")
1392
+ if "reasoning" in multimodal_caps:
1393
+ console.print("[dim] • Extended reasoning (deep thinking for complex problems)[/dim]")
1394
+
1395
+ # Generation capabilities (optional tools that require explicit flags)
1396
+ generation_caps = [s for s in supports if s in ["image_generation", "audio_generation", "video_generation"]]
1397
+
1398
+ if generation_caps:
1399
+ console.print()
1400
+ console.print("[cyan]Optional generation capabilities (requires explicit enablement):[/cyan]")
1401
+
1402
+ gen_choices = []
1403
+ if "image_generation" in generation_caps:
1404
+ gen_choices.append(questionary.Choice("Image Generation (DALL-E, etc.)", value="image_generation", checked=False))
1405
+ if "audio_generation" in generation_caps:
1406
+ gen_choices.append(questionary.Choice("Audio Generation (TTS, music, etc.)", value="audio_generation", checked=False))
1407
+ if "video_generation" in generation_caps:
1408
+ gen_choices.append(questionary.Choice("Video Generation (Sora, etc.)", value="video_generation", checked=False))
1409
+
1410
+ if gen_choices:
1411
+ selected_gen = questionary.checkbox(
1412
+ "Enable generation capabilities (Space to select, Enter to confirm):",
1413
+ choices=gen_choices,
1414
+ style=questionary.Style(
1415
+ [
1416
+ ("selected", "fg:cyan"),
1417
+ ("pointer", "fg:cyan bold"),
1418
+ ("highlighted", "fg:cyan"),
1419
+ ],
1420
+ ),
1421
+ use_arrow_keys=True,
1422
+ ).ask()
1423
+
1424
+ if selected_gen:
1425
+ if "image_generation" in selected_gen:
1426
+ agent["backend"]["enable_image_generation"] = True
1427
+ if "audio_generation" in selected_gen:
1428
+ agent["backend"]["enable_audio_generation"] = True
1429
+ if "video_generation" in selected_gen:
1430
+ agent["backend"]["enable_video_generation"] = True
1431
+
1432
+ console.print(f"✅ Enabled {len(selected_gen)} generation capability(ies)")
1433
+
1434
+ # MCP servers (custom only)
1435
+ # Note: Filesystem is handled internally above, NOT as external MCP
1436
+ if "mcp" in provider_info.get("supports", []):
1437
+ console.print()
1438
+ console.print("[dim]MCP servers are external integrations. Filesystem is handled internally (configured above).[/dim]")
1439
+
1440
+ if questionary.confirm("Add custom MCP servers?", default=False).ask():
1441
+ mcp_servers = []
1442
+ while True:
1443
+ custom_server = self.add_custom_mcp_server()
1444
+ if custom_server:
1445
+ mcp_servers.append(custom_server)
1446
+
1447
+ # Ask if they want to add another
1448
+ if not questionary.confirm("Add another custom MCP server?", default=False).ask():
1449
+ break
1450
+ else:
1451
+ break
1452
+
1453
+ # Add to agent config if any MCPs were configured
1454
+ if mcp_servers:
1455
+ agent["backend"]["mcp_servers"] = mcp_servers
1456
+ console.print(f"\n✅ Total: {len(mcp_servers)} MCP server(s) configured for this agent\n")
1457
+
1458
+ console.print(f"✅ [green]Agent {agent_num} configured[/green]\n")
1459
+ return agent
1460
+
1461
+ except (KeyboardInterrupt, EOFError):
1462
+ raise
1463
+ except Exception as e:
1464
+ console.print(f"[error]❌ Error customizing agent: {e}[/error]")
1465
+ return agent
1466
+
1467
+ def configure_agents(self, use_case: str, api_keys: Dict[str, bool]) -> List[Dict]:
1468
+ """Configure agents with batch creation and individual customization."""
1469
+ try:
1470
+ # Step header
1471
+ step_panel = Panel(
1472
+ "[bold cyan]Step 2 of 4: Agent Setup[/bold cyan]\n\n[italic dim]Choose any provider(s) - all types work for your selected use case[/italic dim]",
1473
+ border_style="cyan",
1474
+ padding=(0, 2),
1475
+ width=80,
1476
+ )
1477
+ console.print(step_panel)
1478
+ console.print()
1479
+
1480
+ # Show available providers now (right when users need to select them)
1481
+ self.show_available_providers(api_keys)
1482
+
1483
+ use_case_info = self.USE_CASES.get(use_case, {})
1484
+ recommended = use_case_info.get("recommended_agents", 1)
1485
+
1486
+ # Step 2a: How many agents?
1487
+ console.print(f" 💡 [dim]Recommended for this use case: {recommended} agent(s)[/dim]")
1488
+ console.print()
1489
+
1490
+ # Build choices with proper default handling
1491
+ num_choices = [
1492
+ questionary.Choice("1 agent", value=1),
1493
+ questionary.Choice("2 agents", value=2),
1494
+ questionary.Choice("3 agents (recommended for diverse perspectives)", value=3),
1495
+ questionary.Choice("4 agents", value=4),
1496
+ questionary.Choice("5 agents", value=5),
1497
+ questionary.Choice("Custom number", value="custom"),
1498
+ ]
1499
+
1500
+ # Find the default choice by value
1501
+ default_choice = None
1502
+ for choice in num_choices:
1503
+ if choice.value == recommended:
1504
+ default_choice = choice.value
1505
+ break
1506
+
1507
+ try:
1508
+ num_agents_choice = questionary.select(
1509
+ "How many agents?",
1510
+ choices=num_choices,
1511
+ default=default_choice,
1512
+ style=questionary.Style(
1513
+ [
1514
+ ("selected", "fg:cyan bold"),
1515
+ ("pointer", "fg:cyan bold"),
1516
+ ("highlighted", "fg:cyan"),
1517
+ ],
1518
+ ),
1519
+ use_arrow_keys=True,
1520
+ ).ask()
1521
+
1522
+ if num_agents_choice is None:
1523
+ raise KeyboardInterrupt # User cancelled
1524
+
1525
+ if num_agents_choice == "custom":
1526
+ num_agents_text = questionary.text(
1527
+ "Enter number of agents:",
1528
+ validate=lambda x: x.isdigit() and int(x) > 0,
1529
+ ).ask()
1530
+ if num_agents_text is None:
1531
+ raise KeyboardInterrupt # User cancelled
1532
+ num_agents = int(num_agents_text) if num_agents_text else recommended
1533
+ else:
1534
+ num_agents = num_agents_choice
1535
+ except Exception as e:
1536
+ console.print(f"[warning]⚠️ Error with selection: {e}[/warning]")
1537
+ console.print(f"[info]Using recommended: {recommended} agents[/info]")
1538
+ num_agents = recommended
1539
+
1540
+ if num_agents < 1:
1541
+ console.print("[warning]⚠️ Number of agents must be at least 1. Setting to 1.[/warning]")
1542
+ num_agents = 1
1543
+
1544
+ available_providers = [p for p, has_key in api_keys.items() if has_key]
1545
+
1546
+ if not available_providers:
1547
+ console.print("[error]❌ No providers with API keys found. Please set at least one API key.[/error]")
1548
+ raise ValueError("No providers available")
1549
+
1550
+ # Step 2b: Same provider or mix?
1551
+ agents = []
1552
+ if num_agents == 1:
1553
+ # Single agent - just pick provider directly
1554
+ console.print()
1555
+
1556
+ provider_choices = [
1557
+ questionary.Choice(
1558
+ self.PROVIDERS.get(pid, {}).get("name", pid),
1559
+ value=pid,
1560
+ )
1561
+ for pid in available_providers
1562
+ ]
1563
+
1564
+ provider_id = questionary.select(
1565
+ "Select provider:",
1566
+ choices=provider_choices,
1567
+ style=questionary.Style(
1568
+ [
1569
+ ("selected", "fg:cyan bold"),
1570
+ ("pointer", "fg:cyan bold"),
1571
+ ("highlighted", "fg:cyan"),
1572
+ ],
1573
+ ),
1574
+ use_arrow_keys=True,
1575
+ ).ask()
1576
+
1577
+ if provider_id is None:
1578
+ raise KeyboardInterrupt # User cancelled
1579
+
1580
+ agents = self.batch_create_agents(1, provider_id)
1581
+ provider_name = self.PROVIDERS.get(provider_id, {}).get("name", provider_id)
1582
+ console.print()
1583
+ console.print(f" ✅ Created 1 {provider_name} agent")
1584
+ console.print()
1585
+
1586
+ else:
1587
+ # Multiple agents - ask if same or different providers
1588
+ console.print()
1589
+
1590
+ setup_mode = questionary.select(
1591
+ "Setup mode:",
1592
+ choices=[
1593
+ questionary.Choice("Same provider for all agents (quick setup)", value="same"),
1594
+ questionary.Choice("Mix different providers (advanced)", value="mix"),
1595
+ ],
1596
+ style=questionary.Style(
1597
+ [
1598
+ ("selected", "fg:cyan bold"),
1599
+ ("pointer", "fg:cyan bold"),
1600
+ ("highlighted", "fg:cyan"),
1601
+ ],
1602
+ ),
1603
+ use_arrow_keys=True,
1604
+ ).ask()
1605
+
1606
+ if setup_mode is None:
1607
+ raise KeyboardInterrupt # User cancelled
1608
+
1609
+ if setup_mode == "same":
1610
+ # Batch creation with same provider
1611
+ console.print()
1612
+
1613
+ provider_choices = [
1614
+ questionary.Choice(
1615
+ self.PROVIDERS.get(pid, {}).get("name", pid),
1616
+ value=pid,
1617
+ )
1618
+ for pid in available_providers
1619
+ ]
1620
+
1621
+ provider_id = questionary.select(
1622
+ "Select provider:",
1623
+ choices=provider_choices,
1624
+ style=questionary.Style(
1625
+ [
1626
+ ("selected", "fg:cyan bold"),
1627
+ ("pointer", "fg:cyan bold"),
1628
+ ("highlighted", "fg:cyan"),
1629
+ ],
1630
+ ),
1631
+ use_arrow_keys=True,
1632
+ ).ask()
1633
+
1634
+ if provider_id is None:
1635
+ raise KeyboardInterrupt # User cancelled
1636
+
1637
+ agents = self.batch_create_agents(num_agents, provider_id)
1638
+ provider_name = self.PROVIDERS.get(provider_id, {}).get("name", provider_id)
1639
+ console.print()
1640
+ console.print(f" ✅ Created {num_agents} {provider_name} agents")
1641
+ console.print()
1642
+
1643
+ else:
1644
+ # Advanced: mix providers
1645
+ console.print()
1646
+ console.print("[yellow] 💡 Advanced mode: Configure each agent individually[/yellow]")
1647
+ console.print()
1648
+ for i in range(num_agents):
1649
+ try:
1650
+ console.print(f"[bold cyan]Agent {i + 1} of {num_agents}:[/bold cyan]")
1651
+
1652
+ provider_choices = [
1653
+ questionary.Choice(
1654
+ self.PROVIDERS.get(pid, {}).get("name", pid),
1655
+ value=pid,
1656
+ )
1657
+ for pid in available_providers
1658
+ ]
1659
+
1660
+ provider_id = questionary.select(
1661
+ f"Select provider for agent {i + 1}:",
1662
+ choices=provider_choices,
1663
+ style=questionary.Style(
1664
+ [
1665
+ ("selected", "fg:cyan bold"),
1666
+ ("pointer", "fg:cyan bold"),
1667
+ ("highlighted", "fg:cyan"),
1668
+ ],
1669
+ ),
1670
+ use_arrow_keys=True,
1671
+ ).ask()
1672
+
1673
+ if not provider_id:
1674
+ provider_id = available_providers[0]
1675
+
1676
+ agent_batch = self.batch_create_agents(1, provider_id)
1677
+ agents.extend(agent_batch)
1678
+
1679
+ provider_name = self.PROVIDERS.get(provider_id, {}).get("name", provider_id)
1680
+ console.print(f"✅ Agent {i + 1} created: {provider_name}\n")
1681
+
1682
+ except (KeyboardInterrupt, EOFError):
1683
+ raise
1684
+ except Exception as e:
1685
+ console.print(f"[error]❌ Error configuring agent {i + 1}: {e}[/error]")
1686
+ console.print("[info]Skipping this agent...[/info]")
1687
+
1688
+ if not agents:
1689
+ console.print("[error]❌ No agents were successfully configured.[/error]")
1690
+ raise ValueError("Failed to configure any agents")
1691
+
1692
+ # Step 2c: Model selection and preset application
1693
+ # Step header
1694
+ step_panel = Panel(
1695
+ "[bold cyan]Step 3 of 4: Agent Configuration[/bold cyan]",
1696
+ border_style="cyan",
1697
+ padding=(0, 2),
1698
+ width=80,
1699
+ )
1700
+ console.print(step_panel)
1701
+ console.print()
1702
+
1703
+ # For non-custom presets, show info and configure models
1704
+ if use_case != "custom":
1705
+ use_case_info = self.USE_CASES.get(use_case, {})
1706
+ recommended_tools = use_case_info.get("recommended_tools", [])
1707
+
1708
+ console.print(f" [bold green]✓ Preset Selected:[/bold green] {use_case_info.get('name', use_case)}")
1709
+ console.print(f" [dim]{use_case_info.get('description', '')}[/dim]")
1710
+ console.print()
1711
+
1712
+ if recommended_tools:
1713
+ console.print(" [cyan]This preset will auto-configure:[/cyan]")
1714
+ for tool in recommended_tools:
1715
+ tool_display = {
1716
+ "filesystem": "📁 Filesystem access",
1717
+ "code_execution": "💻 Code execution",
1718
+ "web_search": "🔍 Web search",
1719
+ "mcp": "🔌 MCP servers",
1720
+ }.get(tool, tool)
1721
+ console.print(f" • {tool_display}")
1722
+
1723
+ if use_case == "coding_docker":
1724
+ console.print(" • 🐳 Docker isolated execution")
1725
+
1726
+ console.print()
1727
+
1728
+ # Let users select models for each agent
1729
+ console.print(" [cyan]Select models for your agents:[/cyan]")
1730
+ console.print()
1731
+ for i, agent in enumerate(agents, 1):
1732
+ backend_type = agent.get("backend", {}).get("type")
1733
+ provider_info = None
1734
+
1735
+ # Find provider info
1736
+ for pid, pinfo in self.PROVIDERS.items():
1737
+ if pinfo.get("type") == backend_type:
1738
+ provider_info = pinfo
1739
+ break
1740
+
1741
+ if provider_info:
1742
+ models = provider_info.get("models", [])
1743
+ if models and len(models) > 1:
1744
+ current_model = agent["backend"].get("model")
1745
+ console.print(f"[bold]Agent {i} ({agent['id']}) - {provider_info.get('name')}:[/bold]")
1746
+
1747
+ model_choices = [
1748
+ questionary.Choice(
1749
+ f"{model}" + (" (default)" if model == current_model else ""),
1750
+ value=model,
1751
+ )
1752
+ for model in models
1753
+ ]
1754
+
1755
+ selected_model = questionary.select(
1756
+ "Select model:",
1757
+ choices=model_choices,
1758
+ default=current_model,
1759
+ style=questionary.Style(
1760
+ [
1761
+ ("selected", "fg:cyan bold"),
1762
+ ("pointer", "fg:cyan bold"),
1763
+ ("highlighted", "fg:cyan"),
1764
+ ],
1765
+ ),
1766
+ use_arrow_keys=True,
1767
+ ).ask()
1768
+
1769
+ if selected_model:
1770
+ agent["backend"]["model"] = selected_model
1771
+ console.print(f" ✓ {selected_model}")
1772
+
1773
+ # Configure text verbosity for OpenAI models only
1774
+ if backend_type in ["openai", "azure_openai"]:
1775
+ console.print("\n [dim]Configure text verbosity:[/dim]")
1776
+ console.print(" [dim]• low: Concise responses[/dim]")
1777
+ console.print(" [dim]• medium: Balanced detail (recommended)[/dim]")
1778
+ console.print(" [dim]• high: Detailed, verbose responses[/dim]\n")
1779
+
1780
+ verbosity_choice = questionary.select(
1781
+ " Text verbosity:",
1782
+ choices=[
1783
+ questionary.Choice("Low (concise)", value="low"),
1784
+ questionary.Choice("Medium (recommended)", value="medium"),
1785
+ questionary.Choice("High (detailed)", value="high"),
1786
+ ],
1787
+ default="medium",
1788
+ style=questionary.Style(
1789
+ [
1790
+ ("selected", "fg:cyan bold"),
1791
+ ("pointer", "fg:cyan bold"),
1792
+ ("highlighted", "fg:cyan"),
1793
+ ],
1794
+ ),
1795
+ use_arrow_keys=True,
1796
+ ).ask()
1797
+
1798
+ agent["backend"]["text"] = {
1799
+ "verbosity": verbosity_choice if verbosity_choice else "medium",
1800
+ }
1801
+ console.print(f" ✓ Text verbosity: {verbosity_choice if verbosity_choice else 'medium'}\n")
1802
+
1803
+ # Auto-add reasoning params for GPT-5 and o-series models
1804
+ if selected_model in ["gpt-5", "gpt-5-mini", "gpt-5-nano", "o4", "o4-mini"]:
1805
+ console.print(" [dim]Configure reasoning effort:[/dim]")
1806
+ console.print(" [dim]• high: Maximum depth (slower)[/dim]")
1807
+ console.print(" [dim]• medium: Balanced (recommended)[/dim]")
1808
+ console.print(" [dim]• low: Faster responses[/dim]\n")
1809
+
1810
+ # Determine default based on model
1811
+ if selected_model in ["gpt-5", "o4"]:
1812
+ default_effort = "medium" # Changed from high to medium
1813
+ elif selected_model in ["gpt-5-mini", "o4-mini"]:
1814
+ default_effort = "medium"
1815
+ else: # gpt-5-nano
1816
+ default_effort = "low"
1817
+
1818
+ effort_choice = questionary.select(
1819
+ " Reasoning effort:",
1820
+ choices=[
1821
+ questionary.Choice("High", value="high"),
1822
+ questionary.Choice("Medium (recommended)", value="medium"),
1823
+ questionary.Choice("Low", value="low"),
1824
+ ],
1825
+ default=default_effort,
1826
+ style=questionary.Style(
1827
+ [
1828
+ ("selected", "fg:cyan bold"),
1829
+ ("pointer", "fg:cyan bold"),
1830
+ ("highlighted", "fg:cyan"),
1831
+ ],
1832
+ ),
1833
+ use_arrow_keys=True,
1834
+ ).ask()
1835
+
1836
+ agent["backend"]["reasoning"] = {
1837
+ "effort": effort_choice if effort_choice else default_effort,
1838
+ "summary": "auto",
1839
+ }
1840
+ console.print(f" ✓ Reasoning effort: {effort_choice if effort_choice else default_effort}\n")
1841
+
1842
+ # Auto-apply preset to all agents
1843
+ console.print()
1844
+ console.print(" [cyan]Applying preset configuration to all agents...[/cyan]")
1845
+ for i, agent in enumerate(agents):
1846
+ agents[i] = self.apply_preset_to_agent(agent, use_case)
1847
+
1848
+ console.print(f" [green]✅ {len(agents)} agent(s) configured with preset[/green]")
1849
+ console.print()
1850
+
1851
+ # Ask if user wants additional customization
1852
+ customize_choice = Confirm.ask("\n [prompt]Further customize agent settings (advanced)?[/prompt]", default=False)
1853
+ if customize_choice is None:
1854
+ raise KeyboardInterrupt # User cancelled
1855
+ if customize_choice:
1856
+ console.print()
1857
+ console.print(" [cyan]Entering advanced customization...[/cyan]")
1858
+ console.print()
1859
+ for i, agent in enumerate(agents, 1):
1860
+ # For agents after the first, offer clone option
1861
+ if i > 1:
1862
+ console.print(f"\n[bold cyan]Agent {i} of {len(agents)}: {agent['id']}[/bold cyan]")
1863
+ clone_choice = questionary.select(
1864
+ "How would you like to configure this agent?",
1865
+ choices=[
1866
+ questionary.Choice(f"📋 Copy agent_{chr(ord('a') + i - 2)}'s configuration", value="clone"),
1867
+ questionary.Choice(f"✏️ Copy agent_{chr(ord('a') + i - 2)} and modify specific settings", value="clone_modify"),
1868
+ questionary.Choice("⚙️ Configure from scratch", value="scratch"),
1869
+ ],
1870
+ style=questionary.Style(
1871
+ [
1872
+ ("selected", "fg:cyan bold"),
1873
+ ("pointer", "fg:cyan bold"),
1874
+ ("highlighted", "fg:cyan"),
1875
+ ],
1876
+ ),
1877
+ use_arrow_keys=True,
1878
+ ).ask()
1879
+
1880
+ if clone_choice == "clone":
1881
+ # Clone the previous agent
1882
+ source_agent = agents[i - 2]
1883
+ agent = self.clone_agent(source_agent, agent["id"])
1884
+ agents[i - 1] = agent
1885
+ console.print(f"✅ Cloned configuration from agent_{chr(ord('a') + i - 2)}")
1886
+ console.print()
1887
+ continue
1888
+ elif clone_choice == "clone_modify":
1889
+ # Clone and selectively modify
1890
+ source_agent = agents[i - 2]
1891
+ agent = self.clone_agent(source_agent, agent["id"])
1892
+ agent = self.modify_cloned_agent(agent, i)
1893
+ agents[i - 1] = agent
1894
+ continue
1895
+
1896
+ # Configure from scratch or first agent
1897
+ agent = self.customize_agent(agent, i, len(agents), use_case=use_case)
1898
+ agents[i - 1] = agent
1899
+ else:
1900
+ # Custom configuration - always customize
1901
+ console.print(" [cyan]Custom configuration - configuring each agent...[/cyan]")
1902
+ console.print()
1903
+ for i, agent in enumerate(agents, 1):
1904
+ # For agents after the first, offer clone option
1905
+ if i > 1:
1906
+ console.print(f"\n[bold cyan]Agent {i} of {len(agents)}: {agent['id']}[/bold cyan]")
1907
+ clone_choice = questionary.select(
1908
+ "How would you like to configure this agent?",
1909
+ choices=[
1910
+ questionary.Choice(f"📋 Copy agent_{chr(ord('a') + i - 2)}'s configuration", value="clone"),
1911
+ questionary.Choice(f"✏️ Copy agent_{chr(ord('a') + i - 2)} and modify specific settings", value="clone_modify"),
1912
+ questionary.Choice("⚙️ Configure from scratch", value="scratch"),
1913
+ ],
1914
+ style=questionary.Style(
1915
+ [
1916
+ ("selected", "fg:cyan bold"),
1917
+ ("pointer", "fg:cyan bold"),
1918
+ ("highlighted", "fg:cyan"),
1919
+ ],
1920
+ ),
1921
+ use_arrow_keys=True,
1922
+ ).ask()
1923
+
1924
+ if clone_choice == "clone":
1925
+ # Clone the previous agent
1926
+ source_agent = agents[i - 2]
1927
+ agent = self.clone_agent(source_agent, agent["id"])
1928
+ agents[i - 1] = agent
1929
+ console.print(f"✅ Cloned configuration from agent_{chr(ord('a') + i - 2)}")
1930
+ console.print()
1931
+ continue
1932
+ elif clone_choice == "clone_modify":
1933
+ # Clone and selectively modify
1934
+ source_agent = agents[i - 2]
1935
+ agent = self.clone_agent(source_agent, agent["id"])
1936
+ agent = self.modify_cloned_agent(agent, i)
1937
+ agents[i - 1] = agent
1938
+ continue
1939
+
1940
+ # Configure from scratch or first agent
1941
+ agent = self.customize_agent(agent, i, len(agents), use_case=use_case)
1942
+ agents[i - 1] = agent
1943
+
1944
+ return agents
1945
+
1946
+ except (KeyboardInterrupt, EOFError):
1947
+ raise
1948
+ except Exception as e:
1949
+ console.print(f"[error]❌ Fatal error in agent configuration: {e}[/error]")
1950
+ raise
1951
+
1952
+ def configure_tools(self, use_case: str, agents: List[Dict]) -> Tuple[List[Dict], Dict]:
1953
+ """Configure orchestrator-level settings (tools are configured per-agent)."""
1954
+ try:
1955
+ # Step header
1956
+ step_panel = Panel(
1957
+ "[bold cyan]Step 4 of 4: Orchestrator Configuration[/bold cyan]\n\n[dim]Note: Tools and capabilities were configured per-agent in the previous step.[/dim]",
1958
+ border_style="cyan",
1959
+ padding=(0, 2),
1960
+ width=80,
1961
+ )
1962
+ console.print(step_panel)
1963
+ console.print()
1964
+
1965
+ orchestrator_config = {}
1966
+
1967
+ # Check if any agents have filesystem enabled (Claude Code with cwd)
1968
+ has_filesystem = any(a.get("backend", {}).get("cwd") or a.get("backend", {}).get("type") == "claude_code" for a in agents)
1969
+
1970
+ if has_filesystem:
1971
+ console.print(" [cyan]Filesystem-enabled agents detected[/cyan]")
1972
+ console.print()
1973
+ orchestrator_config["snapshot_storage"] = "snapshots"
1974
+ orchestrator_config["agent_temporary_workspace"] = "temp_workspaces"
1975
+
1976
+ # Context paths
1977
+ console.print(" [dim]Context paths give agents access to your project files.[/dim]")
1978
+ console.print(" [dim]Paths can be absolute or relative (resolved against current directory).[/dim]")
1979
+ console.print(" [dim]Note: During coordination, all context paths are read-only.[/dim]")
1980
+ console.print(" [dim] Write permission applies only to the final agent.[/dim]")
1981
+ console.print()
1982
+
1983
+ add_paths = Confirm.ask("[prompt]Add context paths?[/prompt]", default=False)
1984
+ if add_paths is None:
1985
+ raise KeyboardInterrupt # User cancelled
1986
+ if add_paths:
1987
+ context_paths = []
1988
+ while True:
1989
+ path = Prompt.ask("[prompt]Enter directory or file path (or press Enter to finish)[/prompt]")
1990
+ if path is None:
1991
+ raise KeyboardInterrupt # User cancelled
1992
+ if not path:
1993
+ break
1994
+
1995
+ permission = Prompt.ask(
1996
+ "[prompt]Permission (write means final agent can modify)[/prompt]",
1997
+ choices=["read", "write"],
1998
+ default="write",
1999
+ )
2000
+ if permission is None:
2001
+ raise KeyboardInterrupt # User cancelled
2002
+
2003
+ context_path_entry = {
2004
+ "path": path,
2005
+ "permission": permission,
2006
+ }
2007
+
2008
+ # If write permission, offer to add protected paths
2009
+ if permission == "write":
2010
+ console.print("[dim]Protected paths are files/directories immune from modification[/dim]")
2011
+ if Confirm.ask("[prompt]Add protected paths (e.g., .env, config.json)?[/prompt]", default=False):
2012
+ protected_paths = []
2013
+ console.print("[dim]Enter paths relative to the context path (or press Enter to finish)[/dim]")
2014
+ while True:
2015
+ protected_path = Prompt.ask("[prompt]Protected path[/prompt]")
2016
+ if not protected_path:
2017
+ break
2018
+ protected_paths.append(protected_path)
2019
+ console.print(f"🔒 Protected: {protected_path}")
2020
+
2021
+ if protected_paths:
2022
+ context_path_entry["protected_paths"] = protected_paths
2023
+
2024
+ context_paths.append(context_path_entry)
2025
+ console.print(f"✅ Added: {path} ({permission})")
2026
+
2027
+ if context_paths:
2028
+ orchestrator_config["context_paths"] = context_paths
2029
+
2030
+ # Multi-turn sessions (always enabled)
2031
+ if not orchestrator_config:
2032
+ orchestrator_config = {}
2033
+ orchestrator_config["session_storage"] = "sessions"
2034
+ console.print()
2035
+ console.print(" ✅ Multi-turn sessions enabled (supports persistent conversations with memory)")
2036
+
2037
+ # Planning Mode (for MCP irreversible actions) - only ask if MCPs are configured
2038
+ has_mcp = any(a.get("backend", {}).get("mcp_servers") for a in agents)
2039
+ if has_mcp:
2040
+ console.print()
2041
+ console.print(" [dim]Planning Mode: Prevents MCP tool execution during coordination[/dim]")
2042
+ console.print(" [dim](for irreversible actions like Discord/Twitter posts)[/dim]")
2043
+ console.print()
2044
+ planning_choice = Confirm.ask(" [prompt]Enable planning mode for MCP tools?[/prompt]", default=False)
2045
+ if planning_choice is None:
2046
+ raise KeyboardInterrupt # User cancelled
2047
+ if planning_choice:
2048
+ orchestrator_config["coordination"] = {
2049
+ "enable_planning_mode": True,
2050
+ }
2051
+ console.print()
2052
+ console.print(" ✅ Planning mode enabled - MCP tools will plan without executing during coordination")
2053
+
2054
+ return agents, orchestrator_config
2055
+
2056
+ except (KeyboardInterrupt, EOFError):
2057
+ raise
2058
+ except Exception as e:
2059
+ console.print(f"[error]❌ Error configuring orchestrator: {e}[/error]")
2060
+ console.print("[info]Returning agents with basic configuration...[/info]")
2061
+ return agents, {}
2062
+
2063
+ def review_and_save(self, agents: List[Dict], orchestrator_config: Dict) -> Optional[str]:
2064
+ """Review configuration and save to file with error handling."""
2065
+ try:
2066
+ # Review header
2067
+ review_panel = Panel(
2068
+ "[bold green]✅ Review & Save Configuration[/bold green]",
2069
+ border_style="green",
2070
+ padding=(0, 2),
2071
+ width=80,
2072
+ )
2073
+ console.print(review_panel)
2074
+ console.print()
2075
+
2076
+ # Build final config
2077
+ self.config["agents"] = agents
2078
+ if orchestrator_config:
2079
+ self.config["orchestrator"] = orchestrator_config
2080
+
2081
+ # Display configuration
2082
+ try:
2083
+ yaml_content = yaml.dump(self.config, default_flow_style=False, sort_keys=False)
2084
+ config_panel = Panel(
2085
+ yaml_content,
2086
+ title="[bold cyan]Generated Configuration[/bold cyan]",
2087
+ border_style="green",
2088
+ padding=(1, 2),
2089
+ width=min(console.width - 4, 100), # Adaptive width, max 100
2090
+ )
2091
+ console.print(config_panel)
2092
+ except Exception as e:
2093
+ console.print(f"[warning]⚠️ Could not preview YAML: {e}[/warning]")
2094
+ console.print("[info]Proceeding with save...[/info]")
2095
+
2096
+ save_choice = Confirm.ask("\n[prompt]Save this configuration?[/prompt]", default=True)
2097
+ if save_choice is None:
2098
+ raise KeyboardInterrupt # User cancelled
2099
+ if not save_choice:
2100
+ console.print("[info]Configuration not saved.[/info]")
2101
+ return None
2102
+
2103
+ # Determine save location
2104
+ if self.default_mode:
2105
+ # First-run mode: save to ~/.config/massgen/config.yaml
2106
+ config_dir = Path.home() / ".config/massgen"
2107
+ config_dir.mkdir(parents=True, exist_ok=True)
2108
+ filepath = config_dir / "config.yaml"
2109
+
2110
+ # If file exists, ask to overwrite
2111
+ if filepath.exists():
2112
+ if not Confirm.ask("\n[yellow]⚠️ Default config already exists. Overwrite?[/yellow]", default=True):
2113
+ console.print("[info]Configuration not saved.[/info]")
2114
+ return None
2115
+
2116
+ # Save the file
2117
+ with open(filepath, "w") as f:
2118
+ yaml.dump(self.config, f, default_flow_style=False, sort_keys=False)
2119
+
2120
+ console.print(f"\n✅ [success]Configuration saved to: {filepath}[/success]")
2121
+ return str(filepath)
2122
+
2123
+ # File saving loop with rename option (standard mode)
2124
+ default_name = "my_massgen_config.yaml"
2125
+ filename = None
2126
+
2127
+ # Ask where to save
2128
+ console.print("\nWhere would you like to save the config?")
2129
+ console.print(" [1] Current directory (default)")
2130
+ console.print(" [2] MassGen config directory (~/.config/massgen/agents/)")
2131
+
2132
+ save_location = Prompt.ask(
2133
+ "[prompt]Choose location[/prompt]",
2134
+ choices=["1", "2"],
2135
+ default="1",
2136
+ )
2137
+
2138
+ if save_location == "2":
2139
+ # Save to ~/.config/massgen/agents/
2140
+ agents_dir = Path.home() / ".config/massgen/agents"
2141
+ agents_dir.mkdir(parents=True, exist_ok=True)
2142
+ default_name = str(agents_dir / "my_massgen_config.yaml")
2143
+
2144
+ while True:
2145
+ try:
2146
+ # Get filename with validation
2147
+ if filename is None:
2148
+ filename = Prompt.ask(
2149
+ "[prompt]Config filename[/prompt]",
2150
+ default=default_name,
2151
+ )
2152
+
2153
+ if not filename:
2154
+ console.print("[warning]⚠️ Empty filename, using default.[/warning]")
2155
+ filename = default_name
2156
+
2157
+ if not filename.endswith(".yaml"):
2158
+ filename += ".yaml"
2159
+
2160
+ filepath = Path(filename)
2161
+
2162
+ # Check if file exists
2163
+ if filepath.exists():
2164
+ console.print(f"\n[yellow]⚠️ File '{filename}' already exists![/yellow]")
2165
+ console.print("\nWhat would you like to do?")
2166
+ console.print(" 1. Rename (enter a new filename)")
2167
+ console.print(" 2. Overwrite (replace existing file)")
2168
+ console.print(" 3. Cancel (don't save)")
2169
+
2170
+ choice = Prompt.ask(
2171
+ "\n[prompt]Choose an option[/prompt]",
2172
+ choices=["1", "2", "3"],
2173
+ default="1",
2174
+ )
2175
+
2176
+ if choice == "1":
2177
+ # Ask for new filename
2178
+ filename = Prompt.ask(
2179
+ "[prompt]Enter new filename[/prompt]",
2180
+ default=f"config_{Path(filename).stem}.yaml",
2181
+ )
2182
+ continue # Loop back to check new filename
2183
+ elif choice == "2":
2184
+ # User chose to overwrite
2185
+ pass # Continue to save
2186
+ else: # choice == "3"
2187
+ console.print("[info]Save cancelled.[/info]")
2188
+ return None
2189
+
2190
+ # Save the file
2191
+ with open(filepath, "w") as f:
2192
+ yaml.dump(self.config, f, default_flow_style=False, sort_keys=False)
2193
+
2194
+ console.print(f"\n✅ [success]Configuration saved to: {filepath.absolute()}[/success]")
2195
+ return str(filepath)
2196
+
2197
+ except PermissionError:
2198
+ console.print(f"[error]❌ Permission denied: Cannot write to {filename}[/error]")
2199
+ console.print("[info]Would you like to try a different filename?[/info]")
2200
+ if Confirm.ask("[prompt]Try again?[/prompt]", default=True):
2201
+ filename = None # Reset to ask again
2202
+ continue
2203
+ else:
2204
+ return None
2205
+ except OSError as e:
2206
+ console.print(f"[error]❌ OS error saving file: {e}[/error]")
2207
+ console.print("[info]Would you like to try a different filename?[/info]")
2208
+ if Confirm.ask("[prompt]Try again?[/prompt]", default=True):
2209
+ filename = None # Reset to ask again
2210
+ continue
2211
+ else:
2212
+ return None
2213
+ except Exception as e:
2214
+ console.print(f"[error]❌ Unexpected error saving file: {e}[/error]")
2215
+ return None
2216
+
2217
+ except (KeyboardInterrupt, EOFError):
2218
+ console.print("\n[info]Save cancelled by user.[/info]")
2219
+ return None
2220
+ except Exception as e:
2221
+ console.print(f"[error]❌ Error in review and save: {e}[/error]")
2222
+ return None
2223
+
2224
+ def run(self) -> Optional[tuple]:
2225
+ """Run the interactive configuration builder with comprehensive error handling."""
2226
+ try:
2227
+ self.show_banner()
2228
+
2229
+ # Detect API keys with error handling
2230
+ try:
2231
+ api_keys = self.detect_api_keys()
2232
+ except Exception as e:
2233
+ console.print(f"[error]❌ Failed to detect API keys: {e}[/error]")
2234
+ api_keys = {}
2235
+
2236
+ # Check if any API keys are available
2237
+ # Note: api_keys includes local models (vLLM, etc.) which are always True
2238
+ if not any(api_keys.values()):
2239
+ # No providers available at all (no API keys, no local models, no Claude Code)
2240
+ console.print("[yellow]⚠️ No API keys or local models detected[/yellow]\n")
2241
+ console.print("[dim]MassGen needs at least one of:[/dim]")
2242
+ console.print("[dim] • API keys for cloud providers (OpenAI, Anthropic, Google, etc.)[/dim]")
2243
+ console.print("[dim] • Local models (vLLM, Ollama, etc.)[/dim]")
2244
+ console.print("[dim] • Claude Code with 'claude login'[/dim]\n")
2245
+
2246
+ setup_choice = Confirm.ask(
2247
+ "[prompt]Would you like to set up API keys now (interactive)?[/prompt]",
2248
+ default=True,
2249
+ )
2250
+
2251
+ if setup_choice is None:
2252
+ raise KeyboardInterrupt
2253
+
2254
+ if setup_choice:
2255
+ # Run interactive setup
2256
+ api_keys = self.interactive_api_key_setup()
2257
+
2258
+ # Check if setup was successful
2259
+ if not any(api_keys.values()):
2260
+ console.print("\n[error]❌ No API keys were configured.[/error]")
2261
+ console.print("\n[dim]Alternatives to API keys:[/dim]")
2262
+ console.print("[dim] • Set up local models (vLLM, Ollama)[/dim]")
2263
+ console.print("[dim] • Use Claude Code with 'claude login'[/dim]")
2264
+ console.print("[dim] • Manually create .env file: ~/.massgen/.env or ./.env[/dim]\n")
2265
+ return None
2266
+ else:
2267
+ # User declined interactive setup
2268
+ console.print("\n[info]To use MassGen, you need at least one provider.[/info]")
2269
+ console.print("\n[cyan]Option 1: API Keys[/cyan]")
2270
+ console.print(" Create .env file with one or more:")
2271
+ for provider_id, provider_info in self.PROVIDERS.items():
2272
+ if provider_info.get("env_var"):
2273
+ console.print(f" • {provider_info['env_var']}")
2274
+ console.print("\n[cyan]Option 2: Local Models[/cyan]")
2275
+ console.print(" • Set up vLLM, Ollama, or other local inference")
2276
+ console.print("\n[cyan]Option 3: Claude Code[/cyan]")
2277
+ console.print(" • Run 'claude login' in your terminal")
2278
+ console.print("\n[dim]Run 'massgen --init' anytime to restart this wizard[/dim]\n")
2279
+ return None
2280
+
2281
+ try:
2282
+ # Step 1: Select use case
2283
+ use_case = self.select_use_case()
2284
+ if not use_case:
2285
+ console.print("[warning]⚠️ No use case selected.[/warning]")
2286
+ return None
2287
+
2288
+ # Step 2: Configure agents
2289
+ agents = self.configure_agents(use_case, api_keys)
2290
+ if not agents:
2291
+ console.print("[error]❌ No agents configured.[/error]")
2292
+ return None
2293
+
2294
+ # Step 3: Configure tools
2295
+ try:
2296
+ agents, orchestrator_config = self.configure_tools(use_case, agents)
2297
+ except Exception as e:
2298
+ console.print(f"[warning]⚠️ Error configuring tools: {e}[/warning]")
2299
+ console.print("[info]Continuing with basic configuration...[/info]")
2300
+ orchestrator_config = {}
2301
+
2302
+ # Step 4: Review and save
2303
+ filepath = self.review_and_save(agents, orchestrator_config)
2304
+
2305
+ if filepath:
2306
+ # Ask if user wants to run now
2307
+ run_choice = Confirm.ask("\n[prompt]Run MassGen with this configuration now?[/prompt]", default=True)
2308
+ if run_choice is None:
2309
+ raise KeyboardInterrupt # User cancelled
2310
+ if run_choice:
2311
+ question = Prompt.ask("\n[prompt]Enter your question[/prompt]")
2312
+ if question is None:
2313
+ raise KeyboardInterrupt # User cancelled
2314
+ if question:
2315
+ console.print(f'\n[info]Running: massgen --config {filepath} "{question}"[/info]\n')
2316
+ return (filepath, question)
2317
+ else:
2318
+ console.print("[warning]⚠️ No question provided.[/warning]")
2319
+ return (filepath, None)
2320
+
2321
+ return (filepath, None) if filepath else None
2322
+
2323
+ except (KeyboardInterrupt, EOFError):
2324
+ console.print("\n\n[bold yellow]Configuration cancelled by user[/bold yellow]")
2325
+ console.print("\n[dim]You can run [bold]massgen --init[/bold] anytime to restart.[/dim]\n")
2326
+ return None
2327
+ except ValueError as e:
2328
+ console.print(f"\n[error]❌ Configuration error: {str(e)}[/error]")
2329
+ console.print("[info]Please check your inputs and try again.[/info]")
2330
+ return None
2331
+ except Exception as e:
2332
+ console.print(f"\n[error]❌ Unexpected error during configuration: {str(e)}[/error]")
2333
+ console.print(f"[info]Error type: {type(e).__name__}[/info]")
2334
+ return None
2335
+
2336
+ except KeyboardInterrupt:
2337
+ console.print("\n\n[bold yellow]Configuration cancelled by user[/bold yellow]")
2338
+ console.print("\n[dim]You can run [bold]massgen --init[/bold] anytime to restart the configuration wizard.[/dim]\n")
2339
+ return None
2340
+ except EOFError:
2341
+ console.print("\n\n[bold yellow]Configuration cancelled[/bold yellow]")
2342
+ console.print("\n[dim]You can run [bold]massgen --init[/bold] anytime to restart the configuration wizard.[/dim]\n")
2343
+ return None
2344
+ except Exception as e:
2345
+ console.print(f"\n[error]❌ Fatal error: {str(e)}[/error]")
2346
+ console.print("[info]Please report this issue if it persists.[/info]")
2347
+ return None
2348
+
2349
+
2350
+ def main() -> None:
2351
+ """Main entry point for the config builder."""
2352
+ try:
2353
+ builder = ConfigBuilder()
2354
+ result = builder.run()
2355
+
2356
+ if result and len(result) == 2:
2357
+ filepath, question = result
2358
+ if question:
2359
+ # Run MassGen with the created config
2360
+ console.print(
2361
+ "\n[bold green]✅ Configuration created successfully![/bold green]",
2362
+ )
2363
+ console.print("\n[bold cyan]Running MassGen...[/bold cyan]\n")
2364
+
2365
+ import asyncio
2366
+ import sys
2367
+
2368
+ # Simulate CLI call with the config
2369
+ original_argv = sys.argv.copy()
2370
+ sys.argv = ["massgen", "--config", filepath, question]
2371
+
2372
+ try:
2373
+ from .cli import main as cli_main
2374
+
2375
+ asyncio.run(cli_main())
2376
+ finally:
2377
+ sys.argv = original_argv
2378
+ else:
2379
+ console.print(
2380
+ "\n[bold green]✅ Configuration saved![/bold green]",
2381
+ )
2382
+ console.print("\n[bold cyan]To use it, run:[/bold cyan]")
2383
+ console.print(
2384
+ f' [yellow]massgen --config {filepath} "Your question"[/yellow]\n',
2385
+ )
2386
+ else:
2387
+ console.print("[yellow]Configuration builder exited.[/yellow]")
2388
+ except KeyboardInterrupt:
2389
+ console.print("\n\n[bold yellow]Configuration cancelled by user[/bold yellow]\n")
2390
+ except Exception as e:
2391
+ console.print(f"\n[error]❌ Unexpected error in main: {str(e)}[/error]")
2392
+ console.print("[info]Please report this issue if it persists.[/info]\n")
2393
+
2394
+
2395
+ if __name__ == "__main__":
2396
+ main()