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