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