massgen 0.0.3__py3-none-any.whl → 0.1.0a1__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 +1504 -287
  46. massgen/config_builder.py +2165 -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.0a1.dist-info/METADATA +1287 -0
  258. massgen-0.1.0a1.dist-info/RECORD +273 -0
  259. massgen-0.1.0a1.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.0a1.dist-info}/WHEEL +0 -0
  267. {massgen-0.0.3.dist-info → massgen-0.1.0a1.dist-info}/licenses/LICENSE +0 -0
  268. {massgen-0.0.3.dist-info → massgen-0.1.0a1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,2165 @@
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": "bright_blue",
37
+ "warning": "bright_yellow",
38
+ "error": "bright_red bold",
39
+ "success": "bright_green bold",
40
+ "prompt": "bright_magenta bold",
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 show_available_providers(
324
+ self,
325
+ api_keys: Dict[str, bool],
326
+ ) -> None:
327
+ """Display providers in a clean Rich table."""
328
+ try:
329
+ # Create Rich table
330
+ table = Table(
331
+ title="[bold cyan]Available Providers[/bold cyan]",
332
+ show_header=True,
333
+ header_style="bold cyan",
334
+ border_style="cyan",
335
+ title_style="bold cyan",
336
+ expand=False, # Don't expand to full width
337
+ padding=(0, 1), # Padding around cells
338
+ )
339
+
340
+ # Add columns
341
+ table.add_column("", justify="center", width=3, no_wrap=True) # Status icon
342
+ table.add_column("Provider", style="bold", min_width=20)
343
+ table.add_column("Models", style="dim", min_width=25)
344
+ table.add_column("Capabilities", style="dim cyan", min_width=20)
345
+
346
+ # Add rows for each provider
347
+ for provider_id, provider_info in self.PROVIDERS.items():
348
+ try:
349
+ has_key = api_keys.get(provider_id, False)
350
+ status = "✅" if has_key else "❌"
351
+ name = provider_info.get("name", "Unknown")
352
+
353
+ # Models (first 2)
354
+ models = provider_info.get("models", [])
355
+ models_display = ", ".join(models[:2])
356
+ if len(models) > 2:
357
+ models_display += f" +{len(models)-2}"
358
+
359
+ # Capabilities (abbreviated, first 3)
360
+ caps = provider_info.get("supports", [])
361
+ cap_abbrev = {
362
+ "web_search": "web",
363
+ "code_execution": "code",
364
+ "filesystem": "files",
365
+ "image_understanding": "img",
366
+ "reasoning": "reason",
367
+ "mcp": "mcp",
368
+ "audio_understanding": "audio",
369
+ "video_understanding": "video",
370
+ }
371
+ caps_display = ", ".join([cap_abbrev.get(c, c[:4]) for c in caps[:3]])
372
+ if len(caps) > 3:
373
+ caps_display += f" +{len(caps)-3}"
374
+
375
+ # Add row
376
+ # Special handling for Claude Code - always available but show hint if no API key
377
+ if provider_id == "claude_code":
378
+ env_var = provider_info.get("env_var", "")
379
+ api_key_set = bool(os.getenv(env_var)) if env_var else False
380
+ if api_key_set:
381
+ table.add_row("✅", name, models_display, caps_display or "basic")
382
+ else:
383
+ name_with_hint = f"{name}\n[dim cyan]⚠️ Requires `claude login` (no API key found)[/dim cyan]"
384
+ table.add_row("✅", name_with_hint, models_display, caps_display or "basic")
385
+ elif has_key:
386
+ table.add_row(status, name, models_display, caps_display or "basic")
387
+ else:
388
+ # For missing keys, add env var hint
389
+ env_var = provider_info.get("env_var", "")
390
+ name_with_hint = f"{name}\n[yellow]Need: {env_var}[/yellow]"
391
+ table.add_row(status, name_with_hint, models_display, caps_display or "basic")
392
+
393
+ except Exception as e:
394
+ console.print(f"[warning]⚠️ Could not display {provider_id}: {e}[/warning]")
395
+
396
+ # Display the table
397
+ console.print(table)
398
+ console.print("\n💡 [dim]Tip: Set API keys in ~/.config/massgen/.env or ~/.massgen/.env[/dim]\n")
399
+
400
+ except Exception as e:
401
+ console.print(f"[error]❌ Error displaying providers: {e}[/error]")
402
+ console.print("[info]Continuing with setup...[/info]\n")
403
+
404
+ def select_use_case(self) -> str:
405
+ """Let user select a use case template with error handling."""
406
+ try:
407
+ # Step header
408
+ step_panel = Panel(
409
+ "[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]",
410
+ border_style="cyan",
411
+ padding=(0, 2),
412
+ width=80,
413
+ )
414
+ console.print(step_panel)
415
+ console.print()
416
+
417
+ # Build choices for questionary - organized with tool hints
418
+ choices = []
419
+
420
+ # Define display with brief tool descriptions
421
+ display_info = [
422
+ ("custom", "⚙️", "Custom Configuration", "Choose your own tools"),
423
+ ("qa", "💬", "Simple Q&A", "Basic chat (no special tools)"),
424
+ ("research", "🔍", "Research & Analysis", "Web search enabled"),
425
+ ("coding", "💻", "Code & Files", "File ops + code execution"),
426
+ ("coding_docker", "🐳", "Code & Files (Docker)", "File ops + isolated Docker execution"),
427
+ ("data_analysis", "📊", "Data Analysis", "Files + code + image analysis"),
428
+ ("multimodal", "🎨", "Multimodal Analysis", "Images, audio, video understanding"),
429
+ ]
430
+
431
+ for use_case_id, emoji, name, tools_hint in display_info:
432
+ try:
433
+ use_case_info = self.USE_CASES.get(use_case_id)
434
+ if not use_case_info:
435
+ continue
436
+
437
+ # Show name with tools hint
438
+ display = f"{emoji} {name:<30} [{tools_hint}]"
439
+
440
+ choices.append(
441
+ questionary.Choice(
442
+ title=display,
443
+ value=use_case_id,
444
+ ),
445
+ )
446
+ except Exception as e:
447
+ console.print(f"[warning]⚠️ Could not display use case: {e}[/warning]")
448
+
449
+ # Add helpful context before the prompt
450
+ console.print("[dim]Choose a preset that matches your task. Each preset auto-configures tools and capabilities.[/dim]")
451
+ console.print("[dim]You can customize everything in later steps.[/dim]\n")
452
+
453
+ use_case_id = questionary.select(
454
+ "Select your use case:",
455
+ choices=choices,
456
+ style=questionary.Style(
457
+ [
458
+ ("selected", "fg:cyan bold"),
459
+ ("pointer", "fg:cyan bold"),
460
+ ("highlighted", "fg:cyan"),
461
+ ],
462
+ ),
463
+ use_arrow_keys=True,
464
+ ).ask()
465
+
466
+ if use_case_id is None:
467
+ raise KeyboardInterrupt # User cancelled, exit immediately
468
+
469
+ # Show selection with description
470
+ selected_info = self.USE_CASES[use_case_id]
471
+ console.print(f"\n✅ Selected: [green]{selected_info.get('name', use_case_id)}[/green]")
472
+ console.print(f" [dim]{selected_info.get('description', '')}[/dim]")
473
+ console.print(f" [dim cyan]→ Recommended: {selected_info.get('recommended_agents', 1)} agent(s)[/dim cyan]\n")
474
+
475
+ # Show preset information (only if there are special features)
476
+ use_case_details = self.USE_CASES[use_case_id]
477
+ if use_case_details.get("info"):
478
+ preset_panel = Panel(
479
+ use_case_details["info"],
480
+ border_style="cyan",
481
+ title="[bold]Preset Configuration[/bold]",
482
+ width=80,
483
+ padding=(1, 2),
484
+ )
485
+ console.print(preset_panel)
486
+ console.print()
487
+
488
+ return use_case_id
489
+ except (KeyboardInterrupt, EOFError):
490
+ raise # Re-raise to be handled by run()
491
+ except Exception as e:
492
+ console.print(f"[error]❌ Error selecting use case: {e}[/error]")
493
+ console.print("[info]Defaulting to 'qa' use case[/info]\n")
494
+ return "qa" # Safe default
495
+
496
+ def add_custom_mcp_server(self) -> Optional[Dict]:
497
+ """Interactive flow to configure a custom MCP server.
498
+
499
+ Returns:
500
+ MCP server configuration dict, or None if cancelled
501
+ """
502
+ try:
503
+ console.print("\n[bold cyan]Configure Custom MCP Server[/bold cyan]\n")
504
+
505
+ # Name
506
+ name = questionary.text(
507
+ "Server name (identifier):",
508
+ validate=lambda x: len(x) > 0,
509
+ ).ask()
510
+
511
+ if not name:
512
+ return None
513
+
514
+ # Type
515
+ server_type = questionary.select(
516
+ "Server type:",
517
+ choices=[
518
+ questionary.Choice("stdio (standard input/output)", value="stdio"),
519
+ questionary.Choice("sse (server-sent events)", value="sse"),
520
+ questionary.Choice("Custom type", value="custom"),
521
+ ],
522
+ default="stdio",
523
+ style=questionary.Style(
524
+ [
525
+ ("selected", "fg:cyan bold"),
526
+ ("pointer", "fg:cyan bold"),
527
+ ("highlighted", "fg:cyan"),
528
+ ],
529
+ ),
530
+ use_arrow_keys=True,
531
+ ).ask()
532
+
533
+ if server_type == "custom":
534
+ server_type = questionary.text("Enter custom type:").ask()
535
+
536
+ if not server_type:
537
+ server_type = "stdio"
538
+
539
+ # Command
540
+ command = questionary.text(
541
+ "Command:",
542
+ default="npx",
543
+ ).ask()
544
+
545
+ if not command:
546
+ command = "npx"
547
+
548
+ # Args
549
+ args_str = questionary.text(
550
+ "Arguments (space-separated, or empty for none):",
551
+ default="",
552
+ ).ask()
553
+
554
+ args = args_str.split() if args_str else []
555
+
556
+ # Environment variables
557
+ env_vars = {}
558
+ if questionary.confirm("Add environment variables?", default=False).ask():
559
+ console.print("\n[dim]Tip: Use ${VAR_NAME} to reference from .env file[/dim]\n")
560
+ while True:
561
+ var_name = questionary.text(
562
+ "Environment variable name (or press Enter to finish):",
563
+ ).ask()
564
+
565
+ if not var_name:
566
+ break
567
+
568
+ var_value = questionary.text(
569
+ f"Value for {var_name}:",
570
+ default=f"${{{var_name}}}",
571
+ ).ask()
572
+
573
+ if var_value:
574
+ env_vars[var_name] = var_value
575
+
576
+ # Build server config
577
+ mcp_server = {
578
+ "name": name,
579
+ "type": server_type,
580
+ "command": command,
581
+ "args": args,
582
+ }
583
+
584
+ if env_vars:
585
+ mcp_server["env"] = env_vars
586
+
587
+ console.print(f"\n✅ Custom MCP server configured: {name}\n")
588
+ return mcp_server
589
+
590
+ except (KeyboardInterrupt, EOFError):
591
+ console.print("\n[info]Cancelled custom MCP configuration[/info]")
592
+ return None
593
+ except Exception as e:
594
+ console.print(f"[error]❌ Error configuring custom MCP: {e}[/error]")
595
+ return None
596
+
597
+ def batch_create_agents(self, count: int, provider_id: str) -> List[Dict]:
598
+ """Create multiple agents with the same provider.
599
+
600
+ Args:
601
+ count: Number of agents to create
602
+ provider_id: Provider ID (e.g., 'openai', 'claude')
603
+
604
+ Returns:
605
+ List of agent configurations with default models
606
+ """
607
+ agents = []
608
+ provider_info = self.PROVIDERS.get(provider_id, {})
609
+
610
+ # Generate agent IDs like agent_a, agent_b, agent_c...
611
+ for i in range(count):
612
+ # Convert index to letter (0->a, 1->b, 2->c, etc.)
613
+ agent_letter = chr(ord("a") + i)
614
+
615
+ agent = {
616
+ "id": f"agent_{agent_letter}",
617
+ "backend": {
618
+ "type": provider_info.get("type", provider_id),
619
+ "model": provider_info.get("models", ["default"])[0], # Default to first model
620
+ },
621
+ }
622
+
623
+ # Add workspace for Claude Code (use numbers, not letters)
624
+ if provider_info.get("type") == "claude_code":
625
+ agent["backend"]["cwd"] = f"workspace{i + 1}"
626
+
627
+ agents.append(agent)
628
+
629
+ return agents
630
+
631
+ def clone_agent(self, source_agent: Dict, new_id: str) -> Dict:
632
+ """Clone an agent's configuration with a new ID.
633
+
634
+ Args:
635
+ source_agent: Agent to clone
636
+ new_id: New agent ID
637
+
638
+ Returns:
639
+ Cloned agent with updated ID and workspace (if applicable)
640
+ """
641
+ import copy
642
+
643
+ cloned = copy.deepcopy(source_agent)
644
+ cloned["id"] = new_id
645
+
646
+ # Update workspace for Claude Code agents to avoid conflicts
647
+ backend_type = cloned.get("backend", {}).get("type")
648
+ if backend_type == "claude_code" and "cwd" in cloned.get("backend", {}):
649
+ # Extract number from new_id (e.g., "agent_b" -> 2)
650
+ if "_" in new_id and len(new_id) > 0:
651
+ agent_letter = new_id.split("_")[-1]
652
+ if len(agent_letter) == 1 and agent_letter.isalpha():
653
+ agent_num = ord(agent_letter.lower()) - ord("a") + 1
654
+ cloned["backend"]["cwd"] = f"workspace{agent_num}"
655
+
656
+ return cloned
657
+
658
+ def modify_cloned_agent(self, agent: Dict, agent_num: int) -> Dict:
659
+ """Allow selective modification of a cloned agent.
660
+
661
+ Args:
662
+ agent: Cloned agent to modify
663
+ agent_num: Agent number (1-indexed)
664
+
665
+ Returns:
666
+ Modified agent configuration
667
+ """
668
+ try:
669
+ console.print(f"\n[bold cyan]Selective Modification: {agent['id']}[/bold cyan]")
670
+ console.print("[dim]Choose which settings to modify (or press Enter to keep all)[/dim]\n")
671
+
672
+ backend_type = agent.get("backend", {}).get("type")
673
+
674
+ # Find provider info
675
+ provider_info = None
676
+ for pid, pinfo in self.PROVIDERS.items():
677
+ if pinfo.get("type") == backend_type:
678
+ provider_info = pinfo
679
+ break
680
+
681
+ if not provider_info:
682
+ console.print("[warning]⚠️ Could not find provider info[/warning]")
683
+ return agent
684
+
685
+ # Ask what to modify
686
+ modify_choices = questionary.checkbox(
687
+ "What would you like to modify? (Space to select, Enter to confirm)",
688
+ choices=[
689
+ questionary.Choice("Model", value="model"),
690
+ questionary.Choice("Tools (web search, code execution)", value="tools"),
691
+ questionary.Choice("Filesystem settings", value="filesystem"),
692
+ questionary.Choice("MCP servers", value="mcp"),
693
+ ],
694
+ style=questionary.Style(
695
+ [
696
+ ("selected", "fg:cyan"),
697
+ ("pointer", "fg:cyan bold"),
698
+ ("highlighted", "fg:cyan"),
699
+ ],
700
+ ),
701
+ use_arrow_keys=True,
702
+ ).ask()
703
+
704
+ if not modify_choices:
705
+ console.print("✅ Keeping all cloned settings")
706
+ return agent
707
+
708
+ # Modify selected aspects
709
+ if "model" in modify_choices:
710
+ models = provider_info.get("models", [])
711
+ if models:
712
+ current_model = agent["backend"].get("model")
713
+ model_choices = [
714
+ questionary.Choice(
715
+ f"{model}" + (" (current)" if model == current_model else ""),
716
+ value=model,
717
+ )
718
+ for model in models
719
+ ]
720
+
721
+ selected_model = questionary.select(
722
+ f"Select model for {agent['id']}:",
723
+ choices=model_choices,
724
+ default=current_model,
725
+ style=questionary.Style(
726
+ [
727
+ ("selected", "fg:cyan bold"),
728
+ ("pointer", "fg:cyan bold"),
729
+ ("highlighted", "fg:cyan"),
730
+ ],
731
+ ),
732
+ use_arrow_keys=True,
733
+ ).ask()
734
+
735
+ if selected_model:
736
+ agent["backend"]["model"] = selected_model
737
+ console.print(f"✅ Model changed to: {selected_model}")
738
+
739
+ if "tools" in modify_choices:
740
+ supports = provider_info.get("supports", [])
741
+ builtin_tools = [s for s in supports if s in ["web_search", "code_execution", "bash"]]
742
+
743
+ if builtin_tools:
744
+ # Show current tools
745
+ current_tools = []
746
+ if agent["backend"].get("enable_web_search"):
747
+ current_tools.append("web_search")
748
+ if agent["backend"].get("enable_code_interpreter") or agent["backend"].get("enable_code_execution"):
749
+ current_tools.append("code_execution")
750
+
751
+ tool_choices = []
752
+ if "web_search" in builtin_tools:
753
+ tool_choices.append(
754
+ questionary.Choice("Web Search", value="web_search", checked="web_search" in current_tools),
755
+ )
756
+ if "code_execution" in builtin_tools:
757
+ tool_choices.append(
758
+ questionary.Choice("Code Execution", value="code_execution", checked="code_execution" in current_tools),
759
+ )
760
+ if "bash" in builtin_tools:
761
+ tool_choices.append(
762
+ questionary.Choice("Bash/Shell", value="bash", checked="bash" in current_tools),
763
+ )
764
+
765
+ if tool_choices:
766
+ selected_tools = questionary.checkbox(
767
+ "Enable built-in tools:",
768
+ choices=tool_choices,
769
+ style=questionary.Style(
770
+ [
771
+ ("selected", "fg:cyan"),
772
+ ("pointer", "fg:cyan bold"),
773
+ ("highlighted", "fg:cyan"),
774
+ ],
775
+ ),
776
+ use_arrow_keys=True,
777
+ ).ask()
778
+
779
+ # Clear existing tools
780
+ agent["backend"].pop("enable_web_search", None)
781
+ agent["backend"].pop("enable_code_interpreter", None)
782
+ agent["backend"].pop("enable_code_execution", None)
783
+
784
+ # Apply selected tools
785
+ if selected_tools:
786
+ if "web_search" in selected_tools:
787
+ if backend_type in ["openai", "claude", "gemini", "grok", "azure_openai"]:
788
+ agent["backend"]["enable_web_search"] = True
789
+
790
+ if "code_execution" in selected_tools:
791
+ if backend_type == "openai" or backend_type == "azure_openai":
792
+ agent["backend"]["enable_code_interpreter"] = True
793
+ elif backend_type in ["claude", "gemini"]:
794
+ agent["backend"]["enable_code_execution"] = True
795
+
796
+ console.print("✅ Tools updated")
797
+
798
+ if "filesystem" in modify_choices and "filesystem" in provider_info.get("supports", []):
799
+ enable_fs = questionary.confirm(
800
+ "Enable filesystem access?",
801
+ default=bool(agent["backend"].get("cwd")),
802
+ ).ask()
803
+
804
+ if enable_fs:
805
+ if backend_type == "claude_code":
806
+ current_cwd = agent["backend"].get("cwd", f"workspace{agent_num}")
807
+ custom_cwd = questionary.text(
808
+ "Workspace directory:",
809
+ default=current_cwd,
810
+ ).ask()
811
+ if custom_cwd:
812
+ agent["backend"]["cwd"] = custom_cwd
813
+ else:
814
+ agent["backend"]["cwd"] = f"workspace{agent_num}"
815
+ console.print(f"✅ Filesystem enabled: {agent['backend']['cwd']}")
816
+ else:
817
+ agent["backend"].pop("cwd", None)
818
+ console.print("✅ Filesystem disabled")
819
+
820
+ if "mcp" in modify_choices and "mcp" in provider_info.get("supports", []):
821
+ if questionary.confirm("Modify MCP servers?", default=False).ask():
822
+ # Show current MCP servers
823
+ current_mcps = agent["backend"].get("mcp_servers", [])
824
+ if current_mcps:
825
+ console.print(f"\n[dim]Current MCP servers: {len(current_mcps)}[/dim]")
826
+ for mcp in current_mcps:
827
+ console.print(f" • {mcp.get('name', 'unnamed')}")
828
+
829
+ if questionary.confirm("Replace with new MCP servers?", default=False).ask():
830
+ mcp_servers = []
831
+ while True:
832
+ custom_server = self.add_custom_mcp_server()
833
+ if custom_server:
834
+ mcp_servers.append(custom_server)
835
+ if not questionary.confirm("Add another MCP server?", default=False).ask():
836
+ break
837
+ else:
838
+ break
839
+
840
+ if mcp_servers:
841
+ agent["backend"]["mcp_servers"] = mcp_servers
842
+ console.print(f"✅ MCP servers updated: {len(mcp_servers)} server(s)")
843
+ else:
844
+ agent["backend"].pop("mcp_servers", None)
845
+ console.print("✅ MCP servers removed")
846
+
847
+ console.print(f"\n✅ [green]Agent {agent['id']} modified[/green]\n")
848
+ return agent
849
+
850
+ except (KeyboardInterrupt, EOFError):
851
+ raise
852
+ except Exception as e:
853
+ console.print(f"[error]❌ Error modifying agent: {e}[/error]")
854
+ return agent
855
+
856
+ def apply_preset_to_agent(self, agent: Dict, use_case: str) -> Dict:
857
+ """Auto-apply preset configuration to an agent.
858
+
859
+ Args:
860
+ agent: Agent configuration dict
861
+ use_case: Use case ID for preset configuration
862
+
863
+ Returns:
864
+ Updated agent configuration with preset applied
865
+ """
866
+ if use_case == "custom":
867
+ return agent
868
+
869
+ use_case_info = self.USE_CASES.get(use_case, {})
870
+ recommended_tools = use_case_info.get("recommended_tools", [])
871
+
872
+ backend_type = agent.get("backend", {}).get("type")
873
+ provider_info = None
874
+
875
+ # Find provider info
876
+ for pid, pinfo in self.PROVIDERS.items():
877
+ if pinfo.get("type") == backend_type:
878
+ provider_info = pinfo
879
+ break
880
+
881
+ if not provider_info:
882
+ return agent
883
+
884
+ # Auto-enable filesystem if recommended
885
+ if "filesystem" in recommended_tools and "filesystem" in provider_info.get("supports", []):
886
+ if not agent["backend"].get("cwd"):
887
+ agent["backend"]["cwd"] = "workspace"
888
+
889
+ # Auto-enable web search if recommended
890
+ if "web_search" in recommended_tools:
891
+ if backend_type in ["openai", "claude", "gemini", "grok", "azure_openai"]:
892
+ agent["backend"]["enable_web_search"] = True
893
+
894
+ # Auto-enable code execution if recommended
895
+ if "code_execution" in recommended_tools:
896
+ if backend_type == "openai" or backend_type == "azure_openai":
897
+ agent["backend"]["enable_code_interpreter"] = True
898
+ elif backend_type in ["claude", "gemini"]:
899
+ agent["backend"]["enable_code_execution"] = True
900
+
901
+ # Auto-enable Docker for Docker preset
902
+ if use_case == "coding_docker" and agent["backend"].get("cwd"):
903
+ agent["backend"]["enable_mcp_command_line"] = True
904
+ agent["backend"]["command_line_execution_mode"] = "docker"
905
+
906
+ # Note: image_understanding, audio_understanding, video_understanding, and reasoning
907
+ # are passive capabilities - they work automatically when the backend supports them
908
+ # and when appropriate content (images/audio/video) is provided in messages.
909
+ # No explicit backend configuration flags needed.
910
+
911
+ return agent
912
+
913
+ def customize_agent(self, agent: Dict, agent_num: int, total_agents: int, use_case: Optional[str] = None) -> Dict:
914
+ """Customize a single agent with Panel UI.
915
+
916
+ Args:
917
+ agent: Agent configuration dict
918
+ agent_num: Agent number (1-indexed)
919
+ total_agents: Total number of agents
920
+ use_case: Use case ID for preset recommendations
921
+
922
+ Returns:
923
+ Updated agent configuration
924
+ """
925
+ try:
926
+ backend_type = agent.get("backend", {}).get("type")
927
+ provider_info = None
928
+
929
+ # Find provider info
930
+ for pid, pinfo in self.PROVIDERS.items():
931
+ if pinfo.get("type") == backend_type:
932
+ provider_info = pinfo
933
+ break
934
+
935
+ if not provider_info:
936
+ console.print(f"[warning]⚠️ Could not find provider for {backend_type}[/warning]")
937
+ return agent
938
+
939
+ # Create Panel for this agent
940
+ panel_content = []
941
+ panel_content.append(f"[bold]Agent {agent_num} of {total_agents}: {agent['id']}[/bold]\n")
942
+
943
+ # Model selection
944
+ models = provider_info.get("models", [])
945
+ if models:
946
+ current_model = agent["backend"].get("model")
947
+ panel_content.append(f"[cyan]Current model:[/cyan] {current_model}")
948
+
949
+ console.print(Panel("\n".join(panel_content), border_style="cyan", width=80))
950
+ console.print()
951
+
952
+ model_choices = [
953
+ questionary.Choice(
954
+ f"{model}" + (" (current)" if model == current_model else ""),
955
+ value=model,
956
+ )
957
+ for model in models
958
+ ]
959
+
960
+ selected_model = questionary.select(
961
+ f"Select model for {agent['id']}:",
962
+ choices=model_choices,
963
+ default=current_model,
964
+ style=questionary.Style(
965
+ [
966
+ ("selected", "fg:cyan bold"),
967
+ ("pointer", "fg:cyan bold"),
968
+ ("highlighted", "fg:cyan"),
969
+ ],
970
+ ),
971
+ use_arrow_keys=True,
972
+ ).ask()
973
+
974
+ if selected_model:
975
+ agent["backend"]["model"] = selected_model
976
+ console.print(f"\n✓ Model set to {selected_model}")
977
+
978
+ # Configure text verbosity for OpenAI models only
979
+ if backend_type in ["openai", "azure_openai"]:
980
+ console.print("\n[dim]Configure text verbosity:[/dim]")
981
+ console.print("[dim] • low: Concise responses[/dim]")
982
+ console.print("[dim] • medium: Balanced detail (recommended)[/dim]")
983
+ console.print("[dim] • high: Detailed, verbose responses[/dim]\n")
984
+
985
+ verbosity_choice = questionary.select(
986
+ "Text verbosity level:",
987
+ choices=[
988
+ questionary.Choice("Low (concise)", value="low"),
989
+ questionary.Choice("Medium (recommended)", value="medium"),
990
+ questionary.Choice("High (detailed)", value="high"),
991
+ ],
992
+ default="medium",
993
+ style=questionary.Style(
994
+ [
995
+ ("selected", "fg:cyan bold"),
996
+ ("pointer", "fg:cyan bold"),
997
+ ("highlighted", "fg:cyan"),
998
+ ],
999
+ ),
1000
+ use_arrow_keys=True,
1001
+ ).ask()
1002
+
1003
+ agent["backend"]["text"] = {
1004
+ "verbosity": verbosity_choice if verbosity_choice else "medium",
1005
+ }
1006
+ console.print(f"✓ Text verbosity set to: {verbosity_choice if verbosity_choice else 'medium'}\n")
1007
+
1008
+ # Auto-add reasoning params for GPT-5 and o-series models
1009
+ if selected_model in ["gpt-5", "gpt-5-mini", "gpt-5-nano", "o4", "o4-mini"]:
1010
+ console.print("[dim]This model supports extended reasoning. Configure reasoning effort:[/dim]")
1011
+ console.print("[dim] • high: Maximum reasoning depth (slower, more thorough)[/dim]")
1012
+ console.print("[dim] • medium: Balanced reasoning (recommended)[/dim]")
1013
+ console.print("[dim] • low: Faster responses with basic reasoning[/dim]\n")
1014
+
1015
+ # Determine default based on model
1016
+ if selected_model in ["gpt-5", "o4"]:
1017
+ default_effort = "medium" # Changed from high to medium
1018
+ elif selected_model in ["gpt-5-mini", "o4-mini"]:
1019
+ default_effort = "medium"
1020
+ else: # gpt-5-nano
1021
+ default_effort = "low"
1022
+
1023
+ effort_choice = questionary.select(
1024
+ "Reasoning effort level:",
1025
+ choices=[
1026
+ questionary.Choice("High (maximum depth)", value="high"),
1027
+ questionary.Choice("Medium (balanced - recommended)", value="medium"),
1028
+ questionary.Choice("Low (faster)", value="low"),
1029
+ ],
1030
+ default=default_effort,
1031
+ style=questionary.Style(
1032
+ [
1033
+ ("selected", "fg:cyan bold"),
1034
+ ("pointer", "fg:cyan bold"),
1035
+ ("highlighted", "fg:cyan"),
1036
+ ],
1037
+ ),
1038
+ use_arrow_keys=True,
1039
+ ).ask()
1040
+
1041
+ agent["backend"]["reasoning"] = {
1042
+ "effort": effort_choice if effort_choice else default_effort,
1043
+ "summary": "auto",
1044
+ }
1045
+ console.print(f"✓ Reasoning effort set to: {effort_choice if effort_choice else default_effort}\n")
1046
+ else:
1047
+ console.print(Panel("\n".join(panel_content), border_style="cyan", width=80))
1048
+
1049
+ # Filesystem access (native or via MCP)
1050
+ if "filesystem" in provider_info.get("supports", []):
1051
+ console.print()
1052
+
1053
+ # Get filesystem support type from capabilities
1054
+ caps = get_capabilities(backend_type)
1055
+ fs_type = caps.filesystem_support if caps else "mcp"
1056
+
1057
+ # Claude Code ALWAYS has filesystem access (that's what makes it special!)
1058
+ if backend_type == "claude_code":
1059
+ # Filesystem is always enabled for Claude Code
1060
+ current_cwd = agent["backend"].get("cwd", "workspace")
1061
+ console.print("[dim]Claude Code has native filesystem access (always enabled)[/dim]")
1062
+ console.print(f"[dim]Current workspace: {current_cwd}[/dim]")
1063
+
1064
+ if questionary.confirm("Customize workspace directory?", default=False).ask():
1065
+ custom_cwd = questionary.text(
1066
+ "Enter workspace directory:",
1067
+ default=current_cwd,
1068
+ ).ask()
1069
+ if custom_cwd:
1070
+ agent["backend"]["cwd"] = custom_cwd
1071
+
1072
+ console.print(f"✅ Filesystem access: {agent['backend']['cwd']} (native)")
1073
+
1074
+ # Ask about Docker bash execution for Claude Code
1075
+ console.print()
1076
+ console.print("[dim]Claude Code bash execution mode:[/dim]")
1077
+ console.print("[dim] • local: Run bash commands directly on your machine (default)[/dim]")
1078
+ console.print("[dim] • docker: Run bash in isolated Docker container (requires Docker setup)[/dim]")
1079
+
1080
+ enable_docker = questionary.confirm(
1081
+ "Enable Docker bash execution? (requires Docker setup)",
1082
+ default=(use_case == "coding_docker"),
1083
+ ).ask()
1084
+
1085
+ if enable_docker:
1086
+ agent["backend"]["enable_mcp_command_line"] = True
1087
+ agent["backend"]["command_line_execution_mode"] = "docker"
1088
+ console.print("🐳 Docker bash execution enabled")
1089
+ else:
1090
+ console.print("💻 Local bash execution enabled (default)")
1091
+ else:
1092
+ # For non-Claude Code backends
1093
+ # Check if filesystem is recommended in the preset
1094
+ filesystem_recommended = False
1095
+ if use_case and use_case != "custom":
1096
+ use_case_info = self.USE_CASES.get(use_case, {})
1097
+ filesystem_recommended = "filesystem" in use_case_info.get("recommended_tools", [])
1098
+
1099
+ if fs_type == "native":
1100
+ console.print("[dim]This backend has native filesystem support[/dim]")
1101
+ else:
1102
+ console.print("[dim]This backend supports filesystem operations via MCP[/dim]")
1103
+
1104
+ if filesystem_recommended:
1105
+ console.print("[dim]💡 Filesystem access recommended for this preset[/dim]")
1106
+
1107
+ # Auto-enable for Docker preset
1108
+ enable_filesystem = filesystem_recommended
1109
+ if not filesystem_recommended:
1110
+ enable_filesystem = questionary.confirm("Enable filesystem access for this agent?", default=True).ask()
1111
+
1112
+ if enable_filesystem:
1113
+ # For MCP-based filesystem, set cwd parameter
1114
+ if not agent["backend"].get("cwd"):
1115
+ # Use agent index for workspace naming
1116
+ agent["backend"]["cwd"] = f"workspace{agent_num}"
1117
+
1118
+ console.print(f"✅ Filesystem access enabled (via MCP): {agent['backend']['cwd']}")
1119
+
1120
+ # Enable Docker execution mode for Docker preset
1121
+ if use_case == "coding_docker":
1122
+ agent["backend"]["enable_mcp_command_line"] = True
1123
+ agent["backend"]["command_line_execution_mode"] = "docker"
1124
+ console.print("🐳 Docker execution mode enabled for isolated code execution")
1125
+
1126
+ # Built-in tools (backend-specific capabilities)
1127
+ # Skip for Claude Code - bash is always available, already configured above
1128
+ if backend_type != "claude_code":
1129
+ supports = provider_info.get("supports", [])
1130
+ builtin_tools = [s for s in supports if s in ["web_search", "code_execution", "bash"]]
1131
+
1132
+ # Get recommended tools from use case
1133
+ recommended_tools = []
1134
+ if use_case:
1135
+ use_case_info = self.USE_CASES.get(use_case, {})
1136
+ recommended_tools = use_case_info.get("recommended_tools", [])
1137
+
1138
+ if builtin_tools:
1139
+ console.print()
1140
+
1141
+ # Show preset info if this is a preset use case
1142
+ if recommended_tools and use_case != "custom":
1143
+ console.print(f"[dim]💡 Preset recommendation: {', '.join(recommended_tools)}[/dim]")
1144
+
1145
+ tool_choices = []
1146
+
1147
+ if "web_search" in builtin_tools:
1148
+ tool_choices.append(questionary.Choice("Web Search", value="web_search", checked="web_search" in recommended_tools))
1149
+ if "code_execution" in builtin_tools:
1150
+ tool_choices.append(questionary.Choice("Code Execution", value="code_execution", checked="code_execution" in recommended_tools))
1151
+ if "bash" in builtin_tools:
1152
+ tool_choices.append(questionary.Choice("Bash/Shell", value="bash", checked="bash" in recommended_tools))
1153
+
1154
+ if tool_choices:
1155
+ selected_tools = questionary.checkbox(
1156
+ "Enable built-in tools for this agent (Space to select, Enter to confirm):",
1157
+ choices=tool_choices,
1158
+ style=questionary.Style(
1159
+ [
1160
+ ("selected", "fg:cyan"),
1161
+ ("pointer", "fg:cyan bold"),
1162
+ ("highlighted", "fg:cyan"),
1163
+ ],
1164
+ ),
1165
+ use_arrow_keys=True,
1166
+ ).ask()
1167
+
1168
+ if selected_tools:
1169
+ # Apply backend-specific configuration
1170
+ if "web_search" in selected_tools:
1171
+ if backend_type in ["openai", "claude", "gemini", "grok", "azure_openai"]:
1172
+ agent["backend"]["enable_web_search"] = True
1173
+
1174
+ if "code_execution" in selected_tools:
1175
+ if backend_type == "openai" or backend_type == "azure_openai":
1176
+ agent["backend"]["enable_code_interpreter"] = True
1177
+ elif backend_type in ["claude", "gemini"]:
1178
+ agent["backend"]["enable_code_execution"] = True
1179
+
1180
+ console.print(f"✅ Enabled {len(selected_tools)} built-in tool(s)")
1181
+
1182
+ # Multimodal capabilities (passive - no config needed)
1183
+ supports = provider_info.get("supports", [])
1184
+ multimodal_caps = [s for s in supports if s in ["image_understanding", "audio_understanding", "video_understanding", "reasoning"]]
1185
+
1186
+ # Show multimodal capabilities info (passive - no config needed)
1187
+ if multimodal_caps:
1188
+ console.print()
1189
+ console.print("[dim]📷 This backend also supports (no configuration needed):[/dim]")
1190
+ if "image_understanding" in multimodal_caps:
1191
+ console.print("[dim] • Image understanding (analyze images, charts, screenshots)[/dim]")
1192
+ if "audio_understanding" in multimodal_caps:
1193
+ console.print("[dim] • Audio understanding (transcribe and analyze audio)[/dim]")
1194
+ if "video_understanding" in multimodal_caps:
1195
+ console.print("[dim] • Video understanding (analyze video content)[/dim]")
1196
+ if "reasoning" in multimodal_caps:
1197
+ console.print("[dim] • Extended reasoning (deep thinking for complex problems)[/dim]")
1198
+
1199
+ # Generation capabilities (optional tools that require explicit flags)
1200
+ generation_caps = [s for s in supports if s in ["image_generation", "audio_generation", "video_generation"]]
1201
+
1202
+ if generation_caps:
1203
+ console.print()
1204
+ console.print("[cyan]Optional generation capabilities (requires explicit enablement):[/cyan]")
1205
+
1206
+ gen_choices = []
1207
+ if "image_generation" in generation_caps:
1208
+ gen_choices.append(questionary.Choice("Image Generation (DALL-E, etc.)", value="image_generation", checked=False))
1209
+ if "audio_generation" in generation_caps:
1210
+ gen_choices.append(questionary.Choice("Audio Generation (TTS, music, etc.)", value="audio_generation", checked=False))
1211
+ if "video_generation" in generation_caps:
1212
+ gen_choices.append(questionary.Choice("Video Generation (Sora, etc.)", value="video_generation", checked=False))
1213
+
1214
+ if gen_choices:
1215
+ selected_gen = questionary.checkbox(
1216
+ "Enable generation capabilities (Space to select, Enter to confirm):",
1217
+ choices=gen_choices,
1218
+ style=questionary.Style(
1219
+ [
1220
+ ("selected", "fg:cyan"),
1221
+ ("pointer", "fg:cyan bold"),
1222
+ ("highlighted", "fg:cyan"),
1223
+ ],
1224
+ ),
1225
+ use_arrow_keys=True,
1226
+ ).ask()
1227
+
1228
+ if selected_gen:
1229
+ if "image_generation" in selected_gen:
1230
+ agent["backend"]["enable_image_generation"] = True
1231
+ if "audio_generation" in selected_gen:
1232
+ agent["backend"]["enable_audio_generation"] = True
1233
+ if "video_generation" in selected_gen:
1234
+ agent["backend"]["enable_video_generation"] = True
1235
+
1236
+ console.print(f"✅ Enabled {len(selected_gen)} generation capability(ies)")
1237
+
1238
+ # MCP servers (custom only)
1239
+ # Note: Filesystem is handled internally above, NOT as external MCP
1240
+ if "mcp" in provider_info.get("supports", []):
1241
+ console.print()
1242
+ console.print("[dim]MCP servers are external integrations. Filesystem is handled internally (configured above).[/dim]")
1243
+
1244
+ if questionary.confirm("Add custom MCP servers?", default=False).ask():
1245
+ mcp_servers = []
1246
+ while True:
1247
+ custom_server = self.add_custom_mcp_server()
1248
+ if custom_server:
1249
+ mcp_servers.append(custom_server)
1250
+
1251
+ # Ask if they want to add another
1252
+ if not questionary.confirm("Add another custom MCP server?", default=False).ask():
1253
+ break
1254
+ else:
1255
+ break
1256
+
1257
+ # Add to agent config if any MCPs were configured
1258
+ if mcp_servers:
1259
+ agent["backend"]["mcp_servers"] = mcp_servers
1260
+ console.print(f"\n✅ Total: {len(mcp_servers)} MCP server(s) configured for this agent\n")
1261
+
1262
+ console.print(f"✅ [green]Agent {agent_num} configured[/green]\n")
1263
+ return agent
1264
+
1265
+ except (KeyboardInterrupt, EOFError):
1266
+ raise
1267
+ except Exception as e:
1268
+ console.print(f"[error]❌ Error customizing agent: {e}[/error]")
1269
+ return agent
1270
+
1271
+ def configure_agents(self, use_case: str, api_keys: Dict[str, bool]) -> List[Dict]:
1272
+ """Configure agents with batch creation and individual customization."""
1273
+ try:
1274
+ # Step header
1275
+ step_panel = Panel(
1276
+ "[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]",
1277
+ border_style="cyan",
1278
+ padding=(0, 2),
1279
+ width=80,
1280
+ )
1281
+ console.print(step_panel)
1282
+ console.print()
1283
+
1284
+ # Show available providers now (right when users need to select them)
1285
+ self.show_available_providers(api_keys)
1286
+
1287
+ use_case_info = self.USE_CASES.get(use_case, {})
1288
+ recommended = use_case_info.get("recommended_agents", 1)
1289
+
1290
+ # Step 2a: How many agents?
1291
+ console.print(f" 💡 [dim]Recommended for this use case: {recommended} agent(s)[/dim]")
1292
+ console.print()
1293
+
1294
+ # Build choices with proper default handling
1295
+ num_choices = [
1296
+ questionary.Choice("1 agent", value=1),
1297
+ questionary.Choice("2 agents", value=2),
1298
+ questionary.Choice("3 agents (recommended for diverse perspectives)", value=3),
1299
+ questionary.Choice("4 agents", value=4),
1300
+ questionary.Choice("5 agents", value=5),
1301
+ questionary.Choice("Custom number", value="custom"),
1302
+ ]
1303
+
1304
+ # Find the default choice by value
1305
+ default_choice = None
1306
+ for choice in num_choices:
1307
+ if choice.value == recommended:
1308
+ default_choice = choice.value
1309
+ break
1310
+
1311
+ try:
1312
+ num_agents_choice = questionary.select(
1313
+ "How many agents?",
1314
+ choices=num_choices,
1315
+ default=default_choice,
1316
+ style=questionary.Style(
1317
+ [
1318
+ ("selected", "fg:cyan bold"),
1319
+ ("pointer", "fg:cyan bold"),
1320
+ ("highlighted", "fg:cyan"),
1321
+ ],
1322
+ ),
1323
+ use_arrow_keys=True,
1324
+ ).ask()
1325
+
1326
+ if num_agents_choice is None:
1327
+ raise KeyboardInterrupt # User cancelled
1328
+
1329
+ if num_agents_choice == "custom":
1330
+ num_agents_text = questionary.text(
1331
+ "Enter number of agents:",
1332
+ validate=lambda x: x.isdigit() and int(x) > 0,
1333
+ ).ask()
1334
+ if num_agents_text is None:
1335
+ raise KeyboardInterrupt # User cancelled
1336
+ num_agents = int(num_agents_text) if num_agents_text else recommended
1337
+ else:
1338
+ num_agents = num_agents_choice
1339
+ except Exception as e:
1340
+ console.print(f"[warning]⚠️ Error with selection: {e}[/warning]")
1341
+ console.print(f"[info]Using recommended: {recommended} agents[/info]")
1342
+ num_agents = recommended
1343
+
1344
+ if num_agents < 1:
1345
+ console.print("[warning]⚠️ Number of agents must be at least 1. Setting to 1.[/warning]")
1346
+ num_agents = 1
1347
+
1348
+ available_providers = [p for p, has_key in api_keys.items() if has_key]
1349
+
1350
+ if not available_providers:
1351
+ console.print("[error]❌ No providers with API keys found. Please set at least one API key.[/error]")
1352
+ raise ValueError("No providers available")
1353
+
1354
+ # Step 2b: Same provider or mix?
1355
+ agents = []
1356
+ if num_agents == 1:
1357
+ # Single agent - just pick provider directly
1358
+ console.print()
1359
+
1360
+ provider_choices = [
1361
+ questionary.Choice(
1362
+ self.PROVIDERS.get(pid, {}).get("name", pid),
1363
+ value=pid,
1364
+ )
1365
+ for pid in available_providers
1366
+ ]
1367
+
1368
+ provider_id = questionary.select(
1369
+ "Select provider:",
1370
+ choices=provider_choices,
1371
+ style=questionary.Style(
1372
+ [
1373
+ ("selected", "fg:cyan bold"),
1374
+ ("pointer", "fg:cyan bold"),
1375
+ ("highlighted", "fg:cyan"),
1376
+ ],
1377
+ ),
1378
+ use_arrow_keys=True,
1379
+ ).ask()
1380
+
1381
+ if provider_id is None:
1382
+ raise KeyboardInterrupt # User cancelled
1383
+
1384
+ agents = self.batch_create_agents(1, provider_id)
1385
+ provider_name = self.PROVIDERS.get(provider_id, {}).get("name", provider_id)
1386
+ console.print()
1387
+ console.print(f" ✅ Created 1 {provider_name} agent")
1388
+ console.print()
1389
+
1390
+ else:
1391
+ # Multiple agents - ask if same or different providers
1392
+ console.print()
1393
+
1394
+ setup_mode = questionary.select(
1395
+ "Setup mode:",
1396
+ choices=[
1397
+ questionary.Choice("Same provider for all agents (quick setup)", value="same"),
1398
+ questionary.Choice("Mix different providers (advanced)", value="mix"),
1399
+ ],
1400
+ style=questionary.Style(
1401
+ [
1402
+ ("selected", "fg:cyan bold"),
1403
+ ("pointer", "fg:cyan bold"),
1404
+ ("highlighted", "fg:cyan"),
1405
+ ],
1406
+ ),
1407
+ use_arrow_keys=True,
1408
+ ).ask()
1409
+
1410
+ if setup_mode is None:
1411
+ raise KeyboardInterrupt # User cancelled
1412
+
1413
+ if setup_mode == "same":
1414
+ # Batch creation with same provider
1415
+ console.print()
1416
+
1417
+ provider_choices = [
1418
+ questionary.Choice(
1419
+ self.PROVIDERS.get(pid, {}).get("name", pid),
1420
+ value=pid,
1421
+ )
1422
+ for pid in available_providers
1423
+ ]
1424
+
1425
+ provider_id = questionary.select(
1426
+ "Select provider:",
1427
+ choices=provider_choices,
1428
+ style=questionary.Style(
1429
+ [
1430
+ ("selected", "fg:cyan bold"),
1431
+ ("pointer", "fg:cyan bold"),
1432
+ ("highlighted", "fg:cyan"),
1433
+ ],
1434
+ ),
1435
+ use_arrow_keys=True,
1436
+ ).ask()
1437
+
1438
+ if provider_id is None:
1439
+ raise KeyboardInterrupt # User cancelled
1440
+
1441
+ agents = self.batch_create_agents(num_agents, provider_id)
1442
+ provider_name = self.PROVIDERS.get(provider_id, {}).get("name", provider_id)
1443
+ console.print()
1444
+ console.print(f" ✅ Created {num_agents} {provider_name} agents")
1445
+ console.print()
1446
+
1447
+ else:
1448
+ # Advanced: mix providers
1449
+ console.print()
1450
+ console.print("[yellow] 💡 Advanced mode: Configure each agent individually[/yellow]")
1451
+ console.print()
1452
+ for i in range(num_agents):
1453
+ try:
1454
+ console.print(f"[bold cyan]Agent {i + 1} of {num_agents}:[/bold cyan]")
1455
+
1456
+ provider_choices = [
1457
+ questionary.Choice(
1458
+ self.PROVIDERS.get(pid, {}).get("name", pid),
1459
+ value=pid,
1460
+ )
1461
+ for pid in available_providers
1462
+ ]
1463
+
1464
+ provider_id = questionary.select(
1465
+ f"Select provider for agent {i + 1}:",
1466
+ choices=provider_choices,
1467
+ style=questionary.Style(
1468
+ [
1469
+ ("selected", "fg:cyan bold"),
1470
+ ("pointer", "fg:cyan bold"),
1471
+ ("highlighted", "fg:cyan"),
1472
+ ],
1473
+ ),
1474
+ use_arrow_keys=True,
1475
+ ).ask()
1476
+
1477
+ if not provider_id:
1478
+ provider_id = available_providers[0]
1479
+
1480
+ agent_batch = self.batch_create_agents(1, provider_id)
1481
+ agents.extend(agent_batch)
1482
+
1483
+ provider_name = self.PROVIDERS.get(provider_id, {}).get("name", provider_id)
1484
+ console.print(f"✅ Agent {i + 1} created: {provider_name}\n")
1485
+
1486
+ except (KeyboardInterrupt, EOFError):
1487
+ raise
1488
+ except Exception as e:
1489
+ console.print(f"[error]❌ Error configuring agent {i + 1}: {e}[/error]")
1490
+ console.print("[info]Skipping this agent...[/info]")
1491
+
1492
+ if not agents:
1493
+ console.print("[error]❌ No agents were successfully configured.[/error]")
1494
+ raise ValueError("Failed to configure any agents")
1495
+
1496
+ # Step 2c: Model selection and preset application
1497
+ # Step header
1498
+ step_panel = Panel(
1499
+ "[bold cyan]Step 3 of 4: Agent Configuration[/bold cyan]",
1500
+ border_style="cyan",
1501
+ padding=(0, 2),
1502
+ width=80,
1503
+ )
1504
+ console.print(step_panel)
1505
+ console.print()
1506
+
1507
+ # For non-custom presets, show info and configure models
1508
+ if use_case != "custom":
1509
+ use_case_info = self.USE_CASES.get(use_case, {})
1510
+ recommended_tools = use_case_info.get("recommended_tools", [])
1511
+
1512
+ console.print(f" [bold green]✓ Preset Selected:[/bold green] {use_case_info.get('name', use_case)}")
1513
+ console.print(f" [dim]{use_case_info.get('description', '')}[/dim]")
1514
+ console.print()
1515
+
1516
+ if recommended_tools:
1517
+ console.print(" [cyan]This preset will auto-configure:[/cyan]")
1518
+ for tool in recommended_tools:
1519
+ tool_display = {
1520
+ "filesystem": "📁 Filesystem access",
1521
+ "code_execution": "💻 Code execution",
1522
+ "web_search": "🔍 Web search",
1523
+ "mcp": "🔌 MCP servers",
1524
+ }.get(tool, tool)
1525
+ console.print(f" • {tool_display}")
1526
+
1527
+ if use_case == "coding_docker":
1528
+ console.print(" • 🐳 Docker isolated execution")
1529
+
1530
+ console.print()
1531
+
1532
+ # Let users select models for each agent
1533
+ console.print(" [cyan]Select models for your agents:[/cyan]")
1534
+ console.print()
1535
+ for i, agent in enumerate(agents, 1):
1536
+ backend_type = agent.get("backend", {}).get("type")
1537
+ provider_info = None
1538
+
1539
+ # Find provider info
1540
+ for pid, pinfo in self.PROVIDERS.items():
1541
+ if pinfo.get("type") == backend_type:
1542
+ provider_info = pinfo
1543
+ break
1544
+
1545
+ if provider_info:
1546
+ models = provider_info.get("models", [])
1547
+ if models and len(models) > 1:
1548
+ current_model = agent["backend"].get("model")
1549
+ console.print(f"[bold]Agent {i} ({agent['id']}) - {provider_info.get('name')}:[/bold]")
1550
+
1551
+ model_choices = [
1552
+ questionary.Choice(
1553
+ f"{model}" + (" (default)" if model == current_model else ""),
1554
+ value=model,
1555
+ )
1556
+ for model in models
1557
+ ]
1558
+
1559
+ selected_model = questionary.select(
1560
+ "Select model:",
1561
+ choices=model_choices,
1562
+ default=current_model,
1563
+ style=questionary.Style(
1564
+ [
1565
+ ("selected", "fg:cyan bold"),
1566
+ ("pointer", "fg:cyan bold"),
1567
+ ("highlighted", "fg:cyan"),
1568
+ ],
1569
+ ),
1570
+ use_arrow_keys=True,
1571
+ ).ask()
1572
+
1573
+ if selected_model:
1574
+ agent["backend"]["model"] = selected_model
1575
+ console.print(f" ✓ {selected_model}")
1576
+
1577
+ # Configure text verbosity for OpenAI models only
1578
+ if backend_type in ["openai", "azure_openai"]:
1579
+ console.print("\n [dim]Configure text verbosity:[/dim]")
1580
+ console.print(" [dim]• low: Concise responses[/dim]")
1581
+ console.print(" [dim]• medium: Balanced detail (recommended)[/dim]")
1582
+ console.print(" [dim]• high: Detailed, verbose responses[/dim]\n")
1583
+
1584
+ verbosity_choice = questionary.select(
1585
+ " Text verbosity:",
1586
+ choices=[
1587
+ questionary.Choice("Low (concise)", value="low"),
1588
+ questionary.Choice("Medium (recommended)", value="medium"),
1589
+ questionary.Choice("High (detailed)", value="high"),
1590
+ ],
1591
+ default="medium",
1592
+ style=questionary.Style(
1593
+ [
1594
+ ("selected", "fg:cyan bold"),
1595
+ ("pointer", "fg:cyan bold"),
1596
+ ("highlighted", "fg:cyan"),
1597
+ ],
1598
+ ),
1599
+ use_arrow_keys=True,
1600
+ ).ask()
1601
+
1602
+ agent["backend"]["text"] = {
1603
+ "verbosity": verbosity_choice if verbosity_choice else "medium",
1604
+ }
1605
+ console.print(f" ✓ Text verbosity: {verbosity_choice if verbosity_choice else 'medium'}\n")
1606
+
1607
+ # Auto-add reasoning params for GPT-5 and o-series models
1608
+ if selected_model in ["gpt-5", "gpt-5-mini", "gpt-5-nano", "o4", "o4-mini"]:
1609
+ console.print(" [dim]Configure reasoning effort:[/dim]")
1610
+ console.print(" [dim]• high: Maximum depth (slower)[/dim]")
1611
+ console.print(" [dim]• medium: Balanced (recommended)[/dim]")
1612
+ console.print(" [dim]• low: Faster responses[/dim]\n")
1613
+
1614
+ # Determine default based on model
1615
+ if selected_model in ["gpt-5", "o4"]:
1616
+ default_effort = "medium" # Changed from high to medium
1617
+ elif selected_model in ["gpt-5-mini", "o4-mini"]:
1618
+ default_effort = "medium"
1619
+ else: # gpt-5-nano
1620
+ default_effort = "low"
1621
+
1622
+ effort_choice = questionary.select(
1623
+ " Reasoning effort:",
1624
+ choices=[
1625
+ questionary.Choice("High", value="high"),
1626
+ questionary.Choice("Medium (recommended)", value="medium"),
1627
+ questionary.Choice("Low", value="low"),
1628
+ ],
1629
+ default=default_effort,
1630
+ style=questionary.Style(
1631
+ [
1632
+ ("selected", "fg:cyan bold"),
1633
+ ("pointer", "fg:cyan bold"),
1634
+ ("highlighted", "fg:cyan"),
1635
+ ],
1636
+ ),
1637
+ use_arrow_keys=True,
1638
+ ).ask()
1639
+
1640
+ agent["backend"]["reasoning"] = {
1641
+ "effort": effort_choice if effort_choice else default_effort,
1642
+ "summary": "auto",
1643
+ }
1644
+ console.print(f" ✓ Reasoning effort: {effort_choice if effort_choice else default_effort}\n")
1645
+
1646
+ # Auto-apply preset to all agents
1647
+ console.print()
1648
+ console.print(" [cyan]Applying preset configuration to all agents...[/cyan]")
1649
+ for i, agent in enumerate(agents):
1650
+ agents[i] = self.apply_preset_to_agent(agent, use_case)
1651
+
1652
+ console.print(f" [green]✅ {len(agents)} agent(s) configured with preset[/green]")
1653
+ console.print()
1654
+
1655
+ # Ask if user wants additional customization
1656
+ customize_choice = Confirm.ask("\n [prompt]Further customize agent settings (advanced)?[/prompt]", default=False)
1657
+ if customize_choice is None:
1658
+ raise KeyboardInterrupt # User cancelled
1659
+ if customize_choice:
1660
+ console.print()
1661
+ console.print(" [cyan]Entering advanced customization...[/cyan]")
1662
+ console.print()
1663
+ for i, agent in enumerate(agents, 1):
1664
+ # For agents after the first, offer clone option
1665
+ if i > 1:
1666
+ console.print(f"\n[bold cyan]Agent {i} of {len(agents)}: {agent['id']}[/bold cyan]")
1667
+ clone_choice = questionary.select(
1668
+ "How would you like to configure this agent?",
1669
+ choices=[
1670
+ questionary.Choice(f"📋 Copy agent_{chr(ord('a') + i - 2)}'s configuration", value="clone"),
1671
+ questionary.Choice(f"✏️ Copy agent_{chr(ord('a') + i - 2)} and modify specific settings", value="clone_modify"),
1672
+ questionary.Choice("⚙️ Configure from scratch", value="scratch"),
1673
+ ],
1674
+ style=questionary.Style(
1675
+ [
1676
+ ("selected", "fg:cyan bold"),
1677
+ ("pointer", "fg:cyan bold"),
1678
+ ("highlighted", "fg:cyan"),
1679
+ ],
1680
+ ),
1681
+ use_arrow_keys=True,
1682
+ ).ask()
1683
+
1684
+ if clone_choice == "clone":
1685
+ # Clone the previous agent
1686
+ source_agent = agents[i - 2]
1687
+ agent = self.clone_agent(source_agent, agent["id"])
1688
+ agents[i - 1] = agent
1689
+ console.print(f"✅ Cloned configuration from agent_{chr(ord('a') + i - 2)}")
1690
+ console.print()
1691
+ continue
1692
+ elif clone_choice == "clone_modify":
1693
+ # Clone and selectively modify
1694
+ source_agent = agents[i - 2]
1695
+ agent = self.clone_agent(source_agent, agent["id"])
1696
+ agent = self.modify_cloned_agent(agent, i)
1697
+ agents[i - 1] = agent
1698
+ continue
1699
+
1700
+ # Configure from scratch or first agent
1701
+ agent = self.customize_agent(agent, i, len(agents), use_case=use_case)
1702
+ agents[i - 1] = agent
1703
+ else:
1704
+ # Custom configuration - always customize
1705
+ console.print(" [cyan]Custom configuration - configuring each agent...[/cyan]")
1706
+ console.print()
1707
+ for i, agent in enumerate(agents, 1):
1708
+ # For agents after the first, offer clone option
1709
+ if i > 1:
1710
+ console.print(f"\n[bold cyan]Agent {i} of {len(agents)}: {agent['id']}[/bold cyan]")
1711
+ clone_choice = questionary.select(
1712
+ "How would you like to configure this agent?",
1713
+ choices=[
1714
+ questionary.Choice(f"📋 Copy agent_{chr(ord('a') + i - 2)}'s configuration", value="clone"),
1715
+ questionary.Choice(f"✏️ Copy agent_{chr(ord('a') + i - 2)} and modify specific settings", value="clone_modify"),
1716
+ questionary.Choice("⚙️ Configure from scratch", value="scratch"),
1717
+ ],
1718
+ style=questionary.Style(
1719
+ [
1720
+ ("selected", "fg:cyan bold"),
1721
+ ("pointer", "fg:cyan bold"),
1722
+ ("highlighted", "fg:cyan"),
1723
+ ],
1724
+ ),
1725
+ use_arrow_keys=True,
1726
+ ).ask()
1727
+
1728
+ if clone_choice == "clone":
1729
+ # Clone the previous agent
1730
+ source_agent = agents[i - 2]
1731
+ agent = self.clone_agent(source_agent, agent["id"])
1732
+ agents[i - 1] = agent
1733
+ console.print(f"✅ Cloned configuration from agent_{chr(ord('a') + i - 2)}")
1734
+ console.print()
1735
+ continue
1736
+ elif clone_choice == "clone_modify":
1737
+ # Clone and selectively modify
1738
+ source_agent = agents[i - 2]
1739
+ agent = self.clone_agent(source_agent, agent["id"])
1740
+ agent = self.modify_cloned_agent(agent, i)
1741
+ agents[i - 1] = agent
1742
+ continue
1743
+
1744
+ # Configure from scratch or first agent
1745
+ agent = self.customize_agent(agent, i, len(agents), use_case=use_case)
1746
+ agents[i - 1] = agent
1747
+
1748
+ return agents
1749
+
1750
+ except (KeyboardInterrupt, EOFError):
1751
+ raise
1752
+ except Exception as e:
1753
+ console.print(f"[error]❌ Fatal error in agent configuration: {e}[/error]")
1754
+ raise
1755
+
1756
+ def configure_tools(self, use_case: str, agents: List[Dict]) -> Tuple[List[Dict], Dict]:
1757
+ """Configure orchestrator-level settings (tools are configured per-agent)."""
1758
+ try:
1759
+ # Step header
1760
+ step_panel = Panel(
1761
+ "[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]",
1762
+ border_style="cyan",
1763
+ padding=(0, 2),
1764
+ width=80,
1765
+ )
1766
+ console.print(step_panel)
1767
+ console.print()
1768
+
1769
+ orchestrator_config = {}
1770
+
1771
+ # Check if any agents have filesystem enabled (Claude Code with cwd)
1772
+ has_filesystem = any(a.get("backend", {}).get("cwd") or a.get("backend", {}).get("type") == "claude_code" for a in agents)
1773
+
1774
+ if has_filesystem:
1775
+ console.print(" [cyan]Filesystem-enabled agents detected[/cyan]")
1776
+ console.print()
1777
+ orchestrator_config["snapshot_storage"] = "snapshots"
1778
+ orchestrator_config["agent_temporary_workspace"] = "temp_workspaces"
1779
+
1780
+ # Context paths
1781
+ console.print(" [dim]Context paths give agents access to your project files.[/dim]")
1782
+ console.print(" [dim]Paths can be absolute or relative (resolved against current directory).[/dim]")
1783
+ console.print(" [dim]Note: During coordination, all context paths are read-only.[/dim]")
1784
+ console.print(" [dim] Write permission applies only to the final agent.[/dim]")
1785
+ console.print()
1786
+
1787
+ add_paths = Confirm.ask("[prompt]Add context paths?[/prompt]", default=False)
1788
+ if add_paths is None:
1789
+ raise KeyboardInterrupt # User cancelled
1790
+ if add_paths:
1791
+ context_paths = []
1792
+ while True:
1793
+ path = Prompt.ask("[prompt]Enter directory or file path (or press Enter to finish)[/prompt]")
1794
+ if path is None:
1795
+ raise KeyboardInterrupt # User cancelled
1796
+ if not path:
1797
+ break
1798
+
1799
+ permission = Prompt.ask(
1800
+ "[prompt]Permission (write means final agent can modify)[/prompt]",
1801
+ choices=["read", "write"],
1802
+ default="write",
1803
+ )
1804
+ if permission is None:
1805
+ raise KeyboardInterrupt # User cancelled
1806
+
1807
+ context_path_entry = {
1808
+ "path": path,
1809
+ "permission": permission,
1810
+ }
1811
+
1812
+ # If write permission, offer to add protected paths
1813
+ if permission == "write":
1814
+ console.print("[dim]Protected paths are files/directories immune from modification[/dim]")
1815
+ if Confirm.ask("[prompt]Add protected paths (e.g., .env, config.json)?[/prompt]", default=False):
1816
+ protected_paths = []
1817
+ console.print("[dim]Enter paths relative to the context path (or press Enter to finish)[/dim]")
1818
+ while True:
1819
+ protected_path = Prompt.ask("[prompt]Protected path[/prompt]")
1820
+ if not protected_path:
1821
+ break
1822
+ protected_paths.append(protected_path)
1823
+ console.print(f"🔒 Protected: {protected_path}")
1824
+
1825
+ if protected_paths:
1826
+ context_path_entry["protected_paths"] = protected_paths
1827
+
1828
+ context_paths.append(context_path_entry)
1829
+ console.print(f"✅ Added: {path} ({permission})")
1830
+
1831
+ if context_paths:
1832
+ orchestrator_config["context_paths"] = context_paths
1833
+
1834
+ # Multi-turn sessions (always enabled)
1835
+ if not orchestrator_config:
1836
+ orchestrator_config = {}
1837
+ orchestrator_config["session_storage"] = "sessions"
1838
+ console.print()
1839
+ console.print(" ✅ Multi-turn sessions enabled (supports persistent conversations with memory)")
1840
+
1841
+ # Planning Mode (for MCP irreversible actions) - only ask if MCPs are configured
1842
+ has_mcp = any(a.get("backend", {}).get("mcp_servers") for a in agents)
1843
+ if has_mcp:
1844
+ console.print()
1845
+ console.print(" [dim]Planning Mode: Prevents MCP tool execution during coordination[/dim]")
1846
+ console.print(" [dim](for irreversible actions like Discord/Twitter posts)[/dim]")
1847
+ console.print()
1848
+ planning_choice = Confirm.ask(" [prompt]Enable planning mode for MCP tools?[/prompt]", default=False)
1849
+ if planning_choice is None:
1850
+ raise KeyboardInterrupt # User cancelled
1851
+ if planning_choice:
1852
+ orchestrator_config["coordination"] = {
1853
+ "enable_planning_mode": True,
1854
+ }
1855
+ console.print()
1856
+ console.print(" ✅ Planning mode enabled - MCP tools will plan without executing during coordination")
1857
+
1858
+ return agents, orchestrator_config
1859
+
1860
+ except (KeyboardInterrupt, EOFError):
1861
+ raise
1862
+ except Exception as e:
1863
+ console.print(f"[error]❌ Error configuring orchestrator: {e}[/error]")
1864
+ console.print("[info]Returning agents with basic configuration...[/info]")
1865
+ return agents, {}
1866
+
1867
+ def review_and_save(self, agents: List[Dict], orchestrator_config: Dict) -> Optional[str]:
1868
+ """Review configuration and save to file with error handling."""
1869
+ try:
1870
+ # Review header
1871
+ review_panel = Panel(
1872
+ "[bold green]✅ Review & Save Configuration[/bold green]",
1873
+ border_style="green",
1874
+ padding=(0, 2),
1875
+ width=80,
1876
+ )
1877
+ console.print(review_panel)
1878
+ console.print()
1879
+
1880
+ # Build final config
1881
+ self.config["agents"] = agents
1882
+ if orchestrator_config:
1883
+ self.config["orchestrator"] = orchestrator_config
1884
+
1885
+ # Display configuration
1886
+ try:
1887
+ yaml_content = yaml.dump(self.config, default_flow_style=False, sort_keys=False)
1888
+ config_panel = Panel(
1889
+ yaml_content,
1890
+ title="[bold cyan]Generated Configuration[/bold cyan]",
1891
+ border_style="green",
1892
+ padding=(1, 2),
1893
+ width=min(console.width - 4, 100), # Adaptive width, max 100
1894
+ )
1895
+ console.print(config_panel)
1896
+ except Exception as e:
1897
+ console.print(f"[warning]⚠️ Could not preview YAML: {e}[/warning]")
1898
+ console.print("[info]Proceeding with save...[/info]")
1899
+
1900
+ save_choice = Confirm.ask("\n[prompt]Save this configuration?[/prompt]", default=True)
1901
+ if save_choice is None:
1902
+ raise KeyboardInterrupt # User cancelled
1903
+ if not save_choice:
1904
+ console.print("[info]Configuration not saved.[/info]")
1905
+ return None
1906
+
1907
+ # Determine save location
1908
+ if self.default_mode:
1909
+ # First-run mode: save to ~/.config/massgen/config.yaml
1910
+ config_dir = Path.home() / ".config/massgen"
1911
+ config_dir.mkdir(parents=True, exist_ok=True)
1912
+ filepath = config_dir / "config.yaml"
1913
+
1914
+ # If file exists, ask to overwrite
1915
+ if filepath.exists():
1916
+ if not Confirm.ask("\n[yellow]⚠️ Default config already exists. Overwrite?[/yellow]", default=True):
1917
+ console.print("[info]Configuration not saved.[/info]")
1918
+ return None
1919
+
1920
+ # Save the file
1921
+ with open(filepath, "w") as f:
1922
+ yaml.dump(self.config, f, default_flow_style=False, sort_keys=False)
1923
+
1924
+ console.print(f"\n✅ [success]Configuration saved to: {filepath}[/success]")
1925
+ return str(filepath)
1926
+
1927
+ # File saving loop with rename option (standard mode)
1928
+ default_name = "my_massgen_config.yaml"
1929
+ filename = None
1930
+
1931
+ # Ask where to save
1932
+ console.print("\nWhere would you like to save the config?")
1933
+ console.print(" [1] Current directory (default)")
1934
+ console.print(" [2] MassGen config directory (~/.config/massgen/agents/)")
1935
+
1936
+ save_location = Prompt.ask(
1937
+ "[prompt]Choose location[/prompt]",
1938
+ choices=["1", "2"],
1939
+ default="1",
1940
+ )
1941
+
1942
+ if save_location == "2":
1943
+ # Save to ~/.config/massgen/agents/
1944
+ agents_dir = Path.home() / ".config/massgen/agents"
1945
+ agents_dir.mkdir(parents=True, exist_ok=True)
1946
+ default_name = str(agents_dir / "my_massgen_config.yaml")
1947
+
1948
+ while True:
1949
+ try:
1950
+ # Get filename with validation
1951
+ if filename is None:
1952
+ filename = Prompt.ask(
1953
+ "[prompt]Config filename[/prompt]",
1954
+ default=default_name,
1955
+ )
1956
+
1957
+ if not filename:
1958
+ console.print("[warning]⚠️ Empty filename, using default.[/warning]")
1959
+ filename = default_name
1960
+
1961
+ if not filename.endswith(".yaml"):
1962
+ filename += ".yaml"
1963
+
1964
+ filepath = Path(filename)
1965
+
1966
+ # Check if file exists
1967
+ if filepath.exists():
1968
+ console.print(f"\n[yellow]⚠️ File '{filename}' already exists![/yellow]")
1969
+ console.print("\nWhat would you like to do?")
1970
+ console.print(" 1. Rename (enter a new filename)")
1971
+ console.print(" 2. Overwrite (replace existing file)")
1972
+ console.print(" 3. Cancel (don't save)")
1973
+
1974
+ choice = Prompt.ask(
1975
+ "\n[prompt]Choose an option[/prompt]",
1976
+ choices=["1", "2", "3"],
1977
+ default="1",
1978
+ )
1979
+
1980
+ if choice == "1":
1981
+ # Ask for new filename
1982
+ filename = Prompt.ask(
1983
+ "[prompt]Enter new filename[/prompt]",
1984
+ default=f"config_{Path(filename).stem}.yaml",
1985
+ )
1986
+ continue # Loop back to check new filename
1987
+ elif choice == "2":
1988
+ # User chose to overwrite
1989
+ pass # Continue to save
1990
+ else: # choice == "3"
1991
+ console.print("[info]Save cancelled.[/info]")
1992
+ return None
1993
+
1994
+ # Save the file
1995
+ with open(filepath, "w") as f:
1996
+ yaml.dump(self.config, f, default_flow_style=False, sort_keys=False)
1997
+
1998
+ console.print(f"\n✅ [success]Configuration saved to: {filepath.absolute()}[/success]")
1999
+ return str(filepath)
2000
+
2001
+ except PermissionError:
2002
+ console.print(f"[error]❌ Permission denied: Cannot write to {filename}[/error]")
2003
+ console.print("[info]Would you like to try a different filename?[/info]")
2004
+ if Confirm.ask("[prompt]Try again?[/prompt]", default=True):
2005
+ filename = None # Reset to ask again
2006
+ continue
2007
+ else:
2008
+ return None
2009
+ except OSError as e:
2010
+ console.print(f"[error]❌ OS error saving file: {e}[/error]")
2011
+ console.print("[info]Would you like to try a different filename?[/info]")
2012
+ if Confirm.ask("[prompt]Try again?[/prompt]", default=True):
2013
+ filename = None # Reset to ask again
2014
+ continue
2015
+ else:
2016
+ return None
2017
+ except Exception as e:
2018
+ console.print(f"[error]❌ Unexpected error saving file: {e}[/error]")
2019
+ return None
2020
+
2021
+ except (KeyboardInterrupt, EOFError):
2022
+ console.print("\n[info]Save cancelled by user.[/info]")
2023
+ return None
2024
+ except Exception as e:
2025
+ console.print(f"[error]❌ Error in review and save: {e}[/error]")
2026
+ return None
2027
+
2028
+ def run(self) -> Optional[tuple]:
2029
+ """Run the interactive configuration builder with comprehensive error handling."""
2030
+ try:
2031
+ self.show_banner()
2032
+
2033
+ # Detect API keys with error handling
2034
+ try:
2035
+ api_keys = self.detect_api_keys()
2036
+ except Exception as e:
2037
+ console.print(f"[error]❌ Failed to detect API keys: {e}[/error]")
2038
+ api_keys = {}
2039
+
2040
+ # Check if any API keys are available
2041
+ if not any(api_keys.values()):
2042
+ console.print("[error]❌ No API keys found in environment![/error]")
2043
+ console.print("\n[yellow]Please set at least one API key in your .env file:[/yellow]")
2044
+ for provider_id, provider_info in self.PROVIDERS.items():
2045
+ if provider_info.get("env_var"):
2046
+ console.print(f" - {provider_info['env_var']}")
2047
+ console.print("\n[info]Tip: Copy .env.example to .env and add your keys[/info]")
2048
+ return None
2049
+
2050
+ try:
2051
+ # Step 1: Select use case
2052
+ use_case = self.select_use_case()
2053
+ if not use_case:
2054
+ console.print("[warning]⚠️ No use case selected.[/warning]")
2055
+ return None
2056
+
2057
+ # Step 2: Configure agents
2058
+ agents = self.configure_agents(use_case, api_keys)
2059
+ if not agents:
2060
+ console.print("[error]❌ No agents configured.[/error]")
2061
+ return None
2062
+
2063
+ # Step 3: Configure tools
2064
+ try:
2065
+ agents, orchestrator_config = self.configure_tools(use_case, agents)
2066
+ except Exception as e:
2067
+ console.print(f"[warning]⚠️ Error configuring tools: {e}[/warning]")
2068
+ console.print("[info]Continuing with basic configuration...[/info]")
2069
+ orchestrator_config = {}
2070
+
2071
+ # Step 4: Review and save
2072
+ filepath = self.review_and_save(agents, orchestrator_config)
2073
+
2074
+ if filepath:
2075
+ # Ask if user wants to run now
2076
+ run_choice = Confirm.ask("\n[prompt]Run MassGen with this configuration now?[/prompt]", default=True)
2077
+ if run_choice is None:
2078
+ raise KeyboardInterrupt # User cancelled
2079
+ if run_choice:
2080
+ question = Prompt.ask("\n[prompt]Enter your question[/prompt]")
2081
+ if question is None:
2082
+ raise KeyboardInterrupt # User cancelled
2083
+ if question:
2084
+ console.print(f'\n[info]Running: massgen --config {filepath} "{question}"[/info]\n')
2085
+ return (filepath, question)
2086
+ else:
2087
+ console.print("[warning]⚠️ No question provided.[/warning]")
2088
+ return (filepath, None)
2089
+
2090
+ return (filepath, None) if filepath else None
2091
+
2092
+ except (KeyboardInterrupt, EOFError):
2093
+ console.print("\n\n[bold yellow]Configuration cancelled by user[/bold yellow]")
2094
+ console.print("\n[dim]You can run [bold]massgen --init[/bold] anytime to restart.[/dim]\n")
2095
+ return None
2096
+ except ValueError as e:
2097
+ console.print(f"\n[error]❌ Configuration error: {str(e)}[/error]")
2098
+ console.print("[info]Please check your inputs and try again.[/info]")
2099
+ return None
2100
+ except Exception as e:
2101
+ console.print(f"\n[error]❌ Unexpected error during configuration: {str(e)}[/error]")
2102
+ console.print(f"[info]Error type: {type(e).__name__}[/info]")
2103
+ return None
2104
+
2105
+ except KeyboardInterrupt:
2106
+ console.print("\n\n[bold yellow]Configuration cancelled by user[/bold yellow]")
2107
+ console.print("\n[dim]You can run [bold]massgen --init[/bold] anytime to restart the configuration wizard.[/dim]\n")
2108
+ return None
2109
+ except EOFError:
2110
+ console.print("\n\n[bold yellow]Configuration cancelled[/bold yellow]")
2111
+ console.print("\n[dim]You can run [bold]massgen --init[/bold] anytime to restart the configuration wizard.[/dim]\n")
2112
+ return None
2113
+ except Exception as e:
2114
+ console.print(f"\n[error]❌ Fatal error: {str(e)}[/error]")
2115
+ console.print("[info]Please report this issue if it persists.[/info]")
2116
+ return None
2117
+
2118
+
2119
+ def main() -> None:
2120
+ """Main entry point for the config builder."""
2121
+ try:
2122
+ builder = ConfigBuilder()
2123
+ result = builder.run()
2124
+
2125
+ if result and len(result) == 2:
2126
+ filepath, question = result
2127
+ if question:
2128
+ # Run MassGen with the created config
2129
+ console.print(
2130
+ "\n[bold green]✅ Configuration created successfully![/bold green]",
2131
+ )
2132
+ console.print("\n[bold cyan]Running MassGen...[/bold cyan]\n")
2133
+
2134
+ import asyncio
2135
+ import sys
2136
+
2137
+ # Simulate CLI call with the config
2138
+ original_argv = sys.argv.copy()
2139
+ sys.argv = ["massgen", "--config", filepath, question]
2140
+
2141
+ try:
2142
+ from .cli import main as cli_main
2143
+
2144
+ asyncio.run(cli_main())
2145
+ finally:
2146
+ sys.argv = original_argv
2147
+ else:
2148
+ console.print(
2149
+ "\n[bold green]✅ Configuration saved![/bold green]",
2150
+ )
2151
+ console.print("\n[bold cyan]To use it, run:[/bold cyan]")
2152
+ console.print(
2153
+ f' [yellow]massgen --config {filepath} "Your question"[/yellow]\n',
2154
+ )
2155
+ else:
2156
+ console.print("[yellow]Configuration builder exited.[/yellow]")
2157
+ except KeyboardInterrupt:
2158
+ console.print("\n\n[bold yellow]Configuration cancelled by user[/bold yellow]\n")
2159
+ except Exception as e:
2160
+ console.print(f"\n[error]❌ Unexpected error in main: {str(e)}[/error]")
2161
+ console.print("[info]Please report this issue if it persists.[/info]\n")
2162
+
2163
+
2164
+ if __name__ == "__main__":
2165
+ main()