massgen 0.1.0a3__py3-none-any.whl → 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (111) hide show
  1. massgen/__init__.py +1 -1
  2. massgen/agent_config.py +17 -0
  3. massgen/api_params_handler/_api_params_handler_base.py +1 -0
  4. massgen/api_params_handler/_chat_completions_api_params_handler.py +8 -1
  5. massgen/api_params_handler/_claude_api_params_handler.py +8 -1
  6. massgen/api_params_handler/_gemini_api_params_handler.py +73 -0
  7. massgen/api_params_handler/_response_api_params_handler.py +8 -1
  8. massgen/backend/base.py +31 -0
  9. massgen/backend/{base_with_mcp.py → base_with_custom_tool_and_mcp.py} +282 -11
  10. massgen/backend/chat_completions.py +182 -92
  11. massgen/backend/claude.py +115 -18
  12. massgen/backend/claude_code.py +378 -14
  13. massgen/backend/docs/CLAUDE_API_RESEARCH.md +3 -3
  14. massgen/backend/gemini.py +1275 -1607
  15. massgen/backend/gemini_mcp_manager.py +545 -0
  16. massgen/backend/gemini_trackers.py +344 -0
  17. massgen/backend/gemini_utils.py +43 -0
  18. massgen/backend/response.py +129 -70
  19. massgen/cli.py +577 -110
  20. massgen/config_builder.py +376 -27
  21. massgen/configs/README.md +111 -80
  22. massgen/configs/basic/multi/three_agents_default.yaml +1 -1
  23. massgen/configs/basic/single/single_agent.yaml +1 -1
  24. massgen/configs/providers/openai/gpt5_nano.yaml +3 -3
  25. massgen/configs/tools/custom_tools/claude_code_custom_tool_example.yaml +32 -0
  26. massgen/configs/tools/custom_tools/claude_code_custom_tool_example_no_path.yaml +28 -0
  27. massgen/configs/tools/custom_tools/claude_code_custom_tool_with_mcp_example.yaml +40 -0
  28. massgen/configs/tools/custom_tools/claude_code_custom_tool_with_wrong_mcp_example.yaml +38 -0
  29. massgen/configs/tools/custom_tools/claude_code_wrong_custom_tool_with_mcp_example.yaml +38 -0
  30. massgen/configs/tools/custom_tools/claude_custom_tool_example.yaml +24 -0
  31. massgen/configs/tools/custom_tools/claude_custom_tool_example_no_path.yaml +22 -0
  32. massgen/configs/tools/custom_tools/claude_custom_tool_with_mcp_example.yaml +35 -0
  33. massgen/configs/tools/custom_tools/claude_custom_tool_with_wrong_mcp_example.yaml +33 -0
  34. massgen/configs/tools/custom_tools/claude_wrong_custom_tool_with_mcp_example.yaml +33 -0
  35. massgen/configs/tools/custom_tools/gemini_custom_tool_example.yaml +24 -0
  36. massgen/configs/tools/custom_tools/gemini_custom_tool_example_no_path.yaml +22 -0
  37. massgen/configs/tools/custom_tools/gemini_custom_tool_with_mcp_example.yaml +35 -0
  38. massgen/configs/tools/custom_tools/gemini_custom_tool_with_wrong_mcp_example.yaml +33 -0
  39. massgen/configs/tools/custom_tools/gemini_wrong_custom_tool_with_mcp_example.yaml +33 -0
  40. massgen/configs/tools/custom_tools/github_issue_market_analysis.yaml +94 -0
  41. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_example.yaml +24 -0
  42. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_example_no_path.yaml +22 -0
  43. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_with_mcp_example.yaml +35 -0
  44. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_with_wrong_mcp_example.yaml +33 -0
  45. massgen/configs/tools/custom_tools/gpt5_nano_wrong_custom_tool_with_mcp_example.yaml +33 -0
  46. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_example.yaml +25 -0
  47. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_example_no_path.yaml +23 -0
  48. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_with_mcp_example.yaml +34 -0
  49. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_with_wrong_mcp_example.yaml +34 -0
  50. massgen/configs/tools/custom_tools/gpt_oss_wrong_custom_tool_with_mcp_example.yaml +34 -0
  51. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_example.yaml +24 -0
  52. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_example_no_path.yaml +22 -0
  53. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_with_mcp_example.yaml +35 -0
  54. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_with_wrong_mcp_example.yaml +33 -0
  55. massgen/configs/tools/custom_tools/grok3_mini_wrong_custom_tool_with_mcp_example.yaml +33 -0
  56. massgen/configs/tools/custom_tools/qwen_api_custom_tool_example.yaml +25 -0
  57. massgen/configs/tools/custom_tools/qwen_api_custom_tool_example_no_path.yaml +23 -0
  58. massgen/configs/tools/custom_tools/qwen_api_custom_tool_with_mcp_example.yaml +36 -0
  59. massgen/configs/tools/custom_tools/qwen_api_custom_tool_with_wrong_mcp_example.yaml +34 -0
  60. massgen/configs/tools/custom_tools/qwen_api_wrong_custom_tool_with_mcp_example.yaml +34 -0
  61. massgen/configs/tools/custom_tools/qwen_local_custom_tool_example.yaml +24 -0
  62. massgen/configs/tools/custom_tools/qwen_local_custom_tool_example_no_path.yaml +22 -0
  63. massgen/configs/tools/custom_tools/qwen_local_custom_tool_with_mcp_example.yaml +35 -0
  64. massgen/configs/tools/custom_tools/qwen_local_custom_tool_with_wrong_mcp_example.yaml +33 -0
  65. massgen/configs/tools/custom_tools/qwen_local_wrong_custom_tool_with_mcp_example.yaml +33 -0
  66. massgen/configs/tools/filesystem/claude_code_context_sharing.yaml +1 -1
  67. massgen/configs/voting/gemini_gpt_voting_sensitivity.yaml +67 -0
  68. massgen/formatter/_chat_completions_formatter.py +104 -0
  69. massgen/formatter/_claude_formatter.py +120 -0
  70. massgen/formatter/_gemini_formatter.py +448 -0
  71. massgen/formatter/_response_formatter.py +88 -0
  72. massgen/frontend/coordination_ui.py +4 -2
  73. massgen/logger_config.py +35 -3
  74. massgen/message_templates.py +56 -6
  75. massgen/orchestrator.py +179 -10
  76. massgen/stream_chunk/base.py +3 -0
  77. massgen/tests/custom_tools_example.py +392 -0
  78. massgen/tests/mcp_test_server.py +17 -7
  79. massgen/tests/test_config_builder.py +423 -0
  80. massgen/tests/test_custom_tools.py +401 -0
  81. massgen/tests/test_tools.py +127 -0
  82. massgen/tool/README.md +935 -0
  83. massgen/tool/__init__.py +39 -0
  84. massgen/tool/_async_helpers.py +70 -0
  85. massgen/tool/_basic/__init__.py +8 -0
  86. massgen/tool/_basic/_two_num_tool.py +24 -0
  87. massgen/tool/_code_executors/__init__.py +10 -0
  88. massgen/tool/_code_executors/_python_executor.py +74 -0
  89. massgen/tool/_code_executors/_shell_executor.py +61 -0
  90. massgen/tool/_exceptions.py +39 -0
  91. massgen/tool/_file_handlers/__init__.py +10 -0
  92. massgen/tool/_file_handlers/_file_operations.py +218 -0
  93. massgen/tool/_manager.py +634 -0
  94. massgen/tool/_registered_tool.py +88 -0
  95. massgen/tool/_result.py +66 -0
  96. massgen/tool/_self_evolution/_github_issue_analyzer.py +369 -0
  97. massgen/tool/docs/builtin_tools.md +681 -0
  98. massgen/tool/docs/exceptions.md +794 -0
  99. massgen/tool/docs/execution_results.md +691 -0
  100. massgen/tool/docs/manager.md +887 -0
  101. massgen/tool/docs/workflow_toolkits.md +529 -0
  102. massgen/tool/workflow_toolkits/__init__.py +57 -0
  103. massgen/tool/workflow_toolkits/base.py +55 -0
  104. massgen/tool/workflow_toolkits/new_answer.py +126 -0
  105. massgen/tool/workflow_toolkits/vote.py +167 -0
  106. {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/METADATA +89 -131
  107. {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/RECORD +111 -36
  108. {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/WHEEL +0 -0
  109. {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/entry_points.txt +0 -0
  110. {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/licenses/LICENSE +0 -0
  111. {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/top_level.txt +0 -0
massgen/cli.py CHANGED
@@ -8,30 +8,34 @@ Supports both interactive mode and single-question mode.
8
8
 
9
9
  Usage examples:
10
10
  # Use YAML/JSON configuration file
11
- python -m massgen.cli --config config.yaml "What is the capital of France?"
11
+ massgen --config config.yaml "What is the capital of France?"
12
12
 
13
13
  # Quick setup with backend and model
14
- python -m massgen.cli --backend openai --model gpt-4o-mini "What is 2+2?"
14
+ massgen --backend openai --model gpt-4o-mini "What is 2+2?"
15
15
 
16
16
  # Interactive mode
17
- python -m massgen.cli --config config.yaml
17
+ massgen --config config.yaml
18
+ massgen # Uses default config if available
18
19
 
19
20
  # Multiple agents from config
20
- python -m massgen.cli --config multi_agent.yaml "Compare different approaches to renewable energy" # noqa
21
+ massgen --config multi_agent.yaml "Compare different approaches to renewable energy"
21
22
  """
22
23
 
23
24
  import argparse
24
25
  import asyncio
26
+ import copy
25
27
  import json
26
28
  import os
27
29
  import shutil
28
30
  import sys
29
31
  from datetime import datetime
30
32
  from pathlib import Path
31
- from typing import Any, Dict, List, Optional
33
+ from typing import Any, Dict, List, Optional, Tuple
32
34
 
35
+ import questionary
33
36
  import yaml
34
37
  from dotenv import load_dotenv
38
+ from prompt_toolkit.styles import Style
35
39
  from rich.console import Console
36
40
  from rich.panel import Panel
37
41
  from rich.table import Table
@@ -48,7 +52,7 @@ from .backend.lmstudio import LMStudioBackend
48
52
  from .backend.response import ResponseBackend
49
53
  from .chat_agent import ConfigurableAgent, SingleAgent
50
54
  from .frontend.coordination_ui import CoordinationUI
51
- from .logger_config import _DEBUG_MODE, logger, setup_logging
55
+ from .logger_config import _DEBUG_MODE, logger, save_execution_metadata, setup_logging
52
56
  from .orchestrator import Orchestrator
53
57
  from .utils import get_backend_type_from_model
54
58
 
@@ -86,6 +90,22 @@ BRIGHT_WHITE = "\033[97m"
86
90
  RESET = "\033[0m"
87
91
  BOLD = "\033[1m"
88
92
 
93
+ # Custom questionary style for polished selection interface
94
+ MASSGEN_QUESTIONARY_STYLE = Style(
95
+ [
96
+ ("qmark", "fg:#00d7ff bold"), # Bright cyan question mark
97
+ ("question", "fg:#ffffff bold"), # White question text
98
+ ("answer", "fg:#00d7ff bold"), # Bright cyan answer
99
+ ("pointer", "fg:#00d7ff bold"), # Bright cyan pointer (▸)
100
+ ("highlighted", "fg:#00d7ff bold"), # Bright cyan highlighted option
101
+ ("selected", "fg:#00ff87"), # Bright green selected
102
+ ("separator", "fg:#6c6c6c"), # Gray separators
103
+ ("instruction", "fg:#808080"), # Gray instructions
104
+ ("text", "fg:#ffffff"), # White text
105
+ ("disabled", "fg:#6c6c6c italic"), # Gray disabled
106
+ ],
107
+ )
108
+
89
109
 
90
110
  class ConfigurationError(Exception):
91
111
  """Configuration error for CLI."""
@@ -802,105 +822,94 @@ async def run_question_with_history(
802
822
  messages = history.copy()
803
823
  messages.append({"role": "user", "content": question})
804
824
 
805
- # Check if we should use orchestrator for single agents (default: False for backward compatibility)
806
- use_orchestrator_for_single = ui_config.get("use_orchestrator_for_single_agent", True)
807
-
808
- if len(agents) == 1 and not use_orchestrator_for_single:
809
- # Single agent mode with history
810
- agent = next(iter(agents.values()))
811
- print(f"\n🤖 {BRIGHT_CYAN}Single Agent Mode{RESET}", flush=True)
812
- print(f"Agent: {agent.agent_id}", flush=True)
813
- if history:
814
- print(f"History: {len(history)//2} previous exchanges", flush=True)
815
- print(f"Question: {question}", flush=True)
816
- print("\n" + "=" * 60, flush=True)
817
-
818
- response_content = ""
819
-
820
- async for chunk in agent.chat(messages):
821
- if chunk.type == "content" and chunk.content:
822
- response_content += chunk.content
823
- print(chunk.content, end="", flush=True)
824
- elif chunk.type == "builtin_tool_results":
825
- # Skip builtin_tool_results to avoid duplication with real-time streaming
826
- # The backends already show tool status during execution
827
- continue
828
- elif chunk.type == "error":
829
- print(f"\n❌ Error: {chunk.error}", flush=True)
830
- return ("", session_info.get("session_id"), session_info.get("current_turn", 0))
831
-
832
- print("\n" + "=" * 60, flush=True)
833
- # Single agent mode doesn't use session storage
834
- return (response_content, session_info.get("session_id"), session_info.get("current_turn", 0))
825
+ # In multiturn mode with session persistence, ALWAYS use orchestrator for proper final/ directory creation
826
+ # Single agents in multiturn mode need the orchestrator to create session artifacts (final/, workspace/, etc.)
827
+ # The orchestrator handles single agents efficiently by skipping unnecessary coordination
828
+
829
+ # Create orchestrator config with timeout settings
830
+ timeout_config = kwargs.get("timeout_config")
831
+ orchestrator_config = AgentConfig()
832
+ if timeout_config:
833
+ orchestrator_config.timeout_config = timeout_config
834
+
835
+ # Get orchestrator parameters from config
836
+ orchestrator_cfg = kwargs.get("orchestrator", {})
837
+
838
+ # Apply voting sensitivity if specified
839
+ if "voting_sensitivity" in orchestrator_cfg:
840
+ orchestrator_config.voting_sensitivity = orchestrator_cfg["voting_sensitivity"]
841
+
842
+ # Apply answer count limit if specified
843
+ if "max_new_answers_per_agent" in orchestrator_cfg:
844
+ orchestrator_config.max_new_answers_per_agent = orchestrator_cfg["max_new_answers_per_agent"]
845
+
846
+ # Apply answer novelty requirement if specified
847
+ if "answer_novelty_requirement" in orchestrator_cfg:
848
+ orchestrator_config.answer_novelty_requirement = orchestrator_cfg["answer_novelty_requirement"]
849
+
850
+ # Get context sharing parameters
851
+ snapshot_storage = orchestrator_cfg.get("snapshot_storage")
852
+ agent_temporary_workspace = orchestrator_cfg.get("agent_temporary_workspace")
853
+ session_storage = orchestrator_cfg.get("session_storage", "sessions") # Default to "sessions"
854
+
855
+ # Get debug/test parameters
856
+ if orchestrator_cfg.get("skip_coordination_rounds", False):
857
+ orchestrator_config.skip_coordination_rounds = True
858
+
859
+ # Load previous turns from session storage for multi-turn conversations
860
+ previous_turns = load_previous_turns(session_info, session_storage)
861
+
862
+ orchestrator = Orchestrator(
863
+ agents=agents,
864
+ config=orchestrator_config,
865
+ snapshot_storage=snapshot_storage,
866
+ agent_temporary_workspace=agent_temporary_workspace,
867
+ previous_turns=previous_turns,
868
+ )
869
+ # Create a fresh UI instance for each question to ensure clean state
870
+ ui = CoordinationUI(
871
+ display_type=ui_config.get("display_type", "rich_terminal"),
872
+ logging_enabled=ui_config.get("logging_enabled", True),
873
+ enable_final_presentation=True, # Required for multi-turn: ensures final answer is saved
874
+ )
835
875
 
876
+ # Determine display mode text
877
+ if len(agents) == 1:
878
+ mode_text = "Single Agent (Orchestrator)"
836
879
  else:
837
- # Multi-agent mode with history
838
- # Create orchestrator config with timeout settings
839
- timeout_config = kwargs.get("timeout_config")
840
- orchestrator_config = AgentConfig()
841
- if timeout_config:
842
- orchestrator_config.timeout_config = timeout_config
843
-
844
- # Get orchestrator parameters from config
845
- orchestrator_cfg = kwargs.get("orchestrator", {})
846
-
847
- # Get context sharing parameters
848
- snapshot_storage = orchestrator_cfg.get("snapshot_storage")
849
- agent_temporary_workspace = orchestrator_cfg.get("agent_temporary_workspace")
850
- session_storage = orchestrator_cfg.get("session_storage", "sessions") # Default to "sessions"
851
-
852
- # Get debug/test parameters
853
- if orchestrator_cfg.get("skip_coordination_rounds", False):
854
- orchestrator_config.skip_coordination_rounds = True
855
-
856
- # Load previous turns from session storage for multi-turn conversations
857
- previous_turns = load_previous_turns(session_info, session_storage)
858
-
859
- orchestrator = Orchestrator(
860
- agents=agents,
861
- config=orchestrator_config,
862
- snapshot_storage=snapshot_storage,
863
- agent_temporary_workspace=agent_temporary_workspace,
864
- previous_turns=previous_turns,
865
- )
866
- # Create a fresh UI instance for each question to ensure clean state
867
- ui = CoordinationUI(
868
- display_type=ui_config.get("display_type", "rich_terminal"),
869
- logging_enabled=ui_config.get("logging_enabled", True),
870
- enable_final_presentation=True, # Required for multi-turn: ensures final answer is saved
871
- )
880
+ mode_text = "Multi-Agent"
872
881
 
873
- print(f"\n🤖 {BRIGHT_CYAN}Multi-Agent Mode{RESET}", flush=True)
874
- print(f"Agents: {', '.join(agents.keys())}", flush=True)
875
- if history:
876
- print(f"History: {len(history)//2} previous exchanges", flush=True)
877
- print(f"Question: {question}", flush=True)
878
- print("\n" + "=" * 60, flush=True)
882
+ print(f"\n🤖 {BRIGHT_CYAN}{mode_text}{RESET}", flush=True)
883
+ print(f"Agents: {', '.join(agents.keys())}", flush=True)
884
+ if history:
885
+ print(f"History: {len(history)//2} previous exchanges", flush=True)
886
+ print(f"Question: {question}", flush=True)
887
+ print("\n" + "=" * 60, flush=True)
879
888
 
880
- # For multi-agent with history, we need to use a different approach
881
- # that maintains coordination UI display while supporting conversation context
889
+ # For multi-agent with history, we need to use a different approach
890
+ # that maintains coordination UI display while supporting conversation context
882
891
 
883
- if history and len(history) > 0:
884
- # Use coordination UI with conversation context
885
- # Extract current question from messages
886
- current_question = messages[-1].get("content", question) if messages else question
892
+ if history and len(history) > 0:
893
+ # Use coordination UI with conversation context
894
+ # Extract current question from messages
895
+ current_question = messages[-1].get("content", question) if messages else question
887
896
 
888
- # Pass the full message context to the UI coordination
889
- response_content = await ui.coordinate_with_context(orchestrator, current_question, messages)
890
- else:
891
- # Standard coordination for new conversations
892
- response_content = await ui.coordinate(orchestrator, question)
893
-
894
- # Handle session persistence if applicable
895
- session_id_to_use, updated_turn, normalized_response = await handle_session_persistence(
896
- orchestrator,
897
- question,
898
- session_info,
899
- session_storage,
900
- )
897
+ # Pass the full message context to the UI coordination
898
+ response_content = await ui.coordinate_with_context(orchestrator, current_question, messages)
899
+ else:
900
+ # Standard coordination for new conversations
901
+ response_content = await ui.coordinate(orchestrator, question)
902
+
903
+ # Handle session persistence if applicable
904
+ session_id_to_use, updated_turn, normalized_response = await handle_session_persistence(
905
+ orchestrator,
906
+ question,
907
+ session_info,
908
+ session_storage,
909
+ )
901
910
 
902
- # Return normalized response so conversation history has correct paths
903
- return (normalized_response or response_content, session_id_to_use, updated_turn)
911
+ # Return normalized response so conversation history has correct paths
912
+ return (normalized_response or response_content, session_id_to_use, updated_turn)
904
913
 
905
914
 
906
915
  async def run_single_question(question: str, agents: Dict[str, SingleAgent], ui_config: Dict[str, Any], **kwargs) -> str:
@@ -945,6 +954,18 @@ async def run_single_question(question: str, agents: Dict[str, SingleAgent], ui_
945
954
  # Get orchestrator parameters from config
946
955
  orchestrator_cfg = kwargs.get("orchestrator", {})
947
956
 
957
+ # Apply voting sensitivity if specified
958
+ if "voting_sensitivity" in orchestrator_cfg:
959
+ orchestrator_config.voting_sensitivity = orchestrator_cfg["voting_sensitivity"]
960
+
961
+ # Apply answer count limit if specified
962
+ if "max_new_answers_per_agent" in orchestrator_cfg:
963
+ orchestrator_config.max_new_answers_per_agent = orchestrator_cfg["max_new_answers_per_agent"]
964
+
965
+ # Apply answer novelty requirement if specified
966
+ if "answer_novelty_requirement" in orchestrator_cfg:
967
+ orchestrator_config.answer_novelty_requirement = orchestrator_cfg["answer_novelty_requirement"]
968
+
948
969
  # Get context sharing parameters
949
970
  snapshot_storage = orchestrator_cfg.get("snapshot_storage")
950
971
  agent_temporary_workspace = orchestrator_cfg.get("agent_temporary_workspace")
@@ -1219,6 +1240,411 @@ def print_example_config(name: str):
1219
1240
  sys.exit(1)
1220
1241
 
1221
1242
 
1243
+ def discover_available_configs() -> Dict[str, List[Tuple[str, Path]]]:
1244
+ """Discover all available configuration files.
1245
+
1246
+ Returns:
1247
+ Dict with categories as keys and list of (display_name, path) tuples as values
1248
+ """
1249
+ configs = {
1250
+ "User Configs": [],
1251
+ "Project Configs": [],
1252
+ "Current Directory": [],
1253
+ "Package Examples": [],
1254
+ }
1255
+
1256
+ # 1. User configs (~/.config/massgen/agents/)
1257
+ user_agents_dir = Path.home() / ".config/massgen/agents"
1258
+ if user_agents_dir.exists():
1259
+ for config_file in sorted(user_agents_dir.glob("*.yaml")):
1260
+ display_name = config_file.stem
1261
+ configs["User Configs"].append((display_name, config_file))
1262
+
1263
+ # 2. Project configs (.massgen/)
1264
+ project_config_dir = Path.cwd() / ".massgen"
1265
+ if project_config_dir.exists():
1266
+ for config_file in sorted(project_config_dir.glob("*.yaml")):
1267
+ display_name = f".massgen/{config_file.name}"
1268
+ configs["Project Configs"].append((display_name, config_file))
1269
+
1270
+ # 3. Current directory (*.yaml files, excluding .massgen/ and non-massgen configs)
1271
+ # Filter out common non-massgen YAML files
1272
+ exclude_patterns = {
1273
+ ".pre-commit-config.yaml",
1274
+ ".readthedocs.yaml",
1275
+ ".github",
1276
+ "docker-compose",
1277
+ "ansible",
1278
+ "kubernetes",
1279
+ }
1280
+
1281
+ for config_file in sorted(Path.cwd().glob("*.yaml")):
1282
+ # Skip if inside .massgen/ (already covered)
1283
+ if ".massgen" in str(config_file):
1284
+ continue
1285
+
1286
+ # Skip common non-massgen config files
1287
+ file_name = config_file.name.lower()
1288
+ if any(pattern in file_name for pattern in exclude_patterns):
1289
+ continue
1290
+
1291
+ display_name = config_file.name
1292
+ configs["Current Directory"].append((display_name, config_file))
1293
+
1294
+ # 4. Package examples (massgen/configs/)
1295
+ try:
1296
+ from importlib.resources import files
1297
+
1298
+ configs_root = files("massgen") / "configs"
1299
+
1300
+ # Organize by subdirectory
1301
+ for config_file in sorted(configs_root.rglob("*.yaml")):
1302
+ # Get relative path from configs root
1303
+ rel_path = str(config_file).replace(str(configs_root) + "/", "")
1304
+ # Skip README and docs
1305
+ if "README" in rel_path or "BACKEND_CONFIGURATION" in rel_path:
1306
+ continue
1307
+ # Use relative path as display name
1308
+ display_name = rel_path.replace(".yaml", "")
1309
+ configs["Package Examples"].append((display_name, Path(str(config_file))))
1310
+
1311
+ except Exception as e:
1312
+ logger.warning(f"Could not load package examples: {e}")
1313
+
1314
+ # Remove empty categories
1315
+ configs = {k: v for k, v in configs.items() if v}
1316
+
1317
+ return configs
1318
+
1319
+
1320
+ def interactive_config_selector() -> Optional[str]:
1321
+ """Interactively select a configuration file.
1322
+
1323
+ Shows user/project/current directory configs directly in a flat list.
1324
+ Package examples are shown hierarchically (category → config).
1325
+
1326
+ Returns:
1327
+ Path to selected config file, or None if cancelled
1328
+ """
1329
+ # Create console instance for rich output
1330
+ selector_console = Console()
1331
+
1332
+ # Discover all available configs
1333
+ configs = discover_available_configs()
1334
+
1335
+ if not configs:
1336
+ selector_console.print(
1337
+ "\n[yellow]⚠️ No configurations found![/yellow]",
1338
+ )
1339
+ selector_console.print("[dim]Create one with: massgen --init[/dim]\n")
1340
+ return None
1341
+
1342
+ # Create a summary table showing what's available
1343
+ summary_table = Table(
1344
+ show_header=True,
1345
+ header_style="bold bright_white",
1346
+ border_style="bright_black",
1347
+ box=None,
1348
+ padding=(0, 1),
1349
+ width=88,
1350
+ )
1351
+ summary_table.add_column("Category", style="bright_cyan", no_wrap=True, width=25)
1352
+ summary_table.add_column("Count", justify="center", style="bright_yellow", width=10)
1353
+ summary_table.add_column("Location", style="dim")
1354
+
1355
+ # Build summary and choices
1356
+ choices = []
1357
+
1358
+ # Build summary table (overview only - no duplication)
1359
+ # User configs
1360
+ if "User Configs" in configs and configs["User Configs"]:
1361
+ summary_table.add_row(
1362
+ "👤 Your Configs",
1363
+ str(len(configs["User Configs"])),
1364
+ "~/.config/massgen/agents/",
1365
+ )
1366
+ choices.append(questionary.Separator("\n─────────────────────────────────"))
1367
+ for display_name, path in configs["User Configs"]:
1368
+ choices.append(
1369
+ questionary.Choice(
1370
+ title=f" 👤 {display_name}",
1371
+ value=str(path),
1372
+ ),
1373
+ )
1374
+
1375
+ # Project configs
1376
+ if "Project Configs" in configs and configs["Project Configs"]:
1377
+ summary_table.add_row(
1378
+ "📁 Project Configs",
1379
+ str(len(configs["Project Configs"])),
1380
+ ".massgen/",
1381
+ )
1382
+ if choices:
1383
+ choices.append(questionary.Separator("\n─────────────────────────────────"))
1384
+ else:
1385
+ choices.append(questionary.Separator("\n─────────────────────────────────"))
1386
+ for display_name, path in configs["Project Configs"]:
1387
+ choices.append(
1388
+ questionary.Choice(
1389
+ title=f" 📁 {display_name}",
1390
+ value=str(path),
1391
+ ),
1392
+ )
1393
+
1394
+ # Current directory configs
1395
+ if "Current Directory" in configs and configs["Current Directory"]:
1396
+ summary_table.add_row(
1397
+ "📂 Current Directory",
1398
+ str(len(configs["Current Directory"])),
1399
+ f"*.yaml in {Path.cwd().name}/",
1400
+ )
1401
+ if choices:
1402
+ choices.append(questionary.Separator("\n─────────────────────────────────"))
1403
+ else:
1404
+ choices.append(questionary.Separator("\n─────────────────────────────────"))
1405
+ for display_name, path in configs["Current Directory"]:
1406
+ choices.append(
1407
+ questionary.Choice(
1408
+ title=f" 📂 {display_name}",
1409
+ value=str(path),
1410
+ ),
1411
+ )
1412
+
1413
+ # Package examples
1414
+ if "Package Examples" in configs and configs["Package Examples"]:
1415
+ summary_table.add_row(
1416
+ "📦 Package Examples",
1417
+ str(len(configs["Package Examples"])),
1418
+ "Built-in examples (hierarchical browser)",
1419
+ )
1420
+ if choices:
1421
+ choices.append(questionary.Separator("\n─────────────────────────────────"))
1422
+ choices.append(
1423
+ questionary.Choice(
1424
+ title=f" 📦 Browse {len(configs['Package Examples'])} example configs →",
1425
+ value="__browse_examples__",
1426
+ ),
1427
+ )
1428
+
1429
+ # Display summary table in a panel
1430
+ selector_console.print()
1431
+ selector_console.print(
1432
+ Panel(
1433
+ summary_table,
1434
+ title="[bold bright_cyan]🚀 Select a Configuration[/bold bright_cyan]",
1435
+ border_style="bright_cyan",
1436
+ padding=(0, 1),
1437
+ width=90,
1438
+ ),
1439
+ )
1440
+
1441
+ # Add cancel option
1442
+ choices.append(questionary.Separator("\n─────────────────────────────────"))
1443
+ choices.append(questionary.Choice(title=" ❌ Cancel", value="__cancel__"))
1444
+
1445
+ # Show the selector
1446
+ selector_console.print()
1447
+ selected = questionary.select(
1448
+ "Select a configuration:",
1449
+ choices=choices,
1450
+ use_shortcuts=True,
1451
+ use_arrow_keys=True,
1452
+ style=MASSGEN_QUESTIONARY_STYLE,
1453
+ pointer="▸",
1454
+ ).ask()
1455
+
1456
+ if selected is None or selected == "__cancel__":
1457
+ selector_console.print("\n[yellow]⚠️ Selection cancelled[/yellow]\n")
1458
+ return None
1459
+
1460
+ # If user wants to browse package examples, show hierarchical navigation
1461
+ if selected == "__browse_examples__":
1462
+ return _select_package_example(configs["Package Examples"], selector_console)
1463
+
1464
+ # Otherwise, return the selected config path
1465
+ selector_console.print(f"\n[bold green]✓ Selected:[/bold green] [cyan]{selected}[/cyan]\n")
1466
+ return selected
1467
+
1468
+
1469
+ def _select_package_example(examples: List[Tuple[str, Path]], console: Console) -> Optional[str]:
1470
+ """Show hierarchical navigation for package examples.
1471
+
1472
+ Args:
1473
+ examples: List of (display_name, path) tuples
1474
+ console: Rich console for output
1475
+
1476
+ Returns:
1477
+ Path to selected config, or None if cancelled/back
1478
+ """
1479
+ # Organize examples by category (first directory in path)
1480
+ categories = {}
1481
+ for display_name, path in examples:
1482
+ # Extract category from display name (e.g., "basic/multi/config" -> "basic")
1483
+ parts = display_name.split("/")
1484
+ category = parts[0] if len(parts) > 1 else "other"
1485
+
1486
+ if category not in categories:
1487
+ categories[category] = []
1488
+ categories[category].append((display_name, path))
1489
+
1490
+ # Emoji mapping for categories
1491
+ category_emojis = {
1492
+ "basic": "🎯",
1493
+ "tools": "🛠️",
1494
+ "providers": "🌐",
1495
+ "configs": "⚙️",
1496
+ "other": "📋",
1497
+ }
1498
+
1499
+ # Create category summary table
1500
+ category_table = Table(
1501
+ show_header=True,
1502
+ header_style="bold bright_white",
1503
+ border_style="bright_black",
1504
+ box=None,
1505
+ padding=(0, 1),
1506
+ width=88,
1507
+ )
1508
+ category_table.add_column("Category", style="bright_cyan", no_wrap=True, width=20)
1509
+ category_table.add_column("Count", justify="center", style="bright_yellow", width=10)
1510
+ category_table.add_column("Description", style="dim")
1511
+
1512
+ # Category descriptions
1513
+ category_descriptions = {
1514
+ "basic": "Simple configurations for getting started",
1515
+ "tools": "Configs demonstrating tool integrations",
1516
+ "providers": "Provider-specific example configs",
1517
+ "configs": "Advanced configuration examples",
1518
+ "other": "Miscellaneous configurations",
1519
+ }
1520
+
1521
+ # Build category table and choices
1522
+ category_choices = []
1523
+ for category in sorted(categories.keys()):
1524
+ count = len(categories[category])
1525
+ emoji = category_emojis.get(category, "📁")
1526
+ description = category_descriptions.get(category, "Example configurations")
1527
+
1528
+ category_table.add_row(
1529
+ f"{emoji} {category.title()}",
1530
+ str(count),
1531
+ description,
1532
+ )
1533
+
1534
+ category_choices.append(
1535
+ questionary.Choice(
1536
+ title=f" {emoji} {category.title()} ({count} config{'s' if count != 1 else ''})",
1537
+ value=category,
1538
+ ),
1539
+ )
1540
+
1541
+ # Display category summary in a panel
1542
+ console.print()
1543
+ console.print(
1544
+ Panel(
1545
+ category_table,
1546
+ title="[bold bright_yellow]📦 Package Examples - Select Category[/bold bright_yellow]",
1547
+ border_style="bright_yellow",
1548
+ padding=(0, 1),
1549
+ width=90,
1550
+ ),
1551
+ )
1552
+
1553
+ # Add back option
1554
+ category_choices.append(questionary.Separator("\n─────────────────────────────────"))
1555
+ category_choices.append(questionary.Choice(title=" ← Back to main menu", value="__back__"))
1556
+
1557
+ # Step 1: Select category
1558
+ console.print()
1559
+ selected_category = questionary.select(
1560
+ "Select a category:",
1561
+ choices=category_choices,
1562
+ use_shortcuts=True,
1563
+ use_arrow_keys=True,
1564
+ style=MASSGEN_QUESTIONARY_STYLE,
1565
+ pointer="▸",
1566
+ ).ask()
1567
+
1568
+ if selected_category is None or selected_category == "__cancel__":
1569
+ console.print("\n[yellow]⚠️ Selection cancelled[/yellow]\n")
1570
+ return None
1571
+
1572
+ if selected_category == "__back__":
1573
+ # Go back to main selector
1574
+ return interactive_config_selector()
1575
+
1576
+ # Create configs table
1577
+ emoji = category_emojis.get(selected_category, "📁")
1578
+ configs_table = Table(
1579
+ show_header=True,
1580
+ header_style="bold bright_white",
1581
+ border_style="bright_black",
1582
+ box=None,
1583
+ padding=(0, 1),
1584
+ width=88,
1585
+ )
1586
+ configs_table.add_column("#", style="dim", width=5, justify="right")
1587
+ configs_table.add_column("Configuration", style="bright_cyan")
1588
+
1589
+ # Build config choices and table
1590
+ config_choices = []
1591
+ for idx, (display_name, path) in enumerate(sorted(categories[selected_category]), 1):
1592
+ # Show relative path within category
1593
+ short_name = display_name.replace(f"{selected_category}/", "")
1594
+ configs_table.add_row(str(idx), short_name)
1595
+ config_choices.append(
1596
+ questionary.Choice(
1597
+ title=f" {idx:2d}. {short_name}",
1598
+ value=str(path),
1599
+ ),
1600
+ )
1601
+
1602
+ # Display configs in a panel
1603
+ console.print()
1604
+ console.print(
1605
+ Panel(
1606
+ configs_table,
1607
+ title=f"[bold bright_green]{emoji} {selected_category.title()} Configurations[/bold bright_green]",
1608
+ border_style="bright_green",
1609
+ padding=(0, 1),
1610
+ width=90,
1611
+ ),
1612
+ )
1613
+
1614
+ # Add back option
1615
+ config_choices.append(questionary.Separator("\n─────────────────────────────────"))
1616
+ config_choices.append(questionary.Choice(title=" ← Back to categories", value="__back__"))
1617
+
1618
+ # Step 2: Select config
1619
+ # For large lists: disable shortcuts (max 36) and enable search filter for better UX
1620
+ # Note: When search filter is enabled, j/k keys must be disabled (they conflict with search)
1621
+ use_shortcuts = len(config_choices) <= 36
1622
+ use_search_filter = len(config_choices) > 36
1623
+ console.print()
1624
+ selected_config = questionary.select(
1625
+ "Select a configuration:",
1626
+ choices=config_choices,
1627
+ use_shortcuts=use_shortcuts,
1628
+ use_arrow_keys=True,
1629
+ use_search_filter=use_search_filter,
1630
+ use_jk_keys=not use_search_filter,
1631
+ style=MASSGEN_QUESTIONARY_STYLE,
1632
+ pointer="▸",
1633
+ ).ask()
1634
+
1635
+ if selected_config is None or selected_config == "__cancel__":
1636
+ console.print("\n[yellow]⚠️ Selection cancelled[/yellow]\n")
1637
+ return None
1638
+
1639
+ if selected_config == "__back__":
1640
+ # Recursively call to go back to category selection
1641
+ return _select_package_example(examples, console)
1642
+
1643
+ # Return the selected config path
1644
+ console.print(f"\n[bold green]✓ Selected:[/bold green] [cyan]{selected_config}[/cyan]\n")
1645
+ return selected_config
1646
+
1647
+
1222
1648
  def should_run_builder() -> bool:
1223
1649
  """Check if config builder should run automatically.
1224
1650
 
@@ -1511,6 +1937,14 @@ async def run_interactive_mode(
1511
1937
  setup_logging(debug=_DEBUG_MODE, turn=next_turn)
1512
1938
  logger.info(f"Starting turn {next_turn}")
1513
1939
 
1940
+ # Save execution metadata for this turn (original_config already has pre-relocation paths)
1941
+ save_execution_metadata(
1942
+ query=question,
1943
+ config_path=config_path,
1944
+ config_content=original_config, # This is the pre-relocation config passed from main()
1945
+ cli_args={"mode": "interactive", "turn": next_turn, "session_id": session_id},
1946
+ )
1947
+
1514
1948
  # Pass session state for multi-turn filesystem support
1515
1949
  session_info = {
1516
1950
  "session_id": session_id,
@@ -1612,6 +2046,9 @@ async def main(args):
1612
2046
  logger.debug(f"Created simple config with backend: {backend}, model: {model}")
1613
2047
  logger.debug(f"Config content: {json.dumps(config, indent=2)}")
1614
2048
 
2049
+ # Save original config before relocation (for execution_metadata.yaml)
2050
+ original_config_for_metadata = copy.deepcopy(config)
2051
+
1615
2052
  # Validate that all context paths exist before proceeding
1616
2053
  validate_context_paths(config)
1617
2054
 
@@ -1695,6 +2132,16 @@ async def main(args):
1695
2132
  if "orchestrator" in config:
1696
2133
  kwargs["orchestrator"] = config["orchestrator"]
1697
2134
 
2135
+ # Save execution metadata for debugging and reconstruction
2136
+ if args.question:
2137
+ # For single question mode, save metadata now (use original config before .massgen/ relocation)
2138
+ save_execution_metadata(
2139
+ query=args.question,
2140
+ config_path=str(resolved_path) if args.config and "resolved_path" in locals() else None,
2141
+ config_content=original_config_for_metadata,
2142
+ cli_args=vars(args),
2143
+ )
2144
+
1698
2145
  # Run mode based on whether question was provided
1699
2146
  try:
1700
2147
  if args.question:
@@ -1734,23 +2181,27 @@ def cli_main():
1734
2181
  epilog="""
1735
2182
  Examples:
1736
2183
  # Use configuration file
1737
- python -m massgen.cli --config config.yaml "What is machine learning?"
2184
+ massgen --config config.yaml "What is machine learning?"
1738
2185
 
1739
2186
  # Quick single agent setup
1740
- python -m massgen.cli --backend openai --model gpt-4o-mini "Explain quantum computing"
1741
- python -m massgen.cli --backend claude --model claude-sonnet-4-20250514 "Analyze this data"
2187
+ massgen --backend openai --model gpt-4o-mini "Explain quantum computing"
2188
+ massgen --backend claude --model claude-sonnet-4-20250514 "Analyze this data"
1742
2189
 
1743
2190
  # Use ChatCompletion backend with custom base URL
1744
- python -m massgen.cli --backend chatcompletion --model gpt-oss-120b --base-url https://api.cerebras.ai/v1/chat/completions "What is 2+2?"
2191
+ massgen --backend chatcompletion --model gpt-oss-120b --base-url https://api.cerebras.ai/v1/chat/completions "What is 2+2?"
1745
2192
 
1746
2193
  # Interactive mode
1747
- python -m massgen.cli --config config.yaml
2194
+ massgen --config config.yaml
2195
+ massgen # Uses default config if available
1748
2196
 
1749
2197
  # Timeout control examples
1750
- python -m massgen.cli --config config.yaml --orchestrator-timeout 600 "Complex task"
2198
+ massgen --config config.yaml --orchestrator-timeout 600 "Complex task"
1751
2199
 
1752
- # Create sample configurations
1753
- python -m massgen.cli --create-samples
2200
+ # Configuration management
2201
+ massgen --init # Create new configuration interactively
2202
+ massgen --select # Choose from available configurations
2203
+ massgen --setup # Set up API keys
2204
+ massgen --list-examples # View example configurations
1754
2205
 
1755
2206
  Environment Variables:
1756
2207
  OPENAI_API_KEY - Required for OpenAI backend
@@ -1782,6 +2233,11 @@ Environment Variables:
1782
2233
  # Configuration options
1783
2234
  config_group = parser.add_mutually_exclusive_group()
1784
2235
  config_group.add_argument("--config", type=str, help="Path to YAML/JSON configuration file or @examples/NAME")
2236
+ config_group.add_argument(
2237
+ "--select",
2238
+ action="store_true",
2239
+ help="Interactively select from available configurations",
2240
+ )
1785
2241
  config_group.add_argument(
1786
2242
  "--backend",
1787
2243
  type=str,
@@ -1825,7 +2281,7 @@ Environment Variables:
1825
2281
  help="Launch interactive configuration builder to create config file",
1826
2282
  )
1827
2283
  parser.add_argument(
1828
- "--setup-keys",
2284
+ "--setup",
1829
2285
  action="store_true",
1830
2286
  help="Launch interactive API key setup wizard to configure credentials",
1831
2287
  )
@@ -1888,7 +2344,7 @@ Environment Variables:
1888
2344
  return
1889
2345
 
1890
2346
  # Launch interactive API key setup if requested
1891
- if args.setup_keys:
2347
+ if args.setup:
1892
2348
  from .config_builder import ConfigBuilder
1893
2349
 
1894
2350
  builder = ConfigBuilder()
@@ -1899,9 +2355,20 @@ Environment Variables:
1899
2355
  print(f"{BRIGHT_CYAN}💡 You can now use MassGen with these providers{RESET}\n")
1900
2356
  else:
1901
2357
  print(f"\n{BRIGHT_YELLOW}⚠️ No API keys configured{RESET}")
1902
- print(f"{BRIGHT_CYAN}💡 You can run 'massgen --setup-keys' anytime to set them up{RESET}\n")
2358
+ print(f"{BRIGHT_CYAN}💡 You can run 'massgen --setup' anytime to set them up{RESET}\n")
1903
2359
  return
1904
2360
 
2361
+ # Launch interactive config selector if requested
2362
+ if args.select:
2363
+ selected_config = interactive_config_selector()
2364
+ if selected_config:
2365
+ # Update args to use the selected config
2366
+ args.config = selected_config
2367
+ # Continue to main() with the selected config
2368
+ else:
2369
+ # User cancelled selection
2370
+ return
2371
+
1905
2372
  # Launch interactive config builder if requested
1906
2373
  if args.init:
1907
2374
  from .config_builder import ConfigBuilder
@@ -1918,7 +2385,7 @@ Environment Variables:
1918
2385
  elif filepath:
1919
2386
  # Config created but user chose not to run
1920
2387
  print(f"\n✅ Configuration saved to: {filepath}")
1921
- print(f'Run with: python -m massgen.cli --config {filepath} "Your question"')
2388
+ print(f'Run with: massgen --config {filepath} "Your question"')
1922
2389
  return
1923
2390
  else:
1924
2391
  # User cancelled