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
massgen/cli.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
2
3
|
"""
|
|
3
4
|
MassGen Command Line Interface
|
|
4
5
|
|
|
@@ -16,34 +17,55 @@ Usage examples:
|
|
|
16
17
|
python -m massgen.cli --config config.yaml
|
|
17
18
|
|
|
18
19
|
# Multiple agents from config
|
|
19
|
-
python -m massgen.cli --config multi_agent.yaml "Compare different approaches to renewable energy"
|
|
20
|
+
python -m massgen.cli --config multi_agent.yaml "Compare different approaches to renewable energy" # noqa
|
|
20
21
|
"""
|
|
21
22
|
|
|
22
23
|
import argparse
|
|
23
24
|
import asyncio
|
|
24
25
|
import json
|
|
25
26
|
import os
|
|
27
|
+
import shutil
|
|
26
28
|
import sys
|
|
27
|
-
import
|
|
29
|
+
from datetime import datetime
|
|
28
30
|
from pathlib import Path
|
|
29
|
-
from typing import Dict,
|
|
30
|
-
|
|
31
|
-
from .utils import MODEL_MAPPINGS, get_backend_type_from_model
|
|
32
|
-
|
|
31
|
+
from typing import Any, Dict, List, Optional
|
|
33
32
|
|
|
34
|
-
|
|
33
|
+
import yaml
|
|
34
|
+
from dotenv import load_dotenv
|
|
35
|
+
from rich.console import Console
|
|
36
|
+
from rich.panel import Panel
|
|
37
|
+
from rich.table import Table
|
|
38
|
+
|
|
39
|
+
from .agent_config import AgentConfig, TimeoutConfig
|
|
40
|
+
from .backend.azure_openai import AzureOpenAIBackend
|
|
41
|
+
from .backend.chat_completions import ChatCompletionsBackend
|
|
42
|
+
from .backend.claude import ClaudeBackend
|
|
43
|
+
from .backend.claude_code import ClaudeCodeBackend
|
|
44
|
+
from .backend.gemini import GeminiBackend
|
|
45
|
+
from .backend.grok import GrokBackend
|
|
46
|
+
from .backend.inference import InferenceBackend
|
|
47
|
+
from .backend.lmstudio import LMStudioBackend
|
|
48
|
+
from .backend.response import ResponseBackend
|
|
49
|
+
from .chat_agent import ConfigurableAgent, SingleAgent
|
|
50
|
+
from .frontend.coordination_ui import CoordinationUI
|
|
51
|
+
from .logger_config import _DEBUG_MODE, logger, setup_logging
|
|
52
|
+
from .orchestrator import Orchestrator
|
|
53
|
+
from .utils import get_backend_type_from_model
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Load environment variables from .env files
|
|
35
57
|
def load_env_file():
|
|
36
|
-
"""Load environment variables from .env
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
58
|
+
"""Load environment variables from .env files.
|
|
59
|
+
|
|
60
|
+
Search order (later files override earlier ones):
|
|
61
|
+
1. MassGen package .env (development fallback)
|
|
62
|
+
2. User home ~/.massgen/.env (global user config)
|
|
63
|
+
3. Current directory .env (project-specific, highest priority)
|
|
64
|
+
"""
|
|
65
|
+
# Load in priority order (later overrides earlier)
|
|
66
|
+
load_dotenv(Path(__file__).parent / ".env") # Package fallback
|
|
67
|
+
load_dotenv(Path.home() / ".massgen" / ".env") # User global
|
|
68
|
+
load_dotenv() # Current directory (highest priority)
|
|
47
69
|
|
|
48
70
|
|
|
49
71
|
# Load .env file at module import
|
|
@@ -53,15 +75,6 @@ load_env_file()
|
|
|
53
75
|
project_root = Path(__file__).parent.parent.parent.parent
|
|
54
76
|
sys.path.insert(0, str(project_root))
|
|
55
77
|
|
|
56
|
-
from massgen.backend.response import ResponseBackend
|
|
57
|
-
from massgen.backend.grok import GrokBackend
|
|
58
|
-
from massgen.backend.claude import ClaudeBackend
|
|
59
|
-
from massgen.backend.gemini import GeminiBackend
|
|
60
|
-
from massgen.chat_agent import SingleAgent, ConfigurableAgent
|
|
61
|
-
from massgen.agent_config import AgentConfig
|
|
62
|
-
from massgen.orchestrator import Orchestrator
|
|
63
|
-
from massgen.frontend.coordination_ui import CoordinationUI
|
|
64
|
-
|
|
65
78
|
# Color constants for terminal output
|
|
66
79
|
BRIGHT_CYAN = "\033[96m"
|
|
67
80
|
BRIGHT_BLUE = "\033[94m"
|
|
@@ -77,22 +90,153 @@ BOLD = "\033[1m"
|
|
|
77
90
|
class ConfigurationError(Exception):
|
|
78
91
|
"""Configuration error for CLI."""
|
|
79
92
|
|
|
80
|
-
|
|
93
|
+
|
|
94
|
+
def _substitute_variables(obj: Any, variables: Dict[str, str]) -> Any:
|
|
95
|
+
"""Recursively substitute ${var} references in config with actual values.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
obj: Config object (dict, list, str, or other)
|
|
99
|
+
variables: Dict of variable names to values
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Config object with variables substituted
|
|
103
|
+
"""
|
|
104
|
+
if isinstance(obj, dict):
|
|
105
|
+
return {k: _substitute_variables(v, variables) for k, v in obj.items()}
|
|
106
|
+
elif isinstance(obj, list):
|
|
107
|
+
return [_substitute_variables(item, variables) for item in obj]
|
|
108
|
+
elif isinstance(obj, str):
|
|
109
|
+
# Replace ${var} with value
|
|
110
|
+
result = obj
|
|
111
|
+
for var_name, var_value in variables.items():
|
|
112
|
+
result = result.replace(f"${{{var_name}}}", var_value)
|
|
113
|
+
return result
|
|
114
|
+
else:
|
|
115
|
+
return obj
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def resolve_config_path(config_arg: Optional[str]) -> Optional[Path]:
|
|
119
|
+
"""Resolve config file with flexible syntax.
|
|
120
|
+
|
|
121
|
+
Priority order:
|
|
122
|
+
|
|
123
|
+
**If --config flag provided (highest priority):**
|
|
124
|
+
1. @examples/NAME → Package examples (search configs directory)
|
|
125
|
+
2. Absolute/relative paths (exact path as specified)
|
|
126
|
+
3. Named configs in ~/.config/massgen/agents/
|
|
127
|
+
|
|
128
|
+
**If NO --config flag (auto-discovery):**
|
|
129
|
+
1. .massgen/config.yaml (project-level config in current directory)
|
|
130
|
+
2. ~/.config/massgen/config.yaml (global default config)
|
|
131
|
+
3. None → trigger config builder
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
config_arg: Config argument from --config flag (can be @examples/NAME, path, or None)
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Path to config file, or None if config builder should run
|
|
138
|
+
|
|
139
|
+
Raises:
|
|
140
|
+
ConfigurationError: If config file not found
|
|
141
|
+
"""
|
|
142
|
+
# Check for default configs if no config_arg provided
|
|
143
|
+
if not config_arg:
|
|
144
|
+
# Priority 1: Project-level config (.massgen/config.yaml in current directory)
|
|
145
|
+
project_config = Path.cwd() / ".massgen" / "config.yaml"
|
|
146
|
+
if project_config.exists():
|
|
147
|
+
return project_config
|
|
148
|
+
|
|
149
|
+
# Priority 2: Global default config
|
|
150
|
+
global_config = Path.home() / ".config/massgen/config.yaml"
|
|
151
|
+
if global_config.exists():
|
|
152
|
+
return global_config
|
|
153
|
+
|
|
154
|
+
return None # Trigger builder
|
|
155
|
+
|
|
156
|
+
# Handle @examples/ prefix - search in package configs
|
|
157
|
+
if config_arg.startswith("@examples/"):
|
|
158
|
+
name = config_arg[10:] # Remove '@examples/' prefix
|
|
159
|
+
try:
|
|
160
|
+
from importlib.resources import files
|
|
161
|
+
|
|
162
|
+
configs_root = files("massgen") / "configs"
|
|
163
|
+
|
|
164
|
+
# Search recursively for matching name
|
|
165
|
+
# Try to find by filename stem match
|
|
166
|
+
for config_file in configs_root.rglob("*.yaml"):
|
|
167
|
+
# Check if name matches the file stem or is contained in the path
|
|
168
|
+
if name in config_file.name or name in str(config_file):
|
|
169
|
+
return Path(str(config_file))
|
|
170
|
+
|
|
171
|
+
raise ConfigurationError(
|
|
172
|
+
f"Config '{config_arg}' not found in package.\n" f"Use --list-examples to see available configs.",
|
|
173
|
+
)
|
|
174
|
+
except Exception as e:
|
|
175
|
+
if isinstance(e, ConfigurationError):
|
|
176
|
+
raise
|
|
177
|
+
raise ConfigurationError(f"Error loading package config: {e}")
|
|
178
|
+
|
|
179
|
+
# Try as regular path (absolute or relative)
|
|
180
|
+
path = Path(config_arg).expanduser()
|
|
181
|
+
if path.exists():
|
|
182
|
+
return path
|
|
183
|
+
|
|
184
|
+
# Try in user config directory (~/.config/massgen/agents/)
|
|
185
|
+
user_agents_dir = Path.home() / ".config/massgen/agents"
|
|
186
|
+
user_config = user_agents_dir / f"{config_arg}.yaml"
|
|
187
|
+
if user_config.exists():
|
|
188
|
+
return user_config
|
|
189
|
+
|
|
190
|
+
# Also try with .yaml extension if not provided
|
|
191
|
+
if not config_arg.endswith((".yaml", ".yml")):
|
|
192
|
+
user_config_with_ext = user_agents_dir / f"{config_arg}.yaml"
|
|
193
|
+
if user_config_with_ext.exists():
|
|
194
|
+
return user_config_with_ext
|
|
195
|
+
|
|
196
|
+
# Config not found anywhere
|
|
197
|
+
raise ConfigurationError(
|
|
198
|
+
f"Configuration file not found: {config_arg}\n"
|
|
199
|
+
f"Searched in:\n"
|
|
200
|
+
f" - Current directory: {Path.cwd() / config_arg}\n"
|
|
201
|
+
f" - User configs: {user_agents_dir / config_arg}.yaml\n"
|
|
202
|
+
f"Use --list-examples to see available package configs.",
|
|
203
|
+
)
|
|
81
204
|
|
|
82
205
|
|
|
83
206
|
def load_config_file(config_path: str) -> Dict[str, Any]:
|
|
84
|
-
"""Load configuration from YAML or JSON file.
|
|
207
|
+
"""Load configuration from YAML or JSON file.
|
|
208
|
+
|
|
209
|
+
Search order:
|
|
210
|
+
1. Exact path as provided (absolute or relative to CWD)
|
|
211
|
+
2. If just a filename, search in package's configs/ directory
|
|
212
|
+
3. If a relative path, also try within package's configs/ directory
|
|
213
|
+
|
|
214
|
+
Supports variable substitution: ${cwd} in any string will be replaced with the agent's cwd value.
|
|
215
|
+
"""
|
|
85
216
|
path = Path(config_path)
|
|
86
217
|
|
|
87
|
-
#
|
|
88
|
-
if
|
|
89
|
-
#
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
218
|
+
# Try the path as-is first (handles absolute paths and relative to CWD)
|
|
219
|
+
if path.exists():
|
|
220
|
+
pass # Use this path
|
|
221
|
+
elif path.is_absolute():
|
|
222
|
+
# Absolute path that doesn't exist
|
|
223
|
+
raise ConfigurationError(f"Configuration file not found: {config_path}")
|
|
224
|
+
else:
|
|
225
|
+
# Relative path or just filename - search in package configs
|
|
226
|
+
package_configs_dir = Path(__file__).parent / "configs"
|
|
227
|
+
|
|
228
|
+
# Try 1: Just the filename in package configs root
|
|
229
|
+
candidate1 = package_configs_dir / path.name
|
|
230
|
+
# Try 2: The full relative path within package configs
|
|
231
|
+
candidate2 = package_configs_dir / path
|
|
232
|
+
|
|
233
|
+
if candidate1.exists():
|
|
234
|
+
path = candidate1
|
|
235
|
+
elif candidate2.exists():
|
|
236
|
+
path = candidate2
|
|
93
237
|
else:
|
|
94
238
|
raise ConfigurationError(
|
|
95
|
-
f"Configuration file not found: {config_path} (
|
|
239
|
+
f"Configuration file not found: {config_path}\n" f"Searched in:\n" f" - {Path.cwd() / config_path}\n" f" - {candidate1}\n" f" - {candidate2}",
|
|
96
240
|
)
|
|
97
241
|
|
|
98
242
|
try:
|
|
@@ -102,186 +246,564 @@ def load_config_file(config_path: str) -> Dict[str, Any]:
|
|
|
102
246
|
elif path.suffix.lower() == ".json":
|
|
103
247
|
return json.load(f)
|
|
104
248
|
else:
|
|
105
|
-
raise ConfigurationError(
|
|
106
|
-
f"Unsupported config file format: {path.suffix}"
|
|
107
|
-
)
|
|
249
|
+
raise ConfigurationError(f"Unsupported config file format: {path.suffix}")
|
|
108
250
|
except Exception as e:
|
|
109
251
|
raise ConfigurationError(f"Error reading config file: {e}")
|
|
110
252
|
|
|
111
253
|
|
|
112
254
|
def create_backend(backend_type: str, **kwargs) -> Any:
|
|
113
|
-
"""Create backend instance from type and parameters.
|
|
255
|
+
"""Create backend instance from type and parameters.
|
|
256
|
+
|
|
257
|
+
Supported backend types:
|
|
258
|
+
- openai: OpenAI API (requires OPENAI_API_KEY)
|
|
259
|
+
- grok: xAI Grok (requires XAI_API_KEY)
|
|
260
|
+
- sglang: SGLang inference server (local)
|
|
261
|
+
- claude: Anthropic Claude (requires ANTHROPIC_API_KEY)
|
|
262
|
+
- gemini: Google Gemini (requires GOOGLE_API_KEY or GEMINI_API_KEY)
|
|
263
|
+
- chatcompletion: OpenAI-compatible providers (auto-detects API key based on base_url)
|
|
264
|
+
|
|
265
|
+
Supported backend with external dependencies:
|
|
266
|
+
- ag2/autogen: AG2 (AutoGen) framework agents
|
|
267
|
+
|
|
268
|
+
For chatcompletion backend, the following providers are auto-detected:
|
|
269
|
+
- Cerebras AI (cerebras.ai) -> CEREBRAS_API_KEY
|
|
270
|
+
- Together AI (together.ai/together.xyz) -> TOGETHER_API_KEY
|
|
271
|
+
- Fireworks AI (fireworks.ai) -> FIREWORKS_API_KEY
|
|
272
|
+
- Groq (groq.com) -> GROQ_API_KEY
|
|
273
|
+
- Nebius AI Studio (studio.nebius.ai) -> NEBIUS_API_KEY
|
|
274
|
+
- OpenRouter (openrouter.ai) -> OPENROUTER_API_KEY
|
|
275
|
+
- POE (poe.com) -> POE_API_KEY
|
|
276
|
+
- Qwen (dashscope.aliyuncs.com) -> QWEN_API_KEY
|
|
277
|
+
|
|
278
|
+
External agent frameworks are supported via the adapter registry.
|
|
279
|
+
"""
|
|
114
280
|
backend_type = backend_type.lower()
|
|
115
281
|
|
|
282
|
+
# Check if this is a framework/adapter type
|
|
283
|
+
from massgen.adapters import adapter_registry
|
|
284
|
+
|
|
285
|
+
if backend_type in adapter_registry:
|
|
286
|
+
# Use ExternalAgentBackend for all registered adapter types
|
|
287
|
+
from massgen.backend.external import ExternalAgentBackend
|
|
288
|
+
|
|
289
|
+
return ExternalAgentBackend(adapter_type=backend_type, **kwargs)
|
|
290
|
+
|
|
116
291
|
if backend_type == "openai":
|
|
117
292
|
api_key = kwargs.get("api_key") or os.getenv("OPENAI_API_KEY")
|
|
118
293
|
if not api_key:
|
|
119
294
|
raise ConfigurationError(
|
|
120
|
-
"OpenAI API key not found. Set OPENAI_API_KEY
|
|
295
|
+
"OpenAI API key not found. Set OPENAI_API_KEY environment variable.\n" "You can add it to a .env file in:\n" " - Current directory: .env\n" " - Global config: ~/.massgen/.env",
|
|
121
296
|
)
|
|
122
|
-
return ResponseBackend(api_key=api_key)
|
|
297
|
+
return ResponseBackend(api_key=api_key, **kwargs)
|
|
123
298
|
|
|
124
299
|
elif backend_type == "grok":
|
|
125
300
|
api_key = kwargs.get("api_key") or os.getenv("XAI_API_KEY")
|
|
126
301
|
if not api_key:
|
|
127
302
|
raise ConfigurationError(
|
|
128
|
-
"Grok API key not found. Set XAI_API_KEY
|
|
303
|
+
"Grok API key not found. Set XAI_API_KEY environment variable.\n" "You can add it to a .env file in:\n" " - Current directory: .env\n" " - Global config: ~/.massgen/.env",
|
|
129
304
|
)
|
|
130
|
-
return GrokBackend(api_key=api_key)
|
|
305
|
+
return GrokBackend(api_key=api_key, **kwargs)
|
|
131
306
|
|
|
132
307
|
elif backend_type == "claude":
|
|
133
308
|
api_key = kwargs.get("api_key") or os.getenv("ANTHROPIC_API_KEY")
|
|
134
309
|
if not api_key:
|
|
135
310
|
raise ConfigurationError(
|
|
136
|
-
"Claude API key not found. Set ANTHROPIC_API_KEY
|
|
311
|
+
"Claude API key not found. Set ANTHROPIC_API_KEY environment variable.\n" "You can add it to a .env file in:\n" " - Current directory: .env\n" " - Global config: ~/.massgen/.env",
|
|
137
312
|
)
|
|
138
|
-
return ClaudeBackend(api_key=api_key)
|
|
313
|
+
return ClaudeBackend(api_key=api_key, **kwargs)
|
|
139
314
|
|
|
140
315
|
elif backend_type == "gemini":
|
|
141
|
-
api_key = (
|
|
142
|
-
kwargs.get("api_key")
|
|
143
|
-
or os.getenv("GOOGLE_API_KEY")
|
|
144
|
-
or os.getenv("GEMINI_API_KEY")
|
|
145
|
-
)
|
|
316
|
+
api_key = kwargs.get("api_key") or os.getenv("GOOGLE_API_KEY") or os.getenv("GEMINI_API_KEY")
|
|
146
317
|
if not api_key:
|
|
147
318
|
raise ConfigurationError(
|
|
148
|
-
"Gemini API key not found. Set GOOGLE_API_KEY
|
|
319
|
+
"Gemini API key not found. Set GOOGLE_API_KEY environment variable.\n" "You can add it to a .env file in:\n" " - Current directory: .env\n" " - Global config: ~/.massgen/.env",
|
|
149
320
|
)
|
|
150
|
-
return GeminiBackend(api_key=api_key)
|
|
321
|
+
return GeminiBackend(api_key=api_key, **kwargs)
|
|
322
|
+
|
|
323
|
+
elif backend_type == "chatcompletion":
|
|
324
|
+
api_key = kwargs.get("api_key")
|
|
325
|
+
base_url = kwargs.get("base_url")
|
|
326
|
+
|
|
327
|
+
# Determine API key based on base URL if not explicitly provided
|
|
328
|
+
if not api_key:
|
|
329
|
+
if base_url and "cerebras.ai" in base_url:
|
|
330
|
+
api_key = os.getenv("CEREBRAS_API_KEY")
|
|
331
|
+
if not api_key:
|
|
332
|
+
raise ConfigurationError(
|
|
333
|
+
"Cerebras AI API key not found. Set CEREBRAS_API_KEY environment variable.\n"
|
|
334
|
+
"You can add it to a .env file in:\n"
|
|
335
|
+
" - Current directory: .env\n"
|
|
336
|
+
" - Global config: ~/.massgen/.env",
|
|
337
|
+
)
|
|
338
|
+
elif base_url and "together.xyz" in base_url:
|
|
339
|
+
api_key = os.getenv("TOGETHER_API_KEY")
|
|
340
|
+
if not api_key:
|
|
341
|
+
raise ConfigurationError(
|
|
342
|
+
"Together AI API key not found. Set TOGETHER_API_KEY environment variable.\n"
|
|
343
|
+
"You can add it to a .env file in:\n"
|
|
344
|
+
" - Current directory: .env\n"
|
|
345
|
+
" - Global config: ~/.massgen/.env",
|
|
346
|
+
)
|
|
347
|
+
elif base_url and "fireworks.ai" in base_url:
|
|
348
|
+
api_key = os.getenv("FIREWORKS_API_KEY")
|
|
349
|
+
if not api_key:
|
|
350
|
+
raise ConfigurationError(
|
|
351
|
+
"Fireworks AI API key not found. Set FIREWORKS_API_KEY environment variable.\n"
|
|
352
|
+
"You can add it to a .env file in:\n"
|
|
353
|
+
" - Current directory: .env\n"
|
|
354
|
+
" - Global config: ~/.massgen/.env",
|
|
355
|
+
)
|
|
356
|
+
elif base_url and "groq.com" in base_url:
|
|
357
|
+
api_key = os.getenv("GROQ_API_KEY")
|
|
358
|
+
if not api_key:
|
|
359
|
+
raise ConfigurationError(
|
|
360
|
+
"Groq API key not found. Set GROQ_API_KEY environment variable.\n" "You can add it to a .env file in:\n" " - Current directory: .env\n" " - Global config: ~/.massgen/.env",
|
|
361
|
+
)
|
|
362
|
+
elif base_url and "nebius.com" in base_url:
|
|
363
|
+
api_key = os.getenv("NEBIUS_API_KEY")
|
|
364
|
+
if not api_key:
|
|
365
|
+
raise ConfigurationError(
|
|
366
|
+
"Nebius AI Studio API key not found. Set NEBIUS_API_KEY environment variable.\n"
|
|
367
|
+
"You can add it to a .env file in:\n"
|
|
368
|
+
" - Current directory: .env\n"
|
|
369
|
+
" - Global config: ~/.massgen/.env",
|
|
370
|
+
)
|
|
371
|
+
elif base_url and "openrouter.ai" in base_url:
|
|
372
|
+
api_key = os.getenv("OPENROUTER_API_KEY")
|
|
373
|
+
if not api_key:
|
|
374
|
+
raise ConfigurationError(
|
|
375
|
+
"OpenRouter API key not found. Set OPENROUTER_API_KEY environment variable.\n"
|
|
376
|
+
"You can add it to a .env file in:\n"
|
|
377
|
+
" - Current directory: .env\n"
|
|
378
|
+
" - Global config: ~/.massgen/.env",
|
|
379
|
+
)
|
|
380
|
+
elif base_url and ("z.ai" in base_url or "bigmodel.cn" in base_url):
|
|
381
|
+
api_key = os.getenv("ZAI_API_KEY")
|
|
382
|
+
if not api_key:
|
|
383
|
+
raise ConfigurationError(
|
|
384
|
+
"ZAI API key not found. Set ZAI_API_KEY environment variable.\n" "You can add it to a .env file in:\n" " - Current directory: .env\n" " - Global config: ~/.massgen/.env",
|
|
385
|
+
)
|
|
386
|
+
elif base_url and ("moonshot.ai" in base_url or "moonshot.cn" in base_url):
|
|
387
|
+
api_key = os.getenv("MOONSHOT_API_KEY") or os.getenv("KIMI_API_KEY")
|
|
388
|
+
if not api_key:
|
|
389
|
+
raise ConfigurationError(
|
|
390
|
+
"Kimi/Moonshot API key not found. Set MOONSHOT_API_KEY or KIMI_API_KEY environment variable.\n"
|
|
391
|
+
"You can add it to a .env file in:\n"
|
|
392
|
+
" - Current directory: .env\n"
|
|
393
|
+
" - Global config: ~/.massgen/.env",
|
|
394
|
+
)
|
|
395
|
+
elif base_url and "poe.com" in base_url:
|
|
396
|
+
api_key = os.getenv("POE_API_KEY")
|
|
397
|
+
if not api_key:
|
|
398
|
+
raise ConfigurationError(
|
|
399
|
+
"POE API key not found. Set POE_API_KEY environment variable.\n" "You can add it to a .env file in:\n" " - Current directory: .env\n" " - Global config: ~/.massgen/.env",
|
|
400
|
+
)
|
|
401
|
+
elif base_url and "aliyuncs.com" in base_url:
|
|
402
|
+
api_key = os.getenv("QWEN_API_KEY")
|
|
403
|
+
if not api_key:
|
|
404
|
+
raise ConfigurationError(
|
|
405
|
+
"Qwen API key not found. Set QWEN_API_KEY environment variable.\n" "You can add it to a .env file in:\n" " - Current directory: .env\n" " - Global config: ~/.massgen/.env",
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
return ChatCompletionsBackend(api_key=api_key, **kwargs)
|
|
409
|
+
|
|
410
|
+
elif backend_type == "zai":
|
|
411
|
+
# ZAI (Zhipu.ai) uses OpenAI-compatible Chat Completions at a custom base_url
|
|
412
|
+
# Supports both global (z.ai) and China (bigmodel.cn) endpoints
|
|
413
|
+
api_key = kwargs.get("api_key") or os.getenv("ZAI_API_KEY")
|
|
414
|
+
if not api_key:
|
|
415
|
+
raise ConfigurationError(
|
|
416
|
+
"ZAI API key not found. Set ZAI_API_KEY environment variable.\n" "You can add it to a .env file in:\n" " - Current directory: .env\n" " - Global config: ~/.massgen/.env",
|
|
417
|
+
)
|
|
418
|
+
return ChatCompletionsBackend(api_key=api_key, **kwargs)
|
|
419
|
+
|
|
420
|
+
elif backend_type == "lmstudio":
|
|
421
|
+
# LM Studio local server (OpenAI-compatible). Defaults handled by backend.
|
|
422
|
+
return LMStudioBackend(**kwargs)
|
|
423
|
+
|
|
424
|
+
elif backend_type == "vllm":
|
|
425
|
+
# vLLM local server (OpenAI-compatible). Defaults handled by backend.
|
|
426
|
+
return InferenceBackend(backend_type="vllm", **kwargs)
|
|
427
|
+
|
|
428
|
+
elif backend_type == "sglang":
|
|
429
|
+
# SGLang local server (OpenAI-compatible). Defaults handled by backend.
|
|
430
|
+
return InferenceBackend(backend_type="sglang", **kwargs)
|
|
431
|
+
|
|
432
|
+
elif backend_type == "claude_code":
|
|
433
|
+
# ClaudeCodeBackend using claude-code-sdk-python
|
|
434
|
+
# Authentication handled by backend (API key or subscription)
|
|
435
|
+
|
|
436
|
+
# Validate claude-code-sdk availability
|
|
437
|
+
try:
|
|
438
|
+
pass
|
|
439
|
+
except ImportError:
|
|
440
|
+
raise ConfigurationError("claude-code-sdk not found. Install with: pip install claude-code-sdk")
|
|
441
|
+
|
|
442
|
+
return ClaudeCodeBackend(**kwargs)
|
|
443
|
+
|
|
444
|
+
elif backend_type == "azure_openai":
|
|
445
|
+
api_key = kwargs.get("api_key") or os.getenv("AZURE_OPENAI_API_KEY")
|
|
446
|
+
endpoint = kwargs.get("base_url") or os.getenv("AZURE_OPENAI_ENDPOINT")
|
|
447
|
+
if not api_key:
|
|
448
|
+
raise ConfigurationError("Azure OpenAI API key not found. Set AZURE_OPENAI_API_KEY or provide in config.")
|
|
449
|
+
if not endpoint:
|
|
450
|
+
raise ConfigurationError("Azure OpenAI endpoint not found. Set AZURE_OPENAI_ENDPOINT or provide base_url in config.")
|
|
451
|
+
return AzureOpenAIBackend(**kwargs)
|
|
151
452
|
|
|
152
453
|
else:
|
|
153
454
|
raise ConfigurationError(f"Unsupported backend type: {backend_type}")
|
|
154
455
|
|
|
155
456
|
|
|
156
|
-
def create_agents_from_config(config: Dict[str, Any]) -> Dict[str, ConfigurableAgent]:
|
|
457
|
+
def create_agents_from_config(config: Dict[str, Any], orchestrator_config: Optional[Dict[str, Any]] = None) -> Dict[str, ConfigurableAgent]:
|
|
157
458
|
"""Create agents from configuration."""
|
|
158
459
|
agents = {}
|
|
159
460
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
461
|
+
agent_entries = [config["agent"]] if "agent" in config else config.get("agents", None)
|
|
462
|
+
|
|
463
|
+
if not agent_entries:
|
|
464
|
+
raise ConfigurationError("Configuration must contain either 'agent' or 'agents' section")
|
|
465
|
+
|
|
466
|
+
for i, agent_data in enumerate(agent_entries, start=1):
|
|
467
|
+
backend_config = agent_data.get("backend", {})
|
|
468
|
+
|
|
469
|
+
# Substitute variables like ${cwd} in backend config
|
|
470
|
+
if "cwd" in backend_config:
|
|
471
|
+
variables = {"cwd": backend_config["cwd"]}
|
|
472
|
+
backend_config = _substitute_variables(backend_config, variables)
|
|
164
473
|
|
|
165
474
|
# Infer backend type from model if not explicitly provided
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
475
|
+
backend_type = backend_config.get("type") or (get_backend_type_from_model(backend_config["model"]) if "model" in backend_config else None)
|
|
476
|
+
if not backend_type:
|
|
477
|
+
raise ConfigurationError("Backend type must be specified or inferrable from model")
|
|
478
|
+
|
|
479
|
+
# Add orchestrator context for filesystem setup if available
|
|
480
|
+
if orchestrator_config:
|
|
481
|
+
if "agent_temporary_workspace" in orchestrator_config:
|
|
482
|
+
backend_config["agent_temporary_workspace"] = orchestrator_config["agent_temporary_workspace"]
|
|
483
|
+
# Add orchestrator-level context_paths to all agents
|
|
484
|
+
if "context_paths" in orchestrator_config:
|
|
485
|
+
# Merge orchestrator context_paths with agent-specific ones
|
|
486
|
+
agent_context_paths = backend_config.get("context_paths", [])
|
|
487
|
+
orchestrator_context_paths = orchestrator_config["context_paths"]
|
|
488
|
+
|
|
489
|
+
# Deduplicate paths - orchestrator paths take precedence
|
|
490
|
+
merged_paths = orchestrator_context_paths.copy()
|
|
491
|
+
orchestrator_paths_set = {path.get("path") for path in orchestrator_context_paths}
|
|
492
|
+
|
|
493
|
+
for agent_path in agent_context_paths:
|
|
494
|
+
if agent_path.get("path") not in orchestrator_paths_set:
|
|
495
|
+
merged_paths.append(agent_path)
|
|
496
|
+
|
|
497
|
+
backend_config["context_paths"] = merged_paths
|
|
174
498
|
|
|
175
499
|
backend = create_backend(backend_type, **backend_config)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
500
|
+
backend_params = {k: v for k, v in backend_config.items() if k != "type"}
|
|
501
|
+
|
|
502
|
+
backend_type_lower = backend_type.lower()
|
|
503
|
+
if backend_type_lower == "openai":
|
|
504
|
+
agent_config = AgentConfig.create_openai_config(**backend_params)
|
|
505
|
+
elif backend_type_lower == "claude":
|
|
506
|
+
agent_config = AgentConfig.create_claude_config(**backend_params)
|
|
507
|
+
elif backend_type_lower == "grok":
|
|
508
|
+
agent_config = AgentConfig.create_grok_config(**backend_params)
|
|
509
|
+
elif backend_type_lower == "gemini":
|
|
510
|
+
agent_config = AgentConfig.create_gemini_config(**backend_params)
|
|
511
|
+
elif backend_type_lower == "zai":
|
|
512
|
+
agent_config = AgentConfig.create_zai_config(**backend_params)
|
|
513
|
+
elif backend_type_lower == "chatcompletion":
|
|
514
|
+
agent_config = AgentConfig.create_chatcompletion_config(**backend_params)
|
|
515
|
+
elif backend_type_lower == "lmstudio":
|
|
516
|
+
agent_config = AgentConfig.create_lmstudio_config(**backend_params)
|
|
517
|
+
elif backend_type_lower == "vllm":
|
|
518
|
+
agent_config = AgentConfig.create_vllm_config(**backend_params)
|
|
519
|
+
elif backend_type_lower == "sglang":
|
|
520
|
+
agent_config = AgentConfig.create_sglang_config(**backend_params)
|
|
194
521
|
else:
|
|
195
|
-
# Fallback to basic config
|
|
196
522
|
agent_config = AgentConfig(backend_params=backend_config)
|
|
197
523
|
|
|
198
|
-
|
|
199
|
-
agent_config.agent_id = agent_config_data.get("id", "agent1")
|
|
200
|
-
agent_config.custom_system_instruction = agent_config_data.get("system_message")
|
|
201
|
-
|
|
202
|
-
agent = ConfigurableAgent(config=agent_config, backend=backend)
|
|
203
|
-
agents[agent.agent_id] = agent
|
|
204
|
-
|
|
205
|
-
# Handle multiple agents configuration
|
|
206
|
-
elif "agents" in config:
|
|
207
|
-
for agent_config_data in config["agents"]:
|
|
208
|
-
backend_config = agent_config_data.get("backend", {})
|
|
209
|
-
|
|
210
|
-
# Infer backend type from model if not explicitly provided
|
|
211
|
-
if "type" not in backend_config and "model" in backend_config:
|
|
212
|
-
backend_type = get_backend_type_from_model(backend_config["model"])
|
|
213
|
-
else:
|
|
214
|
-
backend_type = backend_config.get("type")
|
|
215
|
-
if not backend_type:
|
|
216
|
-
raise ConfigurationError(
|
|
217
|
-
"Backend type must be specified or inferrable from model"
|
|
218
|
-
)
|
|
524
|
+
agent_config.agent_id = agent_data.get("id", f"agent{i}")
|
|
219
525
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
)
|
|
227
|
-
elif backend_type.lower() == "claude":
|
|
228
|
-
agent_config = AgentConfig.create_claude_config(
|
|
229
|
-
**{k: v for k, v in backend_config.items() if k != "type"}
|
|
230
|
-
)
|
|
231
|
-
elif backend_type.lower() == "grok":
|
|
232
|
-
agent_config = AgentConfig.create_grok_config(
|
|
233
|
-
**{k: v for k, v in backend_config.items() if k != "type"}
|
|
234
|
-
)
|
|
526
|
+
# Route system_message to backend-specific system prompt parameter
|
|
527
|
+
system_msg = agent_data.get("system_message")
|
|
528
|
+
if system_msg:
|
|
529
|
+
if backend_type_lower == "claude_code":
|
|
530
|
+
# For Claude Code, use append_system_prompt to preserve Claude Code capabilities
|
|
531
|
+
agent_config.backend_params["append_system_prompt"] = system_msg
|
|
235
532
|
else:
|
|
236
|
-
#
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
# Set agent ID and system message
|
|
240
|
-
agent_config.agent_id = agent_config_data.get("id", f"agent{len(agents)+1}")
|
|
241
|
-
agent_config.custom_system_instruction = agent_config_data.get(
|
|
242
|
-
"system_message"
|
|
243
|
-
)
|
|
533
|
+
# For other backends, fall back to deprecated custom_system_instruction
|
|
534
|
+
# TODO: Add backend-specific routing for other backends
|
|
535
|
+
agent_config.custom_system_instruction = system_msg
|
|
244
536
|
|
|
245
|
-
|
|
246
|
-
agents[agent.agent_id] = agent
|
|
537
|
+
# Timeout configuration will be applied to orchestrator instead of individual agents
|
|
247
538
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
"Configuration must contain either 'agent' or 'agents' section"
|
|
251
|
-
)
|
|
539
|
+
agent = ConfigurableAgent(config=agent_config, backend=backend)
|
|
540
|
+
agents[agent.config.agent_id] = agent
|
|
252
541
|
|
|
253
542
|
return agents
|
|
254
543
|
|
|
255
544
|
|
|
256
545
|
def create_simple_config(
|
|
257
|
-
backend_type: str,
|
|
546
|
+
backend_type: str,
|
|
547
|
+
model: str,
|
|
548
|
+
system_message: Optional[str] = None,
|
|
549
|
+
base_url: Optional[str] = None,
|
|
550
|
+
ui_config: Optional[Dict[str, Any]] = None,
|
|
258
551
|
) -> Dict[str, Any]:
|
|
259
552
|
"""Create a simple single-agent configuration."""
|
|
260
|
-
|
|
553
|
+
backend_config = {"type": backend_type, "model": model}
|
|
554
|
+
if base_url:
|
|
555
|
+
backend_config["base_url"] = base_url
|
|
556
|
+
|
|
557
|
+
# Add required workspace configuration for Claude Code backend
|
|
558
|
+
if backend_type == "claude_code":
|
|
559
|
+
backend_config["cwd"] = "workspace1"
|
|
560
|
+
|
|
561
|
+
# Use provided UI config or default to rich_terminal for CLI usage
|
|
562
|
+
if ui_config is None:
|
|
563
|
+
ui_config = {"display_type": "rich_terminal", "logging_enabled": True}
|
|
564
|
+
|
|
565
|
+
config = {
|
|
261
566
|
"agent": {
|
|
262
567
|
"id": "agent1",
|
|
263
|
-
"backend":
|
|
568
|
+
"backend": backend_config,
|
|
264
569
|
"system_message": system_message or "You are a helpful AI assistant.",
|
|
265
570
|
},
|
|
266
|
-
"ui":
|
|
571
|
+
"ui": ui_config,
|
|
267
572
|
}
|
|
268
573
|
|
|
574
|
+
# Add orchestrator config with .massgen/ structure for Claude Code
|
|
575
|
+
if backend_type == "claude_code":
|
|
576
|
+
config["orchestrator"] = {
|
|
577
|
+
"snapshot_storage": ".massgen/snapshots",
|
|
578
|
+
"agent_temporary_workspace": ".massgen/temp_workspaces",
|
|
579
|
+
"session_storage": ".massgen/sessions",
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return config
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def validate_context_paths(config: Dict[str, Any]) -> None:
|
|
586
|
+
"""Validate that all context paths in the config exist.
|
|
587
|
+
|
|
588
|
+
Context paths can be either files or directories.
|
|
589
|
+
File-level context paths allow access to specific files without exposing sibling files.
|
|
590
|
+
Raises ConfigurationError with clear message if any paths don't exist.
|
|
591
|
+
"""
|
|
592
|
+
orchestrator_cfg = config.get("orchestrator", {})
|
|
593
|
+
context_paths = orchestrator_cfg.get("context_paths", [])
|
|
594
|
+
|
|
595
|
+
missing_paths = []
|
|
596
|
+
|
|
597
|
+
for context_path_config in context_paths:
|
|
598
|
+
if isinstance(context_path_config, dict):
|
|
599
|
+
path = context_path_config.get("path")
|
|
600
|
+
else:
|
|
601
|
+
# Handle string format for backwards compatibility
|
|
602
|
+
path = context_path_config
|
|
603
|
+
|
|
604
|
+
if path:
|
|
605
|
+
path_obj = Path(path)
|
|
606
|
+
if not path_obj.exists():
|
|
607
|
+
missing_paths.append(path)
|
|
608
|
+
|
|
609
|
+
if missing_paths:
|
|
610
|
+
errors = ["Context paths not found:"]
|
|
611
|
+
for path in missing_paths:
|
|
612
|
+
errors.append(f" - {path}")
|
|
613
|
+
errors.append("\nPlease update your configuration with valid paths.")
|
|
614
|
+
raise ConfigurationError("\n".join(errors))
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def relocate_filesystem_paths(config: Dict[str, Any]) -> None:
|
|
618
|
+
"""Relocate filesystem paths (orchestrator paths and agent workspaces) to be under .massgen/ directory.
|
|
619
|
+
|
|
620
|
+
Modifies the config in-place to ensure all MassGen state is organized
|
|
621
|
+
under .massgen/ for clean project structure.
|
|
622
|
+
"""
|
|
623
|
+
massgen_dir = Path(".massgen")
|
|
624
|
+
|
|
625
|
+
# Relocate orchestrator paths
|
|
626
|
+
orchestrator_cfg = config.get("orchestrator", {})
|
|
627
|
+
if orchestrator_cfg:
|
|
628
|
+
path_fields = [
|
|
629
|
+
"snapshot_storage",
|
|
630
|
+
"agent_temporary_workspace",
|
|
631
|
+
"session_storage",
|
|
632
|
+
]
|
|
633
|
+
|
|
634
|
+
for field in path_fields:
|
|
635
|
+
if field in orchestrator_cfg:
|
|
636
|
+
user_path = orchestrator_cfg[field]
|
|
637
|
+
# If user provided an absolute path or already starts with .massgen/, keep as-is
|
|
638
|
+
if Path(user_path).is_absolute() or user_path.startswith(".massgen/"):
|
|
639
|
+
continue
|
|
640
|
+
# Otherwise, relocate under .massgen/
|
|
641
|
+
orchestrator_cfg[field] = str(massgen_dir / user_path)
|
|
642
|
+
|
|
643
|
+
# Relocate agent workspaces (cwd fields)
|
|
644
|
+
agent_entries = [config["agent"]] if "agent" in config else config.get("agents", [])
|
|
645
|
+
for agent_data in agent_entries:
|
|
646
|
+
backend_config = agent_data.get("backend", {})
|
|
647
|
+
if "cwd" in backend_config:
|
|
648
|
+
user_cwd = backend_config["cwd"]
|
|
649
|
+
# If user provided an absolute path or already starts with .massgen/, keep as-is
|
|
650
|
+
if Path(user_cwd).is_absolute() or user_cwd.startswith(".massgen/"):
|
|
651
|
+
continue
|
|
652
|
+
# Otherwise, relocate under .massgen/workspaces/
|
|
653
|
+
backend_config["cwd"] = str(massgen_dir / "workspaces" / user_cwd)
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def load_previous_turns(session_info: Dict[str, Any], session_storage: str) -> List[Dict[str, Any]]:
|
|
657
|
+
"""
|
|
658
|
+
Load previous turns from session storage.
|
|
659
|
+
|
|
660
|
+
Returns:
|
|
661
|
+
List of previous turn metadata dicts
|
|
662
|
+
"""
|
|
663
|
+
session_id = session_info.get("session_id")
|
|
664
|
+
if not session_id:
|
|
665
|
+
return []
|
|
666
|
+
|
|
667
|
+
session_dir = Path(session_storage) / session_id
|
|
668
|
+
if not session_dir.exists():
|
|
669
|
+
return []
|
|
670
|
+
|
|
671
|
+
previous_turns = []
|
|
672
|
+
turn_num = 1
|
|
673
|
+
|
|
674
|
+
while True:
|
|
675
|
+
turn_dir = session_dir / f"turn_{turn_num}"
|
|
676
|
+
if not turn_dir.exists():
|
|
677
|
+
break
|
|
678
|
+
|
|
679
|
+
metadata_file = turn_dir / "metadata.json"
|
|
680
|
+
if metadata_file.exists():
|
|
681
|
+
metadata = json.loads(metadata_file.read_text(encoding="utf-8"))
|
|
682
|
+
# Use absolute path for workspace
|
|
683
|
+
workspace_path = (turn_dir / "workspace").resolve()
|
|
684
|
+
previous_turns.append(
|
|
685
|
+
{
|
|
686
|
+
"turn": turn_num,
|
|
687
|
+
"path": str(workspace_path),
|
|
688
|
+
"task": metadata.get("task", ""),
|
|
689
|
+
"winning_agent": metadata.get("winning_agent", ""),
|
|
690
|
+
},
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
turn_num += 1
|
|
694
|
+
|
|
695
|
+
return previous_turns
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
async def handle_session_persistence(
|
|
699
|
+
orchestrator,
|
|
700
|
+
question: str,
|
|
701
|
+
session_info: Dict[str, Any],
|
|
702
|
+
session_storage: str,
|
|
703
|
+
) -> tuple[Optional[str], int, Optional[str]]:
|
|
704
|
+
"""
|
|
705
|
+
Handle session persistence after orchestrator completes.
|
|
706
|
+
|
|
707
|
+
Returns:
|
|
708
|
+
tuple: (session_id, updated_turn_number, normalized_answer)
|
|
709
|
+
"""
|
|
710
|
+
# Get final result from orchestrator
|
|
711
|
+
final_result = orchestrator.get_final_result()
|
|
712
|
+
if not final_result:
|
|
713
|
+
# No filesystem work to persist
|
|
714
|
+
return (session_info.get("session_id"), session_info.get("current_turn", 0), None)
|
|
715
|
+
|
|
716
|
+
# Initialize or reuse session ID
|
|
717
|
+
session_id = session_info.get("session_id")
|
|
718
|
+
if not session_id:
|
|
719
|
+
session_id = f"session_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
|
720
|
+
|
|
721
|
+
# Increment turn
|
|
722
|
+
current_turn = session_info.get("current_turn", 0) + 1
|
|
723
|
+
|
|
724
|
+
# Create turn directory
|
|
725
|
+
session_dir = Path(session_storage) / session_id
|
|
726
|
+
turn_dir = session_dir / f"turn_{current_turn}"
|
|
727
|
+
turn_dir.mkdir(parents=True, exist_ok=True)
|
|
728
|
+
|
|
729
|
+
# Normalize answer paths
|
|
730
|
+
final_answer = final_result["final_answer"]
|
|
731
|
+
workspace_path = final_result.get("workspace_path")
|
|
732
|
+
turn_workspace_path = (turn_dir / "workspace").resolve() # Make absolute
|
|
733
|
+
|
|
734
|
+
if workspace_path:
|
|
735
|
+
# Replace workspace paths in answer with absolute path
|
|
736
|
+
normalized_answer = final_answer.replace(workspace_path, str(turn_workspace_path))
|
|
737
|
+
else:
|
|
738
|
+
normalized_answer = final_answer
|
|
739
|
+
|
|
740
|
+
# Save normalized answer
|
|
741
|
+
answer_file = turn_dir / "answer.txt"
|
|
742
|
+
answer_file.write_text(normalized_answer, encoding="utf-8")
|
|
743
|
+
|
|
744
|
+
# Save metadata
|
|
745
|
+
metadata = {
|
|
746
|
+
"turn": current_turn,
|
|
747
|
+
"timestamp": datetime.now().isoformat(),
|
|
748
|
+
"winning_agent": final_result["winning_agent_id"],
|
|
749
|
+
"task": question,
|
|
750
|
+
"session_id": session_id,
|
|
751
|
+
}
|
|
752
|
+
metadata_file = turn_dir / "metadata.json"
|
|
753
|
+
metadata_file.write_text(json.dumps(metadata, indent=2), encoding="utf-8")
|
|
754
|
+
|
|
755
|
+
# Create/update session summary for easy viewing
|
|
756
|
+
session_summary_file = session_dir / "SESSION_SUMMARY.txt"
|
|
757
|
+
summary_lines = []
|
|
758
|
+
|
|
759
|
+
if session_summary_file.exists():
|
|
760
|
+
summary_lines = session_summary_file.read_text(encoding="utf-8").splitlines()
|
|
761
|
+
else:
|
|
762
|
+
summary_lines.append("=" * 80)
|
|
763
|
+
summary_lines.append(f"Multi-Turn Session: {session_id}")
|
|
764
|
+
summary_lines.append("=" * 80)
|
|
765
|
+
summary_lines.append("")
|
|
766
|
+
|
|
767
|
+
# Add turn separator and info
|
|
768
|
+
summary_lines.append("")
|
|
769
|
+
summary_lines.append("=" * 80)
|
|
770
|
+
summary_lines.append(f"TURN {current_turn}")
|
|
771
|
+
summary_lines.append("=" * 80)
|
|
772
|
+
summary_lines.append(f"Timestamp: {metadata['timestamp']}")
|
|
773
|
+
summary_lines.append(f"Winning Agent: {metadata['winning_agent']}")
|
|
774
|
+
summary_lines.append(f"Task: {question}")
|
|
775
|
+
summary_lines.append(f"Workspace: {turn_workspace_path}")
|
|
776
|
+
summary_lines.append(f"Answer: See {(turn_dir / 'answer.txt').resolve()}")
|
|
777
|
+
summary_lines.append("")
|
|
778
|
+
|
|
779
|
+
session_summary_file.write_text("\n".join(summary_lines), encoding="utf-8")
|
|
780
|
+
|
|
781
|
+
# Copy workspace if it exists
|
|
782
|
+
if workspace_path and Path(workspace_path).exists():
|
|
783
|
+
shutil.copytree(workspace_path, turn_workspace_path, dirs_exist_ok=True)
|
|
784
|
+
|
|
785
|
+
return (session_id, current_turn, normalized_answer)
|
|
786
|
+
|
|
269
787
|
|
|
270
788
|
async def run_question_with_history(
|
|
271
789
|
question: str,
|
|
272
790
|
agents: Dict[str, SingleAgent],
|
|
273
791
|
ui_config: Dict[str, Any],
|
|
274
792
|
history: List[Dict[str, Any]],
|
|
275
|
-
|
|
276
|
-
|
|
793
|
+
session_info: Dict[str, Any],
|
|
794
|
+
**kwargs,
|
|
795
|
+
) -> tuple[str, Optional[str], int]:
|
|
796
|
+
"""Run MassGen with a question and conversation history.
|
|
797
|
+
|
|
798
|
+
Returns:
|
|
799
|
+
tuple: (response_text, session_id, turn_number)
|
|
800
|
+
"""
|
|
277
801
|
# Build messages including history
|
|
278
802
|
messages = history.copy()
|
|
279
803
|
messages.append({"role": "user", "content": question})
|
|
280
804
|
|
|
281
805
|
# Check if we should use orchestrator for single agents (default: False for backward compatibility)
|
|
282
|
-
use_orchestrator_for_single = ui_config.get(
|
|
283
|
-
"use_orchestrator_for_single_agent", True
|
|
284
|
-
)
|
|
806
|
+
use_orchestrator_for_single = ui_config.get("use_orchestrator_for_single_agent", True)
|
|
285
807
|
|
|
286
808
|
if len(agents) == 1 and not use_orchestrator_for_single:
|
|
287
809
|
# Single agent mode with history
|
|
@@ -290,7 +812,7 @@ async def run_question_with_history(
|
|
|
290
812
|
print(f"Agent: {agent.agent_id}", flush=True)
|
|
291
813
|
if history:
|
|
292
814
|
print(f"History: {len(history)//2} previous exchanges", flush=True)
|
|
293
|
-
print(f"Question: {
|
|
815
|
+
print(f"Question: {question}", flush=True)
|
|
294
816
|
print("\n" + "=" * 60, flush=True)
|
|
295
817
|
|
|
296
818
|
response_content = ""
|
|
@@ -305,25 +827,54 @@ async def run_question_with_history(
|
|
|
305
827
|
continue
|
|
306
828
|
elif chunk.type == "error":
|
|
307
829
|
print(f"\n❌ Error: {chunk.error}", flush=True)
|
|
308
|
-
return ""
|
|
830
|
+
return ("", session_info.get("session_id"), session_info.get("current_turn", 0))
|
|
309
831
|
|
|
310
832
|
print("\n" + "=" * 60, flush=True)
|
|
311
|
-
|
|
833
|
+
# Single agent mode doesn't use session storage
|
|
834
|
+
return (response_content, session_info.get("session_id"), session_info.get("current_turn", 0))
|
|
312
835
|
|
|
313
836
|
else:
|
|
314
837
|
# Multi-agent mode with history
|
|
315
|
-
orchestrator
|
|
838
|
+
# Create orchestrator config with timeout settings
|
|
839
|
+
timeout_config = kwargs.get("timeout_config")
|
|
840
|
+
orchestrator_config = AgentConfig()
|
|
841
|
+
if timeout_config:
|
|
842
|
+
orchestrator_config.timeout_config = timeout_config
|
|
843
|
+
|
|
844
|
+
# Get orchestrator parameters from config
|
|
845
|
+
orchestrator_cfg = kwargs.get("orchestrator", {})
|
|
846
|
+
|
|
847
|
+
# Get context sharing parameters
|
|
848
|
+
snapshot_storage = orchestrator_cfg.get("snapshot_storage")
|
|
849
|
+
agent_temporary_workspace = orchestrator_cfg.get("agent_temporary_workspace")
|
|
850
|
+
session_storage = orchestrator_cfg.get("session_storage", "sessions") # Default to "sessions"
|
|
851
|
+
|
|
852
|
+
# Get debug/test parameters
|
|
853
|
+
if orchestrator_cfg.get("skip_coordination_rounds", False):
|
|
854
|
+
orchestrator_config.skip_coordination_rounds = True
|
|
855
|
+
|
|
856
|
+
# Load previous turns from session storage for multi-turn conversations
|
|
857
|
+
previous_turns = load_previous_turns(session_info, session_storage)
|
|
858
|
+
|
|
859
|
+
orchestrator = Orchestrator(
|
|
860
|
+
agents=agents,
|
|
861
|
+
config=orchestrator_config,
|
|
862
|
+
snapshot_storage=snapshot_storage,
|
|
863
|
+
agent_temporary_workspace=agent_temporary_workspace,
|
|
864
|
+
previous_turns=previous_turns,
|
|
865
|
+
)
|
|
316
866
|
# Create a fresh UI instance for each question to ensure clean state
|
|
317
867
|
ui = CoordinationUI(
|
|
318
868
|
display_type=ui_config.get("display_type", "rich_terminal"),
|
|
319
869
|
logging_enabled=ui_config.get("logging_enabled", True),
|
|
870
|
+
enable_final_presentation=True, # Required for multi-turn: ensures final answer is saved
|
|
320
871
|
)
|
|
321
872
|
|
|
322
873
|
print(f"\n🤖 {BRIGHT_CYAN}Multi-Agent Mode{RESET}", flush=True)
|
|
323
874
|
print(f"Agents: {', '.join(agents.keys())}", flush=True)
|
|
324
875
|
if history:
|
|
325
876
|
print(f"History: {len(history)//2} previous exchanges", flush=True)
|
|
326
|
-
print(f"Question: {
|
|
877
|
+
print(f"Question: {question}", flush=True)
|
|
327
878
|
print("\n" + "=" * 60, flush=True)
|
|
328
879
|
|
|
329
880
|
# For multi-agent with history, we need to use a different approach
|
|
@@ -332,29 +883,30 @@ async def run_question_with_history(
|
|
|
332
883
|
if history and len(history) > 0:
|
|
333
884
|
# Use coordination UI with conversation context
|
|
334
885
|
# Extract current question from messages
|
|
335
|
-
current_question = (
|
|
336
|
-
messages[-1].get("content", question) if messages else question
|
|
337
|
-
)
|
|
886
|
+
current_question = messages[-1].get("content", question) if messages else question
|
|
338
887
|
|
|
339
888
|
# Pass the full message context to the UI coordination
|
|
340
|
-
response_content = await ui.coordinate_with_context(
|
|
341
|
-
orchestrator, current_question, messages
|
|
342
|
-
)
|
|
889
|
+
response_content = await ui.coordinate_with_context(orchestrator, current_question, messages)
|
|
343
890
|
else:
|
|
344
891
|
# Standard coordination for new conversations
|
|
345
892
|
response_content = await ui.coordinate(orchestrator, question)
|
|
346
893
|
|
|
347
|
-
|
|
894
|
+
# Handle session persistence if applicable
|
|
895
|
+
session_id_to_use, updated_turn, normalized_response = await handle_session_persistence(
|
|
896
|
+
orchestrator,
|
|
897
|
+
question,
|
|
898
|
+
session_info,
|
|
899
|
+
session_storage,
|
|
900
|
+
)
|
|
901
|
+
|
|
902
|
+
# Return normalized response so conversation history has correct paths
|
|
903
|
+
return (normalized_response or response_content, session_id_to_use, updated_turn)
|
|
348
904
|
|
|
349
905
|
|
|
350
|
-
async def run_single_question(
|
|
351
|
-
question: str, agents: Dict[str, SingleAgent], ui_config: Dict[str, Any]
|
|
352
|
-
) -> str:
|
|
906
|
+
async def run_single_question(question: str, agents: Dict[str, SingleAgent], ui_config: Dict[str, Any], **kwargs) -> str:
|
|
353
907
|
"""Run MassGen with a single question."""
|
|
354
908
|
# Check if we should use orchestrator for single agents (default: False for backward compatibility)
|
|
355
|
-
use_orchestrator_for_single = ui_config.get(
|
|
356
|
-
"use_orchestrator_for_single_agent", True
|
|
357
|
-
)
|
|
909
|
+
use_orchestrator_for_single = ui_config.get("use_orchestrator_for_single_agent", True)
|
|
358
910
|
|
|
359
911
|
if len(agents) == 1 and not use_orchestrator_for_single:
|
|
360
912
|
# Single agent mode with existing SimpleDisplay frontend
|
|
@@ -362,7 +914,7 @@ async def run_single_question(
|
|
|
362
914
|
|
|
363
915
|
print(f"\n🤖 {BRIGHT_CYAN}Single Agent Mode{RESET}", flush=True)
|
|
364
916
|
print(f"Agent: {agent.agent_id}", flush=True)
|
|
365
|
-
print(f"Question: {
|
|
917
|
+
print(f"Question: {question}", flush=True)
|
|
366
918
|
print("\n" + "=" * 60, flush=True)
|
|
367
919
|
|
|
368
920
|
messages = [{"role": "user", "content": question}]
|
|
@@ -384,67 +936,472 @@ async def run_single_question(
|
|
|
384
936
|
|
|
385
937
|
else:
|
|
386
938
|
# Multi-agent mode
|
|
387
|
-
orchestrator
|
|
939
|
+
# Create orchestrator config with timeout settings
|
|
940
|
+
timeout_config = kwargs.get("timeout_config")
|
|
941
|
+
orchestrator_config = AgentConfig()
|
|
942
|
+
if timeout_config:
|
|
943
|
+
orchestrator_config.timeout_config = timeout_config
|
|
944
|
+
|
|
945
|
+
# Get orchestrator parameters from config
|
|
946
|
+
orchestrator_cfg = kwargs.get("orchestrator", {})
|
|
947
|
+
|
|
948
|
+
# Get context sharing parameters
|
|
949
|
+
snapshot_storage = orchestrator_cfg.get("snapshot_storage")
|
|
950
|
+
agent_temporary_workspace = orchestrator_cfg.get("agent_temporary_workspace")
|
|
951
|
+
|
|
952
|
+
# Get debug/test parameters
|
|
953
|
+
if orchestrator_cfg.get("skip_coordination_rounds", False):
|
|
954
|
+
orchestrator_config.skip_coordination_rounds = True
|
|
955
|
+
|
|
956
|
+
orchestrator = Orchestrator(
|
|
957
|
+
agents=agents,
|
|
958
|
+
config=orchestrator_config,
|
|
959
|
+
snapshot_storage=snapshot_storage,
|
|
960
|
+
agent_temporary_workspace=agent_temporary_workspace,
|
|
961
|
+
)
|
|
388
962
|
# Create a fresh UI instance for each question to ensure clean state
|
|
389
963
|
ui = CoordinationUI(
|
|
390
964
|
display_type=ui_config.get("display_type", "rich_terminal"),
|
|
391
965
|
logging_enabled=ui_config.get("logging_enabled", True),
|
|
966
|
+
enable_final_presentation=True, # Ensures final presentation is generated
|
|
392
967
|
)
|
|
393
968
|
|
|
394
969
|
print(f"\n🤖 {BRIGHT_CYAN}Multi-Agent Mode{RESET}", flush=True)
|
|
395
970
|
print(f"Agents: {', '.join(agents.keys())}", flush=True)
|
|
396
|
-
print(f"Question: {
|
|
971
|
+
print(f"Question: {question}", flush=True)
|
|
397
972
|
print("\n" + "=" * 60, flush=True)
|
|
398
973
|
|
|
399
974
|
final_response = await ui.coordinate(orchestrator, question)
|
|
400
975
|
return final_response
|
|
401
976
|
|
|
402
977
|
|
|
978
|
+
def prompt_for_context_paths(original_config: Dict[str, Any], orchestrator_cfg: Dict[str, Any]) -> bool:
|
|
979
|
+
"""Prompt user to add context paths in interactive mode.
|
|
980
|
+
|
|
981
|
+
Returns True if config was modified, False otherwise.
|
|
982
|
+
"""
|
|
983
|
+
# Check if filesystem is enabled (at least one agent has cwd)
|
|
984
|
+
agent_entries = [original_config["agent"]] if "agent" in original_config else original_config.get("agents", [])
|
|
985
|
+
has_filesystem = any("cwd" in agent.get("backend", {}) for agent in agent_entries)
|
|
986
|
+
|
|
987
|
+
if not has_filesystem:
|
|
988
|
+
return False
|
|
989
|
+
|
|
990
|
+
# Show current context paths
|
|
991
|
+
existing_paths = orchestrator_cfg.get("context_paths", [])
|
|
992
|
+
cwd = Path.cwd()
|
|
993
|
+
|
|
994
|
+
# Use Rich for better display
|
|
995
|
+
from rich.console import Console as RichConsole
|
|
996
|
+
from rich.panel import Panel as RichPanel
|
|
997
|
+
|
|
998
|
+
rich_console = RichConsole()
|
|
999
|
+
|
|
1000
|
+
# Build context paths display
|
|
1001
|
+
context_content = []
|
|
1002
|
+
if existing_paths:
|
|
1003
|
+
for path_config in existing_paths:
|
|
1004
|
+
path = path_config.get("path") if isinstance(path_config, dict) else path_config
|
|
1005
|
+
permission = path_config.get("permission", "read") if isinstance(path_config, dict) else "read"
|
|
1006
|
+
context_content.append(f" [green]✓[/green] {path} [dim]({permission})[/dim]")
|
|
1007
|
+
else:
|
|
1008
|
+
context_content.append(" [yellow]No context paths configured[/yellow]")
|
|
1009
|
+
|
|
1010
|
+
context_panel = RichPanel(
|
|
1011
|
+
"\n".join(context_content),
|
|
1012
|
+
title="[bold bright_cyan]📂 Context Paths[/bold bright_cyan]",
|
|
1013
|
+
border_style="cyan",
|
|
1014
|
+
padding=(0, 2),
|
|
1015
|
+
width=80,
|
|
1016
|
+
)
|
|
1017
|
+
rich_console.print(context_panel)
|
|
1018
|
+
print()
|
|
1019
|
+
|
|
1020
|
+
# Check if CWD is already in context paths
|
|
1021
|
+
cwd_str = str(cwd)
|
|
1022
|
+
cwd_already_added = any((path_config.get("path") if isinstance(path_config, dict) else path_config) == cwd_str for path_config in existing_paths)
|
|
1023
|
+
|
|
1024
|
+
if not cwd_already_added:
|
|
1025
|
+
# Create prompt panel
|
|
1026
|
+
prompt_content = [
|
|
1027
|
+
"[bold cyan]Add current directory as context path?[/bold cyan]",
|
|
1028
|
+
f" [yellow]{cwd}[/yellow]",
|
|
1029
|
+
"",
|
|
1030
|
+
" [dim]Context paths give agents access to your project files.[/dim]",
|
|
1031
|
+
" [dim]• Read-only during coordination (prevents conflicts)[/dim]",
|
|
1032
|
+
" [dim]• Write permission for final agent to save results[/dim]",
|
|
1033
|
+
"",
|
|
1034
|
+
" [dim]Options:[/dim]",
|
|
1035
|
+
" [green]Y[/green] → Add with write permission (default)",
|
|
1036
|
+
" [cyan]P[/cyan] → Add with protected paths (e.g., .env, secrets)",
|
|
1037
|
+
" [yellow]N[/yellow] → Skip",
|
|
1038
|
+
" [blue]C[/blue] → Add custom path",
|
|
1039
|
+
]
|
|
1040
|
+
prompt_panel = RichPanel(
|
|
1041
|
+
"\n".join(prompt_content),
|
|
1042
|
+
border_style="cyan",
|
|
1043
|
+
padding=(1, 2),
|
|
1044
|
+
width=80,
|
|
1045
|
+
)
|
|
1046
|
+
rich_console.print(prompt_panel)
|
|
1047
|
+
print()
|
|
1048
|
+
try:
|
|
1049
|
+
response = input(f" {BRIGHT_CYAN}Your choice [Y/P/N/C]:{RESET} ").strip().lower()
|
|
1050
|
+
|
|
1051
|
+
if response in ["y", "yes", ""]:
|
|
1052
|
+
# Add CWD with write permission
|
|
1053
|
+
if "context_paths" not in orchestrator_cfg:
|
|
1054
|
+
orchestrator_cfg["context_paths"] = []
|
|
1055
|
+
orchestrator_cfg["context_paths"].append({"path": cwd_str, "permission": "write"})
|
|
1056
|
+
print(f" {BRIGHT_GREEN}✅ Added: {cwd} (write){RESET}", flush=True)
|
|
1057
|
+
return True
|
|
1058
|
+
elif response in ["p", "protected"]:
|
|
1059
|
+
# Add CWD with write permission and protected paths
|
|
1060
|
+
protected_paths = []
|
|
1061
|
+
print(f"\n {BRIGHT_CYAN}Enter protected paths (one per line, empty to finish):{RESET}", flush=True)
|
|
1062
|
+
print(f" {BRIGHT_YELLOW}Tip: Protected paths are relative to {cwd}{RESET}", flush=True)
|
|
1063
|
+
while True:
|
|
1064
|
+
protected_input = input(f" {BRIGHT_CYAN}→{RESET} ").strip()
|
|
1065
|
+
if not protected_input:
|
|
1066
|
+
break
|
|
1067
|
+
protected_paths.append(protected_input)
|
|
1068
|
+
print(f" {BRIGHT_GREEN}✓ Added: {protected_input}{RESET}", flush=True)
|
|
1069
|
+
|
|
1070
|
+
if "context_paths" not in orchestrator_cfg:
|
|
1071
|
+
orchestrator_cfg["context_paths"] = []
|
|
1072
|
+
|
|
1073
|
+
context_config = {"path": cwd_str, "permission": "write"}
|
|
1074
|
+
if protected_paths:
|
|
1075
|
+
context_config["protected_paths"] = protected_paths
|
|
1076
|
+
|
|
1077
|
+
orchestrator_cfg["context_paths"].append(context_config)
|
|
1078
|
+
print(f"\n {BRIGHT_GREEN}✅ Added: {cwd} (write) with {len(protected_paths)} protected path(s){RESET}", flush=True)
|
|
1079
|
+
return True
|
|
1080
|
+
elif response in ["n", "no"]:
|
|
1081
|
+
# User explicitly declined
|
|
1082
|
+
return False
|
|
1083
|
+
elif response in ["c", "custom"]:
|
|
1084
|
+
# Loop until valid path or user cancels
|
|
1085
|
+
print()
|
|
1086
|
+
while True:
|
|
1087
|
+
custom_path = input(f" {BRIGHT_CYAN}Enter path (absolute or relative):{RESET} ").strip()
|
|
1088
|
+
if not custom_path:
|
|
1089
|
+
print(f" {BRIGHT_YELLOW}⚠️ Cancelled{RESET}", flush=True)
|
|
1090
|
+
return False
|
|
1091
|
+
|
|
1092
|
+
# Resolve to absolute path
|
|
1093
|
+
abs_path = str(Path(custom_path).resolve())
|
|
1094
|
+
|
|
1095
|
+
# Check if path exists
|
|
1096
|
+
if not Path(abs_path).exists():
|
|
1097
|
+
print(f" {BRIGHT_RED}✗ Path does not exist: {abs_path}{RESET}", flush=True)
|
|
1098
|
+
retry = input(f" {BRIGHT_CYAN}Try again? [Y/n]:{RESET} ").strip().lower()
|
|
1099
|
+
if retry in ["n", "no"]:
|
|
1100
|
+
return False
|
|
1101
|
+
continue
|
|
1102
|
+
|
|
1103
|
+
# Valid path (file or directory), ask for permission
|
|
1104
|
+
permission = input(f" {BRIGHT_CYAN}Permission [read/write] (default: write):{RESET} ").strip().lower() or "write"
|
|
1105
|
+
if permission not in ["read", "write"]:
|
|
1106
|
+
permission = "write"
|
|
1107
|
+
|
|
1108
|
+
# Ask about protected paths if write permission
|
|
1109
|
+
protected_paths = []
|
|
1110
|
+
if permission == "write":
|
|
1111
|
+
add_protected = input(f" {BRIGHT_CYAN}Add protected paths? [y/N]:{RESET} ").strip().lower()
|
|
1112
|
+
if add_protected in ["y", "yes"]:
|
|
1113
|
+
print(f" {BRIGHT_CYAN}Enter protected paths (one per line, empty to finish):{RESET}", flush=True)
|
|
1114
|
+
while True:
|
|
1115
|
+
protected_input = input(f" {BRIGHT_CYAN}→{RESET} ").strip()
|
|
1116
|
+
if not protected_input:
|
|
1117
|
+
break
|
|
1118
|
+
protected_paths.append(protected_input)
|
|
1119
|
+
print(f" {BRIGHT_GREEN}✓ Added: {protected_input}{RESET}", flush=True)
|
|
1120
|
+
|
|
1121
|
+
if "context_paths" not in orchestrator_cfg:
|
|
1122
|
+
orchestrator_cfg["context_paths"] = []
|
|
1123
|
+
|
|
1124
|
+
context_config = {"path": abs_path, "permission": permission}
|
|
1125
|
+
if protected_paths:
|
|
1126
|
+
context_config["protected_paths"] = protected_paths
|
|
1127
|
+
|
|
1128
|
+
orchestrator_cfg["context_paths"].append(context_config)
|
|
1129
|
+
if protected_paths:
|
|
1130
|
+
print(f" {BRIGHT_GREEN}✅ Added: {abs_path} ({permission}) with {len(protected_paths)} protected path(s){RESET}", flush=True)
|
|
1131
|
+
else:
|
|
1132
|
+
print(f" {BRIGHT_GREEN}✅ Added: {abs_path} ({permission}){RESET}", flush=True)
|
|
1133
|
+
return True
|
|
1134
|
+
else:
|
|
1135
|
+
# Invalid response - clarify options
|
|
1136
|
+
print(f"\n {BRIGHT_RED}✗ Invalid option: '{response}'{RESET}", flush=True)
|
|
1137
|
+
print(f" {BRIGHT_YELLOW}Please choose: Y (yes), P (protected), N (no), or C (custom){RESET}", flush=True)
|
|
1138
|
+
return False
|
|
1139
|
+
except (KeyboardInterrupt, EOFError):
|
|
1140
|
+
print() # New line after Ctrl+C
|
|
1141
|
+
return False
|
|
1142
|
+
|
|
1143
|
+
return False
|
|
1144
|
+
|
|
1145
|
+
|
|
1146
|
+
def show_available_examples():
|
|
1147
|
+
"""Display available example configurations from package."""
|
|
1148
|
+
try:
|
|
1149
|
+
from importlib.resources import files
|
|
1150
|
+
|
|
1151
|
+
configs_root = files("massgen") / "configs"
|
|
1152
|
+
|
|
1153
|
+
print(f"\n{BRIGHT_CYAN}Available Example Configurations{RESET}")
|
|
1154
|
+
print("=" * 60)
|
|
1155
|
+
|
|
1156
|
+
# Organize by category
|
|
1157
|
+
categories = {}
|
|
1158
|
+
for config_file in sorted(configs_root.rglob("*.yaml")):
|
|
1159
|
+
# Get relative path from configs root
|
|
1160
|
+
rel_path = str(config_file).replace(str(configs_root) + "/", "")
|
|
1161
|
+
# Extract category (first directory)
|
|
1162
|
+
parts = rel_path.split("/")
|
|
1163
|
+
category = parts[0] if len(parts) > 1 else "root"
|
|
1164
|
+
|
|
1165
|
+
if category not in categories:
|
|
1166
|
+
categories[category] = []
|
|
1167
|
+
|
|
1168
|
+
# Create a short name for @examples/
|
|
1169
|
+
# Use the path without .yaml extension
|
|
1170
|
+
short_name = rel_path.replace(".yaml", "").replace("/", "_")
|
|
1171
|
+
|
|
1172
|
+
categories[category].append((short_name, rel_path))
|
|
1173
|
+
|
|
1174
|
+
# Display categories
|
|
1175
|
+
for category, configs in sorted(categories.items()):
|
|
1176
|
+
print(f"\n{BRIGHT_YELLOW}{category.title()}:{RESET}")
|
|
1177
|
+
for short_name, rel_path in configs[:10]: # Limit to avoid overwhelming
|
|
1178
|
+
print(f" {BRIGHT_GREEN}@examples/{short_name:<40}{RESET} {rel_path}")
|
|
1179
|
+
|
|
1180
|
+
if len(configs) > 10:
|
|
1181
|
+
print(f" ... and {len(configs) - 10} more")
|
|
1182
|
+
|
|
1183
|
+
print(f"\n{BRIGHT_BLUE}Usage:{RESET}")
|
|
1184
|
+
print(' massgen --config @examples/SHORTNAME "Your question"')
|
|
1185
|
+
print(" massgen --example SHORTNAME > my-config.yaml")
|
|
1186
|
+
print()
|
|
1187
|
+
|
|
1188
|
+
except Exception as e:
|
|
1189
|
+
print(f"Error listing examples: {e}")
|
|
1190
|
+
print("Examples may not be available (development mode?)")
|
|
1191
|
+
|
|
1192
|
+
|
|
1193
|
+
def print_example_config(name: str):
|
|
1194
|
+
"""Print an example config to stdout.
|
|
1195
|
+
|
|
1196
|
+
Args:
|
|
1197
|
+
name: Name of the example (can include or exclude @examples/ prefix)
|
|
1198
|
+
"""
|
|
1199
|
+
try:
|
|
1200
|
+
# Remove @examples/ prefix if present
|
|
1201
|
+
if name.startswith("@examples/"):
|
|
1202
|
+
name = name[10:]
|
|
1203
|
+
|
|
1204
|
+
# Try to resolve the config
|
|
1205
|
+
resolved = resolve_config_path(f"@examples/{name}")
|
|
1206
|
+
if resolved:
|
|
1207
|
+
with open(resolved, "r") as f:
|
|
1208
|
+
print(f.read())
|
|
1209
|
+
else:
|
|
1210
|
+
print(f"Error: Could not find example '{name}'", file=sys.stderr)
|
|
1211
|
+
print("Use --list-examples to see available configs", file=sys.stderr)
|
|
1212
|
+
sys.exit(1)
|
|
1213
|
+
|
|
1214
|
+
except ConfigurationError as e:
|
|
1215
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
1216
|
+
sys.exit(1)
|
|
1217
|
+
except Exception as e:
|
|
1218
|
+
print(f"Error printing example config: {e}", file=sys.stderr)
|
|
1219
|
+
sys.exit(1)
|
|
1220
|
+
|
|
1221
|
+
|
|
1222
|
+
def should_run_builder() -> bool:
|
|
1223
|
+
"""Check if config builder should run automatically.
|
|
1224
|
+
|
|
1225
|
+
Returns True if:
|
|
1226
|
+
- No default config exists at ~/.config/massgen/config.yaml
|
|
1227
|
+
"""
|
|
1228
|
+
default_config = Path.home() / ".config/massgen/config.yaml"
|
|
1229
|
+
return not default_config.exists()
|
|
1230
|
+
|
|
1231
|
+
|
|
403
1232
|
def print_help_messages():
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
1233
|
+
"""Display help messages using Rich for better formatting."""
|
|
1234
|
+
rich_console = Console()
|
|
1235
|
+
|
|
1236
|
+
help_content = """[dim]💬 Type your questions below
|
|
1237
|
+
💡 Use slash commands: [cyan]/help[/cyan], [cyan]/quit[/cyan], [cyan]/reset[/cyan], [cyan]/status[/cyan], [cyan]/config[/cyan]
|
|
1238
|
+
⌨️ Press [cyan]Ctrl+C[/cyan] to exit[/dim]"""
|
|
1239
|
+
|
|
1240
|
+
help_panel = Panel(
|
|
1241
|
+
help_content,
|
|
1242
|
+
border_style="dim",
|
|
1243
|
+
padding=(0, 2),
|
|
1244
|
+
width=80,
|
|
407
1245
|
)
|
|
408
|
-
print(
|
|
409
|
-
print("=" * 60, flush=True)
|
|
1246
|
+
rich_console.print(help_panel)
|
|
410
1247
|
|
|
411
1248
|
|
|
412
1249
|
async def run_interactive_mode(
|
|
413
|
-
agents: Dict[str, SingleAgent],
|
|
1250
|
+
agents: Dict[str, SingleAgent],
|
|
1251
|
+
ui_config: Dict[str, Any],
|
|
1252
|
+
original_config: Dict[str, Any] = None,
|
|
1253
|
+
orchestrator_cfg: Dict[str, Any] = None,
|
|
1254
|
+
config_path: Optional[str] = None,
|
|
1255
|
+
**kwargs,
|
|
414
1256
|
):
|
|
415
1257
|
"""Run MassGen in interactive mode with conversation history."""
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
1258
|
+
|
|
1259
|
+
# Use Rich console for better display
|
|
1260
|
+
rich_console = Console()
|
|
1261
|
+
|
|
1262
|
+
# Clear screen
|
|
1263
|
+
rich_console.clear()
|
|
1264
|
+
|
|
1265
|
+
# ASCII art for interactive multi-agent mode
|
|
1266
|
+
ascii_art = """[bold #4A90E2]
|
|
1267
|
+
███╗ ███╗ █████╗ ███████╗███████╗ ██████╗ ███████╗███╗ ██╗
|
|
1268
|
+
████╗ ████║██╔══██╗██╔════╝██╔════╝██╔════╝ ██╔════╝████╗ ██║
|
|
1269
|
+
██╔████╔██║███████║███████╗███████╗██║ ███╗█████╗ ██╔██╗ ██║
|
|
1270
|
+
██║╚██╔╝██║██╔══██║╚════██║╚════██║██║ ██║██╔══╝ ██║╚██╗██║
|
|
1271
|
+
██║ ╚═╝ ██║██║ ██║███████║███████║╚██████╔╝███████╗██║ ╚████║
|
|
1272
|
+
╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝[/bold #4A90E2]
|
|
1273
|
+
|
|
1274
|
+
[dim] 🤖 🤖 🤖 → 💬 collaborate → 🎯 winner → 📢 final[/dim]
|
|
1275
|
+
"""
|
|
1276
|
+
|
|
1277
|
+
# Wrap ASCII art in a panel
|
|
1278
|
+
ascii_panel = Panel(
|
|
1279
|
+
ascii_art,
|
|
1280
|
+
border_style="bold #4A90E2",
|
|
1281
|
+
padding=(0, 2),
|
|
1282
|
+
width=80,
|
|
1283
|
+
)
|
|
1284
|
+
rich_console.print(ascii_panel)
|
|
1285
|
+
print()
|
|
1286
|
+
|
|
1287
|
+
# Create configuration table
|
|
1288
|
+
config_table = Table(
|
|
1289
|
+
show_header=False,
|
|
1290
|
+
box=None,
|
|
1291
|
+
padding=(0, 2),
|
|
1292
|
+
show_edge=False,
|
|
428
1293
|
)
|
|
1294
|
+
config_table.add_column("Label", style="bold cyan", no_wrap=True)
|
|
1295
|
+
config_table.add_column("Value", style="white")
|
|
1296
|
+
|
|
1297
|
+
# Determine mode
|
|
1298
|
+
ui_config.get("use_orchestrator_for_single_agent", True)
|
|
429
1299
|
if len(agents) == 1:
|
|
430
|
-
mode =
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
1300
|
+
mode = "Single Agent"
|
|
1301
|
+
mode_icon = "🤖"
|
|
1302
|
+
else:
|
|
1303
|
+
mode = f"Multi-Agent ({len(agents)} agents)"
|
|
1304
|
+
mode_icon = "🤝"
|
|
1305
|
+
|
|
1306
|
+
config_table.add_row(f"{mode_icon} Mode:", f"[bold]{mode}[/bold]")
|
|
1307
|
+
|
|
1308
|
+
# Add agents info
|
|
1309
|
+
if len(agents) <= 3:
|
|
1310
|
+
# Show all agents if 3 or fewer
|
|
1311
|
+
for agent_id, agent in agents.items():
|
|
1312
|
+
# Get model name from config
|
|
1313
|
+
model = agent.config.backend_params.get("model", "unknown")
|
|
1314
|
+
backend_name = agent.backend.__class__.__name__.replace("Backend", "")
|
|
1315
|
+
# Show model with backend in parentheses
|
|
1316
|
+
display = f"{model} [dim]({backend_name})[/dim]"
|
|
1317
|
+
config_table.add_row(f" ├─ {agent_id}:", display)
|
|
435
1318
|
else:
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
1319
|
+
# Show count and first 2 agents
|
|
1320
|
+
agent_list = list(agents.items())
|
|
1321
|
+
for i, (agent_id, agent) in enumerate(agent_list[:2]):
|
|
1322
|
+
model = agent.config.backend_params.get("model", "unknown")
|
|
1323
|
+
backend_name = agent.backend.__class__.__name__.replace("Backend", "")
|
|
1324
|
+
display = f"{model} [dim]({backend_name})[/dim]"
|
|
1325
|
+
config_table.add_row(f" ├─ {agent_id}:", display)
|
|
1326
|
+
config_table.add_row(" └─ ...", f"[dim]and {len(agents) - 2} more[/dim]")
|
|
1327
|
+
|
|
1328
|
+
# Create main panel with configuration
|
|
1329
|
+
config_panel = Panel(
|
|
1330
|
+
config_table,
|
|
1331
|
+
title="[bold bright_yellow]⚙️ Session Configuration[/bold bright_yellow]",
|
|
1332
|
+
border_style="yellow",
|
|
1333
|
+
padding=(0, 2),
|
|
1334
|
+
width=80,
|
|
1335
|
+
)
|
|
1336
|
+
rich_console.print(config_panel)
|
|
1337
|
+
print()
|
|
1338
|
+
|
|
1339
|
+
# Prompt for context paths if filesystem is enabled
|
|
1340
|
+
if original_config and orchestrator_cfg:
|
|
1341
|
+
config_modified = prompt_for_context_paths(original_config, orchestrator_cfg)
|
|
1342
|
+
if config_modified:
|
|
1343
|
+
# Recreate agents with updated context paths
|
|
1344
|
+
agents = create_agents_from_config(original_config, orchestrator_cfg)
|
|
1345
|
+
print(f" {BRIGHT_GREEN}✓ Agents reloaded with updated context paths{RESET}", flush=True)
|
|
1346
|
+
print()
|
|
439
1347
|
|
|
440
1348
|
print_help_messages()
|
|
441
1349
|
|
|
442
1350
|
# Maintain conversation history
|
|
443
1351
|
conversation_history = []
|
|
444
1352
|
|
|
1353
|
+
# Session management for multi-turn filesystem support
|
|
1354
|
+
session_id = None
|
|
1355
|
+
current_turn = 0
|
|
1356
|
+
session_storage = kwargs.get("orchestrator", {}).get("session_storage", "sessions")
|
|
1357
|
+
|
|
445
1358
|
try:
|
|
446
1359
|
while True:
|
|
447
1360
|
try:
|
|
1361
|
+
# Recreate agents with previous turn as read-only context path.
|
|
1362
|
+
# This provides agents with BOTH:
|
|
1363
|
+
# 1. Read-only context path (original turn n-1 results) - for reference/comparison
|
|
1364
|
+
# 2. Writable workspace (copy of turn n-1 results, pre-populated by orchestrator) - for modification
|
|
1365
|
+
# This allows agents to compare "what I changed" vs "what was originally there".
|
|
1366
|
+
# TODO: We may want to avoid full recreation if possible in the future, conditioned on being able to easily reset MCPs.
|
|
1367
|
+
if current_turn > 0 and original_config and orchestrator_cfg:
|
|
1368
|
+
# Get the most recent turn path (the one just completed)
|
|
1369
|
+
session_dir = Path(session_storage) / session_id
|
|
1370
|
+
latest_turn_dir = session_dir / f"turn_{current_turn}"
|
|
1371
|
+
latest_turn_workspace = latest_turn_dir / "workspace"
|
|
1372
|
+
|
|
1373
|
+
if latest_turn_workspace.exists():
|
|
1374
|
+
logger.info(f"[CLI] Recreating agents with turn {current_turn} workspace as read-only context path")
|
|
1375
|
+
|
|
1376
|
+
# Clean up existing agents' backends and filesystem managers
|
|
1377
|
+
for agent_id, agent in agents.items():
|
|
1378
|
+
# Cleanup filesystem manager (Docker containers, etc.)
|
|
1379
|
+
if hasattr(agent, "backend") and hasattr(agent.backend, "filesystem_manager"):
|
|
1380
|
+
if agent.backend.filesystem_manager:
|
|
1381
|
+
try:
|
|
1382
|
+
agent.backend.filesystem_manager.cleanup()
|
|
1383
|
+
except Exception as e:
|
|
1384
|
+
logger.warning(f"[CLI] Cleanup failed for agent {agent_id}: {e}")
|
|
1385
|
+
|
|
1386
|
+
# Cleanup backend itself
|
|
1387
|
+
if hasattr(agent.backend, "__aexit__"):
|
|
1388
|
+
await agent.backend.__aexit__(None, None, None)
|
|
1389
|
+
|
|
1390
|
+
# Inject previous turn path as read-only context
|
|
1391
|
+
modified_config = original_config.copy()
|
|
1392
|
+
agent_entries = [modified_config["agent"]] if "agent" in modified_config else modified_config.get("agents", [])
|
|
1393
|
+
|
|
1394
|
+
for agent_data in agent_entries:
|
|
1395
|
+
backend_config = agent_data.get("backend", {})
|
|
1396
|
+
if "cwd" in backend_config: # Only inject if agent has filesystem support
|
|
1397
|
+
existing_context_paths = backend_config.get("context_paths", [])
|
|
1398
|
+
new_turn_config = {"path": str(latest_turn_workspace.resolve()), "permission": "read"}
|
|
1399
|
+
backend_config["context_paths"] = existing_context_paths + [new_turn_config]
|
|
1400
|
+
|
|
1401
|
+
# Recreate agents from modified config
|
|
1402
|
+
agents = create_agents_from_config(modified_config, orchestrator_cfg)
|
|
1403
|
+
logger.info(f"[CLI] Successfully recreated {len(agents)} agents with turn {current_turn} path as read-only context")
|
|
1404
|
+
|
|
448
1405
|
question = input(f"\n{BRIGHT_BLUE}👤 User:{RESET} ").strip()
|
|
449
1406
|
|
|
450
1407
|
# Handle slash commands
|
|
@@ -465,9 +1422,7 @@ async def run_interactive_mode(
|
|
|
465
1422
|
)
|
|
466
1423
|
continue
|
|
467
1424
|
elif command in ["/help", "/h"]:
|
|
468
|
-
print(
|
|
469
|
-
f"\n{BRIGHT_CYAN}📚 Available Commands:{RESET}", flush=True
|
|
470
|
-
)
|
|
1425
|
+
print(f"\n{BRIGHT_CYAN}📚 Available Commands:{RESET}", flush=True)
|
|
471
1426
|
print(" /quit, /exit, /q - Exit the program", flush=True)
|
|
472
1427
|
print(
|
|
473
1428
|
" /reset, /clear - Clear conversation history",
|
|
@@ -477,9 +1432,8 @@ async def run_interactive_mode(
|
|
|
477
1432
|
" /help, /h - Show this help message",
|
|
478
1433
|
flush=True,
|
|
479
1434
|
)
|
|
480
|
-
print(
|
|
481
|
-
|
|
482
|
-
)
|
|
1435
|
+
print(" /status - Show current status", flush=True)
|
|
1436
|
+
print(" /config - Open config file in editor", flush=True)
|
|
483
1437
|
continue
|
|
484
1438
|
elif command == "/status":
|
|
485
1439
|
print(f"\n{BRIGHT_CYAN}📊 Current Status:{RESET}", flush=True)
|
|
@@ -487,15 +1441,9 @@ async def run_interactive_mode(
|
|
|
487
1441
|
f" Agents: {len(agents)} ({', '.join(agents.keys())})",
|
|
488
1442
|
flush=True,
|
|
489
1443
|
)
|
|
490
|
-
use_orch_single = ui_config.get(
|
|
491
|
-
"use_orchestrator_for_single_agent", True
|
|
492
|
-
)
|
|
1444
|
+
use_orch_single = ui_config.get("use_orchestrator_for_single_agent", True)
|
|
493
1445
|
if len(agents) == 1:
|
|
494
|
-
mode_display = (
|
|
495
|
-
"Single Agent (Orchestrator)"
|
|
496
|
-
if use_orch_single
|
|
497
|
-
else "Single Agent (Direct)"
|
|
498
|
-
)
|
|
1446
|
+
mode_display = "Single Agent (Orchestrator)" if use_orch_single else "Single Agent (Direct)"
|
|
499
1447
|
else:
|
|
500
1448
|
mode_display = "Multi-Agent"
|
|
501
1449
|
print(f" Mode: {mode_display}", flush=True)
|
|
@@ -503,6 +1451,28 @@ async def run_interactive_mode(
|
|
|
503
1451
|
f" History: {len(conversation_history)//2} exchanges",
|
|
504
1452
|
flush=True,
|
|
505
1453
|
)
|
|
1454
|
+
if config_path:
|
|
1455
|
+
print(f" Config: {config_path}", flush=True)
|
|
1456
|
+
continue
|
|
1457
|
+
elif command == "/config":
|
|
1458
|
+
if config_path:
|
|
1459
|
+
import platform
|
|
1460
|
+
import subprocess
|
|
1461
|
+
|
|
1462
|
+
try:
|
|
1463
|
+
system = platform.system()
|
|
1464
|
+
if system == "Darwin": # macOS
|
|
1465
|
+
subprocess.run(["open", config_path])
|
|
1466
|
+
elif system == "Windows":
|
|
1467
|
+
subprocess.run(["start", config_path], shell=True)
|
|
1468
|
+
else: # Linux and others
|
|
1469
|
+
subprocess.run(["xdg-open", config_path])
|
|
1470
|
+
print(f"\n📝 Opening config file: {config_path}", flush=True)
|
|
1471
|
+
except Exception as e:
|
|
1472
|
+
print(f"\n❌ Error opening config file: {e}", flush=True)
|
|
1473
|
+
print(f" Config location: {config_path}", flush=True)
|
|
1474
|
+
else:
|
|
1475
|
+
print("\n❌ No config file available (using CLI arguments)", flush=True)
|
|
506
1476
|
continue
|
|
507
1477
|
else:
|
|
508
1478
|
print(f"❓ Unknown command: {command}", flush=True)
|
|
@@ -530,16 +1500,40 @@ async def run_interactive_mode(
|
|
|
530
1500
|
|
|
531
1501
|
print(f"\n🔄 {BRIGHT_YELLOW}Processing...{RESET}", flush=True)
|
|
532
1502
|
|
|
533
|
-
|
|
534
|
-
|
|
1503
|
+
# Increment turn counter BEFORE processing so logs go to correct turn_N directory
|
|
1504
|
+
next_turn = current_turn + 1
|
|
1505
|
+
|
|
1506
|
+
# Initialize session ID on first turn
|
|
1507
|
+
if session_id is None:
|
|
1508
|
+
session_id = f"session_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
|
1509
|
+
|
|
1510
|
+
# Reconfigure logging for the turn we're about to process
|
|
1511
|
+
setup_logging(debug=_DEBUG_MODE, turn=next_turn)
|
|
1512
|
+
logger.info(f"Starting turn {next_turn}")
|
|
1513
|
+
|
|
1514
|
+
# Pass session state for multi-turn filesystem support
|
|
1515
|
+
session_info = {
|
|
1516
|
+
"session_id": session_id,
|
|
1517
|
+
"current_turn": current_turn, # Pass CURRENT turn (for looking up previous turns)
|
|
1518
|
+
"session_storage": session_storage,
|
|
1519
|
+
}
|
|
1520
|
+
response, updated_session_id, updated_turn = await run_question_with_history(
|
|
1521
|
+
question,
|
|
1522
|
+
agents,
|
|
1523
|
+
ui_config,
|
|
1524
|
+
conversation_history,
|
|
1525
|
+
session_info,
|
|
1526
|
+
**kwargs,
|
|
535
1527
|
)
|
|
536
1528
|
|
|
1529
|
+
# Update session state after completion
|
|
1530
|
+
session_id = updated_session_id
|
|
1531
|
+
current_turn = updated_turn
|
|
1532
|
+
|
|
537
1533
|
if response:
|
|
538
1534
|
# Add to conversation history
|
|
539
1535
|
conversation_history.append({"role": "user", "content": question})
|
|
540
|
-
conversation_history.append(
|
|
541
|
-
{"role": "assistant", "content": response}
|
|
542
|
-
)
|
|
1536
|
+
conversation_history.append({"role": "assistant", "content": response})
|
|
543
1537
|
print(f"\n{BRIGHT_GREEN}✅ Complete!{RESET}", flush=True)
|
|
544
1538
|
print(
|
|
545
1539
|
f"{BRIGHT_CYAN}💭 History: {len(conversation_history)//2} exchanges{RESET}",
|
|
@@ -560,8 +1554,180 @@ async def run_interactive_mode(
|
|
|
560
1554
|
except KeyboardInterrupt:
|
|
561
1555
|
print("\n👋 Goodbye!")
|
|
562
1556
|
|
|
563
|
-
|
|
564
|
-
|
|
1557
|
+
|
|
1558
|
+
async def main(args):
|
|
1559
|
+
"""Main CLI entry point (async operations only)."""
|
|
1560
|
+
# Check if bare `massgen` with no args - use default config if it exists
|
|
1561
|
+
if not args.backend and not args.model and not args.config:
|
|
1562
|
+
# Use resolve_config_path to check project-level then global config
|
|
1563
|
+
resolved_default = resolve_config_path(None)
|
|
1564
|
+
if resolved_default:
|
|
1565
|
+
# Use discovered config for interactive mode (no question) or single query (with question)
|
|
1566
|
+
args.config = str(resolved_default)
|
|
1567
|
+
else:
|
|
1568
|
+
# No default config - this will be handled by wizard trigger in cli_main()
|
|
1569
|
+
if args.question:
|
|
1570
|
+
# User provided a question but no config exists - this is an error
|
|
1571
|
+
print("❌ Configuration error: No default configuration found.", flush=True)
|
|
1572
|
+
print("Run 'massgen --init' to create one, or use 'massgen --model MODEL \"question\"'", flush=True)
|
|
1573
|
+
sys.exit(1)
|
|
1574
|
+
# No question and no config - wizard will be triggered in cli_main()
|
|
1575
|
+
return
|
|
1576
|
+
|
|
1577
|
+
# Validate arguments (only if we didn't auto-set config above)
|
|
1578
|
+
if not args.backend:
|
|
1579
|
+
if not args.model and not args.config:
|
|
1580
|
+
print("❌ Configuration error: Either --config, --model, or --backend must be specified", flush=True)
|
|
1581
|
+
sys.exit(1)
|
|
1582
|
+
|
|
1583
|
+
try:
|
|
1584
|
+
# Load or create configuration
|
|
1585
|
+
if args.config:
|
|
1586
|
+
# Resolve config path (handles @examples/, paths, ~/.config/massgen/agents/)
|
|
1587
|
+
resolved_path = resolve_config_path(args.config)
|
|
1588
|
+
if resolved_path is None:
|
|
1589
|
+
# This shouldn't happen if we reached here, but handle it
|
|
1590
|
+
raise ConfigurationError("Could not resolve config path")
|
|
1591
|
+
config = load_config_file(str(resolved_path))
|
|
1592
|
+
if args.debug:
|
|
1593
|
+
logger.debug(f"Resolved config path: {resolved_path}")
|
|
1594
|
+
logger.debug(f"Config content: {json.dumps(config, indent=2)}")
|
|
1595
|
+
else:
|
|
1596
|
+
model = args.model
|
|
1597
|
+
if args.backend:
|
|
1598
|
+
backend = args.backend
|
|
1599
|
+
else:
|
|
1600
|
+
backend = get_backend_type_from_model(model=model)
|
|
1601
|
+
if args.system_message:
|
|
1602
|
+
system_message = args.system_message
|
|
1603
|
+
else:
|
|
1604
|
+
system_message = None
|
|
1605
|
+
config = create_simple_config(
|
|
1606
|
+
backend_type=backend,
|
|
1607
|
+
model=model,
|
|
1608
|
+
system_message=system_message,
|
|
1609
|
+
base_url=args.base_url,
|
|
1610
|
+
)
|
|
1611
|
+
if args.debug:
|
|
1612
|
+
logger.debug(f"Created simple config with backend: {backend}, model: {model}")
|
|
1613
|
+
logger.debug(f"Config content: {json.dumps(config, indent=2)}")
|
|
1614
|
+
|
|
1615
|
+
# Validate that all context paths exist before proceeding
|
|
1616
|
+
validate_context_paths(config)
|
|
1617
|
+
|
|
1618
|
+
# Relocate all filesystem paths to .massgen/ directory
|
|
1619
|
+
relocate_filesystem_paths(config)
|
|
1620
|
+
|
|
1621
|
+
# Apply command-line overrides
|
|
1622
|
+
ui_config = config.get("ui", {})
|
|
1623
|
+
if args.no_display:
|
|
1624
|
+
ui_config["display_type"] = "simple"
|
|
1625
|
+
if args.no_logs:
|
|
1626
|
+
ui_config["logging_enabled"] = False
|
|
1627
|
+
if args.debug:
|
|
1628
|
+
ui_config["debug"] = True
|
|
1629
|
+
# Enable logging if debug is on
|
|
1630
|
+
ui_config["logging_enabled"] = True
|
|
1631
|
+
# # Force simple UI in debug mode
|
|
1632
|
+
# ui_config["display_type"] = "simple"
|
|
1633
|
+
|
|
1634
|
+
# Apply timeout overrides from CLI arguments
|
|
1635
|
+
timeout_settings = config.get("timeout_settings", {})
|
|
1636
|
+
if args.orchestrator_timeout is not None:
|
|
1637
|
+
timeout_settings["orchestrator_timeout_seconds"] = args.orchestrator_timeout
|
|
1638
|
+
|
|
1639
|
+
# Update config with timeout settings
|
|
1640
|
+
config["timeout_settings"] = timeout_settings
|
|
1641
|
+
|
|
1642
|
+
# Create agents
|
|
1643
|
+
if args.debug:
|
|
1644
|
+
logger.debug("Creating agents from config...")
|
|
1645
|
+
# Extract orchestrator config for agent setup
|
|
1646
|
+
orchestrator_cfg = config.get("orchestrator", {})
|
|
1647
|
+
|
|
1648
|
+
# Check if any agent has cwd (filesystem support) and validate orchestrator config
|
|
1649
|
+
agent_entries = [config["agent"]] if "agent" in config else config.get("agents", [])
|
|
1650
|
+
has_cwd = any("cwd" in agent.get("backend", {}) for agent in agent_entries)
|
|
1651
|
+
|
|
1652
|
+
if has_cwd:
|
|
1653
|
+
if not orchestrator_cfg:
|
|
1654
|
+
raise ConfigurationError(
|
|
1655
|
+
"Agents with 'cwd' (filesystem support) require orchestrator configuration.\n"
|
|
1656
|
+
"Please add an 'orchestrator' section to your config file.\n\n"
|
|
1657
|
+
"Example (customize paths as needed):\n"
|
|
1658
|
+
"orchestrator:\n"
|
|
1659
|
+
' snapshot_storage: "your_snapshot_dir"\n'
|
|
1660
|
+
' agent_temporary_workspace: "your_temp_dir"',
|
|
1661
|
+
)
|
|
1662
|
+
|
|
1663
|
+
# Check for required fields in orchestrator config
|
|
1664
|
+
if "snapshot_storage" not in orchestrator_cfg:
|
|
1665
|
+
raise ConfigurationError(
|
|
1666
|
+
"Missing 'snapshot_storage' in orchestrator configuration.\n"
|
|
1667
|
+
"This is required for agents with filesystem support (cwd).\n\n"
|
|
1668
|
+
"Add to your orchestrator section:\n"
|
|
1669
|
+
' snapshot_storage: "your_snapshot_dir" # Directory for workspace snapshots',
|
|
1670
|
+
)
|
|
1671
|
+
|
|
1672
|
+
if "agent_temporary_workspace" not in orchestrator_cfg:
|
|
1673
|
+
raise ConfigurationError(
|
|
1674
|
+
"Missing 'agent_temporary_workspace' in orchestrator configuration.\n"
|
|
1675
|
+
"This is required for agents with filesystem support (cwd).\n\n"
|
|
1676
|
+
"Add to your orchestrator section:\n"
|
|
1677
|
+
' agent_temporary_workspace: "your_temp_dir" # Directory for temporary agent workspaces',
|
|
1678
|
+
)
|
|
1679
|
+
|
|
1680
|
+
agents = create_agents_from_config(config, orchestrator_cfg)
|
|
1681
|
+
|
|
1682
|
+
if not agents:
|
|
1683
|
+
raise ConfigurationError("No agents configured")
|
|
1684
|
+
|
|
1685
|
+
if args.debug:
|
|
1686
|
+
logger.debug(f"Created {len(agents)} agent(s): {list(agents.keys())}")
|
|
1687
|
+
|
|
1688
|
+
# Create timeout config from settings and put it in kwargs
|
|
1689
|
+
timeout_settings = config.get("timeout_settings", {})
|
|
1690
|
+
timeout_config = TimeoutConfig(**timeout_settings) if timeout_settings else TimeoutConfig()
|
|
1691
|
+
|
|
1692
|
+
kwargs = {"timeout_config": timeout_config}
|
|
1693
|
+
|
|
1694
|
+
# Add orchestrator configuration if present
|
|
1695
|
+
if "orchestrator" in config:
|
|
1696
|
+
kwargs["orchestrator"] = config["orchestrator"]
|
|
1697
|
+
|
|
1698
|
+
# Run mode based on whether question was provided
|
|
1699
|
+
try:
|
|
1700
|
+
if args.question:
|
|
1701
|
+
await run_single_question(args.question, agents, ui_config, **kwargs)
|
|
1702
|
+
# if response:
|
|
1703
|
+
# print(f"\n{BRIGHT_GREEN}Final Response:{RESET}", flush=True)
|
|
1704
|
+
# print(f"{response}", flush=True)
|
|
1705
|
+
else:
|
|
1706
|
+
# Pass the config path to interactive mode
|
|
1707
|
+
config_file_path = str(resolved_path) if args.config and resolved_path else None
|
|
1708
|
+
await run_interactive_mode(agents, ui_config, original_config=config, orchestrator_cfg=orchestrator_cfg, config_path=config_file_path, **kwargs)
|
|
1709
|
+
finally:
|
|
1710
|
+
# Cleanup all agents' filesystem managers (including Docker containers)
|
|
1711
|
+
for agent_id, agent in agents.items():
|
|
1712
|
+
if hasattr(agent, "backend") and hasattr(agent.backend, "filesystem_manager"):
|
|
1713
|
+
if agent.backend.filesystem_manager:
|
|
1714
|
+
try:
|
|
1715
|
+
agent.backend.filesystem_manager.cleanup()
|
|
1716
|
+
except Exception as e:
|
|
1717
|
+
logger.warning(f"[CLI] Cleanup failed for agent {agent_id}: {e}")
|
|
1718
|
+
|
|
1719
|
+
except ConfigurationError as e:
|
|
1720
|
+
print(f"❌ Configuration error: {e}", flush=True)
|
|
1721
|
+
sys.exit(1)
|
|
1722
|
+
except KeyboardInterrupt:
|
|
1723
|
+
print("\n👋 Goodbye!", flush=True)
|
|
1724
|
+
except Exception as e:
|
|
1725
|
+
print(f"❌ Error: {e}", flush=True)
|
|
1726
|
+
sys.exit(1)
|
|
1727
|
+
|
|
1728
|
+
|
|
1729
|
+
def cli_main():
|
|
1730
|
+
"""Synchronous wrapper for CLI entry point."""
|
|
565
1731
|
parser = argparse.ArgumentParser(
|
|
566
1732
|
description="MassGen - Multi-Agent Coordination CLI",
|
|
567
1733
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
@@ -569,21 +1735,40 @@ async def main():
|
|
|
569
1735
|
Examples:
|
|
570
1736
|
# Use configuration file
|
|
571
1737
|
python -m massgen.cli --config config.yaml "What is machine learning?"
|
|
572
|
-
|
|
1738
|
+
|
|
573
1739
|
# Quick single agent setup
|
|
574
1740
|
python -m massgen.cli --backend openai --model gpt-4o-mini "Explain quantum computing"
|
|
575
1741
|
python -m massgen.cli --backend claude --model claude-sonnet-4-20250514 "Analyze this data"
|
|
576
|
-
|
|
1742
|
+
|
|
1743
|
+
# Use ChatCompletion backend with custom base URL
|
|
1744
|
+
python -m massgen.cli --backend chatcompletion --model gpt-oss-120b --base-url https://api.cerebras.ai/v1/chat/completions "What is 2+2?"
|
|
1745
|
+
|
|
577
1746
|
# Interactive mode
|
|
578
1747
|
python -m massgen.cli --config config.yaml
|
|
579
|
-
|
|
1748
|
+
|
|
1749
|
+
# Timeout control examples
|
|
1750
|
+
python -m massgen.cli --config config.yaml --orchestrator-timeout 600 "Complex task"
|
|
1751
|
+
|
|
580
1752
|
# Create sample configurations
|
|
581
1753
|
python -m massgen.cli --create-samples
|
|
582
1754
|
|
|
583
1755
|
Environment Variables:
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
1756
|
+
OPENAI_API_KEY - Required for OpenAI backend
|
|
1757
|
+
XAI_API_KEY - Required for Grok backend
|
|
1758
|
+
ANTHROPIC_API_KEY - Required for Claude backend
|
|
1759
|
+
GOOGLE_API_KEY - Required for Gemini backend (or GEMINI_API_KEY)
|
|
1760
|
+
ZAI_API_KEY - Required for ZAI backend
|
|
1761
|
+
|
|
1762
|
+
CEREBRAS_API_KEY - For Cerebras AI (cerebras.ai)
|
|
1763
|
+
TOGETHER_API_KEY - For Together AI (together.ai, together.xyz)
|
|
1764
|
+
FIREWORKS_API_KEY - For Fireworks AI (fireworks.ai)
|
|
1765
|
+
GROQ_API_KEY - For Groq (groq.com)
|
|
1766
|
+
NEBIUS_API_KEY - For Nebius AI Studio (studio.nebius.ai)
|
|
1767
|
+
OPENROUTER_API_KEY - For OpenRouter (openrouter.ai)
|
|
1768
|
+
POE_API_KEY - For POE (poe.com)
|
|
1769
|
+
|
|
1770
|
+
Note: The chatcompletion backend auto-detects the provider from the base_url
|
|
1771
|
+
and uses the appropriate environment variable for API key.
|
|
587
1772
|
""",
|
|
588
1773
|
)
|
|
589
1774
|
|
|
@@ -596,13 +1781,23 @@ Environment Variables:
|
|
|
596
1781
|
|
|
597
1782
|
# Configuration options
|
|
598
1783
|
config_group = parser.add_mutually_exclusive_group()
|
|
599
|
-
config_group.add_argument(
|
|
600
|
-
"--config", type=str, help="Path to YAML/JSON configuration file"
|
|
601
|
-
)
|
|
1784
|
+
config_group.add_argument("--config", type=str, help="Path to YAML/JSON configuration file or @examples/NAME")
|
|
602
1785
|
config_group.add_argument(
|
|
603
1786
|
"--backend",
|
|
604
1787
|
type=str,
|
|
605
|
-
choices=[
|
|
1788
|
+
choices=[
|
|
1789
|
+
"chatcompletion",
|
|
1790
|
+
"claude",
|
|
1791
|
+
"gemini",
|
|
1792
|
+
"grok",
|
|
1793
|
+
"openai",
|
|
1794
|
+
"azure_openai",
|
|
1795
|
+
"claude_code",
|
|
1796
|
+
"zai",
|
|
1797
|
+
"lmstudio",
|
|
1798
|
+
"vllm",
|
|
1799
|
+
"sglang",
|
|
1800
|
+
],
|
|
606
1801
|
help="Backend type for quick setup",
|
|
607
1802
|
)
|
|
608
1803
|
|
|
@@ -610,77 +1805,167 @@ Environment Variables:
|
|
|
610
1805
|
parser.add_argument(
|
|
611
1806
|
"--model",
|
|
612
1807
|
type=str,
|
|
613
|
-
default=
|
|
614
|
-
help="Model name for quick setup
|
|
1808
|
+
default=None,
|
|
1809
|
+
help="Model name for quick setup",
|
|
615
1810
|
)
|
|
1811
|
+
parser.add_argument("--system-message", type=str, help="System message for quick setup")
|
|
616
1812
|
parser.add_argument(
|
|
617
|
-
"--
|
|
1813
|
+
"--base-url",
|
|
1814
|
+
type=str,
|
|
1815
|
+
help="Base URL for API endpoint (e.g., https://api.cerebras.ai/v1/chat/completions)",
|
|
618
1816
|
)
|
|
619
1817
|
|
|
620
1818
|
# UI options
|
|
1819
|
+
parser.add_argument("--no-display", action="store_true", help="Disable visual coordination display")
|
|
1820
|
+
parser.add_argument("--no-logs", action="store_true", help="Disable logging")
|
|
1821
|
+
parser.add_argument("--debug", action="store_true", help="Enable debug mode with verbose logging")
|
|
621
1822
|
parser.add_argument(
|
|
622
|
-
"--
|
|
1823
|
+
"--init",
|
|
1824
|
+
action="store_true",
|
|
1825
|
+
help="Launch interactive configuration builder to create config file",
|
|
1826
|
+
)
|
|
1827
|
+
parser.add_argument(
|
|
1828
|
+
"--setup-keys",
|
|
1829
|
+
action="store_true",
|
|
1830
|
+
help="Launch interactive API key setup wizard to configure credentials",
|
|
1831
|
+
)
|
|
1832
|
+
parser.add_argument(
|
|
1833
|
+
"--list-examples",
|
|
1834
|
+
action="store_true",
|
|
1835
|
+
help="List available example configurations from package",
|
|
1836
|
+
)
|
|
1837
|
+
parser.add_argument(
|
|
1838
|
+
"--example",
|
|
1839
|
+
type=str,
|
|
1840
|
+
help="Print example config to stdout (e.g., --example basic_multi)",
|
|
1841
|
+
)
|
|
1842
|
+
parser.add_argument(
|
|
1843
|
+
"--show-schema",
|
|
1844
|
+
action="store_true",
|
|
1845
|
+
help="Display configuration schema and available parameters",
|
|
1846
|
+
)
|
|
1847
|
+
parser.add_argument(
|
|
1848
|
+
"--schema-backend",
|
|
1849
|
+
type=str,
|
|
1850
|
+
help="Show schema for specific backend (use with --show-schema)",
|
|
1851
|
+
)
|
|
1852
|
+
parser.add_argument(
|
|
1853
|
+
"--with-examples",
|
|
1854
|
+
action="store_true",
|
|
1855
|
+
help="Include example configurations in schema display",
|
|
1856
|
+
)
|
|
1857
|
+
|
|
1858
|
+
# Timeout options
|
|
1859
|
+
timeout_group = parser.add_argument_group("timeout settings", "Override timeout settings from config")
|
|
1860
|
+
timeout_group.add_argument(
|
|
1861
|
+
"--orchestrator-timeout",
|
|
1862
|
+
type=int,
|
|
1863
|
+
help="Maximum time for orchestrator coordination in seconds (default: 1800)",
|
|
623
1864
|
)
|
|
624
|
-
parser.add_argument("--no-logs", action="store_true", help="Disable logging")
|
|
625
1865
|
|
|
626
1866
|
args = parser.parse_args()
|
|
627
1867
|
|
|
628
|
-
#
|
|
629
|
-
|
|
630
|
-
if not args.model and not args.config:
|
|
631
|
-
parser.error(
|
|
632
|
-
"If there is not --backend, either --config or --model must be specified"
|
|
633
|
-
)
|
|
1868
|
+
# Always setup logging (will save INFO to file, console output depends on debug flag)
|
|
1869
|
+
setup_logging(debug=args.debug)
|
|
634
1870
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
config = load_config_file(args.config)
|
|
639
|
-
else:
|
|
640
|
-
model = args.model
|
|
641
|
-
if args.backend:
|
|
642
|
-
backend = args.backend
|
|
643
|
-
else:
|
|
644
|
-
backend = get_backend_type_from_model(model=model)
|
|
645
|
-
if args.system_message:
|
|
646
|
-
system_message = args.system_message
|
|
647
|
-
else:
|
|
648
|
-
system_message = None
|
|
649
|
-
config = create_simple_config(
|
|
650
|
-
backend_type=backend, model=model, system_message=system_message
|
|
651
|
-
)
|
|
1871
|
+
if args.debug:
|
|
1872
|
+
logger.info("Debug mode enabled")
|
|
1873
|
+
logger.debug(f"Command line arguments: {vars(args)}")
|
|
652
1874
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
if args.no_logs:
|
|
658
|
-
ui_config["logging_enabled"] = False
|
|
1875
|
+
# Handle special commands first
|
|
1876
|
+
if args.list_examples:
|
|
1877
|
+
show_available_examples()
|
|
1878
|
+
return
|
|
659
1879
|
|
|
660
|
-
|
|
661
|
-
|
|
1880
|
+
if args.example:
|
|
1881
|
+
print_example_config(args.example)
|
|
1882
|
+
return
|
|
662
1883
|
|
|
663
|
-
|
|
664
|
-
|
|
1884
|
+
if args.show_schema:
|
|
1885
|
+
from .schema_display import show_schema
|
|
665
1886
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
1887
|
+
show_schema(backend=args.schema_backend, show_examples=args.with_examples)
|
|
1888
|
+
return
|
|
1889
|
+
|
|
1890
|
+
# Launch interactive API key setup if requested
|
|
1891
|
+
if args.setup_keys:
|
|
1892
|
+
from .config_builder import ConfigBuilder
|
|
1893
|
+
|
|
1894
|
+
builder = ConfigBuilder()
|
|
1895
|
+
api_keys = builder.interactive_api_key_setup()
|
|
1896
|
+
|
|
1897
|
+
if any(api_keys.values()):
|
|
1898
|
+
print(f"\n{BRIGHT_GREEN}✅ API key setup complete!{RESET}")
|
|
1899
|
+
print(f"{BRIGHT_CYAN}💡 You can now use MassGen with these providers{RESET}\n")
|
|
672
1900
|
else:
|
|
673
|
-
|
|
1901
|
+
print(f"\n{BRIGHT_YELLOW}⚠️ No API keys configured{RESET}")
|
|
1902
|
+
print(f"{BRIGHT_CYAN}💡 You can run 'massgen --setup-keys' anytime to set them up{RESET}\n")
|
|
1903
|
+
return
|
|
1904
|
+
|
|
1905
|
+
# Launch interactive config builder if requested
|
|
1906
|
+
if args.init:
|
|
1907
|
+
from .config_builder import ConfigBuilder
|
|
1908
|
+
|
|
1909
|
+
builder = ConfigBuilder()
|
|
1910
|
+
result = builder.run()
|
|
1911
|
+
|
|
1912
|
+
if result and len(result) == 2:
|
|
1913
|
+
filepath, question = result
|
|
1914
|
+
if filepath and question:
|
|
1915
|
+
# Update args to use the newly created config
|
|
1916
|
+
args.config = filepath
|
|
1917
|
+
args.question = question
|
|
1918
|
+
elif filepath:
|
|
1919
|
+
# Config created but user chose not to run
|
|
1920
|
+
print(f"\n✅ Configuration saved to: {filepath}")
|
|
1921
|
+
print(f'Run with: python -m massgen.cli --config {filepath} "Your question"')
|
|
1922
|
+
return
|
|
1923
|
+
else:
|
|
1924
|
+
# User cancelled
|
|
1925
|
+
return
|
|
1926
|
+
else:
|
|
1927
|
+
# Builder returned None (cancelled or error)
|
|
1928
|
+
return
|
|
1929
|
+
|
|
1930
|
+
# First-run detection: auto-trigger builder if no config specified and first run
|
|
1931
|
+
if not args.question and not args.config and not args.model and not args.backend:
|
|
1932
|
+
if should_run_builder():
|
|
1933
|
+
print()
|
|
1934
|
+
print()
|
|
1935
|
+
print(f"{BRIGHT_CYAN}{'=' * 60}{RESET}")
|
|
1936
|
+
print(f"{BRIGHT_CYAN} 👋 Welcome to MassGen!{RESET}")
|
|
1937
|
+
print(f"{BRIGHT_CYAN}{'=' * 60}{RESET}")
|
|
1938
|
+
print()
|
|
1939
|
+
print(" Let's set up your default configuration...")
|
|
1940
|
+
print()
|
|
1941
|
+
|
|
1942
|
+
from .config_builder import ConfigBuilder
|
|
1943
|
+
|
|
1944
|
+
builder = ConfigBuilder(default_mode=True)
|
|
1945
|
+
result = builder.run()
|
|
1946
|
+
|
|
1947
|
+
if result and len(result) == 2:
|
|
1948
|
+
filepath, question = result
|
|
1949
|
+
if filepath:
|
|
1950
|
+
args.config = filepath
|
|
1951
|
+
if question:
|
|
1952
|
+
args.question = question
|
|
1953
|
+
else:
|
|
1954
|
+
print("\n✅ Configuration saved! You can now run queries.")
|
|
1955
|
+
print('Example: massgen "Your question here"')
|
|
1956
|
+
return
|
|
1957
|
+
else:
|
|
1958
|
+
return
|
|
1959
|
+
else:
|
|
1960
|
+
return
|
|
674
1961
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
1962
|
+
# Now call the async main with the parsed arguments
|
|
1963
|
+
try:
|
|
1964
|
+
asyncio.run(main(args))
|
|
678
1965
|
except KeyboardInterrupt:
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
print(f"❌ Error: {e}", flush=True)
|
|
682
|
-
sys.exit(1)
|
|
1966
|
+
# User pressed Ctrl+C - exit gracefully without traceback
|
|
1967
|
+
pass
|
|
683
1968
|
|
|
684
1969
|
|
|
685
1970
|
if __name__ == "__main__":
|
|
686
|
-
|
|
1971
|
+
cli_main()
|