massgen 0.1.0a3__py3-none-any.whl → 0.1.2__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 (120) 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 +15 -2
  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 +83 -0
  9. massgen/backend/{base_with_mcp.py → base_with_custom_tool_and_mcp.py} +286 -15
  10. massgen/backend/capabilities.py +6 -6
  11. massgen/backend/chat_completions.py +200 -103
  12. massgen/backend/claude.py +115 -18
  13. massgen/backend/claude_code.py +378 -14
  14. massgen/backend/docs/CLAUDE_API_RESEARCH.md +3 -3
  15. massgen/backend/gemini.py +1333 -1629
  16. massgen/backend/gemini_mcp_manager.py +545 -0
  17. massgen/backend/gemini_trackers.py +344 -0
  18. massgen/backend/gemini_utils.py +43 -0
  19. massgen/backend/grok.py +39 -6
  20. massgen/backend/response.py +147 -81
  21. massgen/cli.py +605 -110
  22. massgen/config_builder.py +376 -27
  23. massgen/configs/README.md +123 -80
  24. massgen/configs/basic/multi/three_agents_default.yaml +3 -3
  25. massgen/configs/basic/single/single_agent.yaml +1 -1
  26. massgen/configs/providers/openai/gpt5_nano.yaml +3 -3
  27. massgen/configs/tools/custom_tools/claude_code_custom_tool_example.yaml +32 -0
  28. massgen/configs/tools/custom_tools/claude_code_custom_tool_example_no_path.yaml +28 -0
  29. massgen/configs/tools/custom_tools/claude_code_custom_tool_with_mcp_example.yaml +40 -0
  30. massgen/configs/tools/custom_tools/claude_code_custom_tool_with_wrong_mcp_example.yaml +38 -0
  31. massgen/configs/tools/custom_tools/claude_code_wrong_custom_tool_with_mcp_example.yaml +38 -0
  32. massgen/configs/tools/custom_tools/claude_custom_tool_example.yaml +24 -0
  33. massgen/configs/tools/custom_tools/claude_custom_tool_example_no_path.yaml +22 -0
  34. massgen/configs/tools/custom_tools/claude_custom_tool_with_mcp_example.yaml +35 -0
  35. massgen/configs/tools/custom_tools/claude_custom_tool_with_wrong_mcp_example.yaml +33 -0
  36. massgen/configs/tools/custom_tools/claude_wrong_custom_tool_with_mcp_example.yaml +33 -0
  37. massgen/configs/tools/custom_tools/gemini_custom_tool_example.yaml +24 -0
  38. massgen/configs/tools/custom_tools/gemini_custom_tool_example_no_path.yaml +22 -0
  39. massgen/configs/tools/custom_tools/gemini_custom_tool_with_mcp_example.yaml +35 -0
  40. massgen/configs/tools/custom_tools/gemini_custom_tool_with_wrong_mcp_example.yaml +33 -0
  41. massgen/configs/tools/custom_tools/gemini_wrong_custom_tool_with_mcp_example.yaml +33 -0
  42. massgen/configs/tools/custom_tools/github_issue_market_analysis.yaml +94 -0
  43. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_example.yaml +24 -0
  44. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_example_no_path.yaml +22 -0
  45. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_with_mcp_example.yaml +35 -0
  46. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_with_wrong_mcp_example.yaml +33 -0
  47. massgen/configs/tools/custom_tools/gpt5_nano_wrong_custom_tool_with_mcp_example.yaml +33 -0
  48. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_example.yaml +25 -0
  49. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_example_no_path.yaml +23 -0
  50. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_with_mcp_example.yaml +34 -0
  51. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_with_wrong_mcp_example.yaml +34 -0
  52. massgen/configs/tools/custom_tools/gpt_oss_wrong_custom_tool_with_mcp_example.yaml +34 -0
  53. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_example.yaml +24 -0
  54. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_example_no_path.yaml +22 -0
  55. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_with_mcp_example.yaml +35 -0
  56. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_with_wrong_mcp_example.yaml +33 -0
  57. massgen/configs/tools/custom_tools/grok3_mini_wrong_custom_tool_with_mcp_example.yaml +33 -0
  58. massgen/configs/tools/custom_tools/qwen_api_custom_tool_example.yaml +25 -0
  59. massgen/configs/tools/custom_tools/qwen_api_custom_tool_example_no_path.yaml +23 -0
  60. massgen/configs/tools/custom_tools/qwen_api_custom_tool_with_mcp_example.yaml +36 -0
  61. massgen/configs/tools/custom_tools/qwen_api_custom_tool_with_wrong_mcp_example.yaml +34 -0
  62. massgen/configs/tools/custom_tools/qwen_api_wrong_custom_tool_with_mcp_example.yaml +34 -0
  63. massgen/configs/tools/custom_tools/qwen_local_custom_tool_example.yaml +24 -0
  64. massgen/configs/tools/custom_tools/qwen_local_custom_tool_example_no_path.yaml +22 -0
  65. massgen/configs/tools/custom_tools/qwen_local_custom_tool_with_mcp_example.yaml +35 -0
  66. massgen/configs/tools/custom_tools/qwen_local_custom_tool_with_wrong_mcp_example.yaml +33 -0
  67. massgen/configs/tools/custom_tools/qwen_local_wrong_custom_tool_with_mcp_example.yaml +33 -0
  68. massgen/configs/tools/filesystem/claude_code_context_sharing.yaml +1 -1
  69. massgen/configs/tools/planning/five_agents_discord_mcp_planning_mode.yaml +7 -29
  70. massgen/configs/tools/planning/five_agents_filesystem_mcp_planning_mode.yaml +5 -6
  71. massgen/configs/tools/planning/five_agents_notion_mcp_planning_mode.yaml +4 -4
  72. massgen/configs/tools/planning/five_agents_twitter_mcp_planning_mode.yaml +4 -4
  73. massgen/configs/tools/planning/gpt5_mini_case_study_mcp_planning_mode.yaml +2 -2
  74. massgen/configs/voting/gemini_gpt_voting_sensitivity.yaml +67 -0
  75. massgen/formatter/_chat_completions_formatter.py +104 -0
  76. massgen/formatter/_claude_formatter.py +120 -0
  77. massgen/formatter/_gemini_formatter.py +448 -0
  78. massgen/formatter/_response_formatter.py +88 -0
  79. massgen/frontend/coordination_ui.py +4 -2
  80. massgen/logger_config.py +35 -3
  81. massgen/message_templates.py +56 -6
  82. massgen/orchestrator.py +512 -16
  83. massgen/stream_chunk/base.py +3 -0
  84. massgen/tests/custom_tools_example.py +392 -0
  85. massgen/tests/mcp_test_server.py +17 -7
  86. massgen/tests/test_config_builder.py +423 -0
  87. massgen/tests/test_custom_tools.py +401 -0
  88. massgen/tests/test_intelligent_planning_mode.py +643 -0
  89. massgen/tests/test_tools.py +127 -0
  90. massgen/token_manager/token_manager.py +13 -4
  91. massgen/tool/README.md +935 -0
  92. massgen/tool/__init__.py +39 -0
  93. massgen/tool/_async_helpers.py +70 -0
  94. massgen/tool/_basic/__init__.py +8 -0
  95. massgen/tool/_basic/_two_num_tool.py +24 -0
  96. massgen/tool/_code_executors/__init__.py +10 -0
  97. massgen/tool/_code_executors/_python_executor.py +74 -0
  98. massgen/tool/_code_executors/_shell_executor.py +61 -0
  99. massgen/tool/_exceptions.py +39 -0
  100. massgen/tool/_file_handlers/__init__.py +10 -0
  101. massgen/tool/_file_handlers/_file_operations.py +218 -0
  102. massgen/tool/_manager.py +634 -0
  103. massgen/tool/_registered_tool.py +88 -0
  104. massgen/tool/_result.py +66 -0
  105. massgen/tool/_self_evolution/_github_issue_analyzer.py +369 -0
  106. massgen/tool/docs/builtin_tools.md +681 -0
  107. massgen/tool/docs/exceptions.md +794 -0
  108. massgen/tool/docs/execution_results.md +691 -0
  109. massgen/tool/docs/manager.md +887 -0
  110. massgen/tool/docs/workflow_toolkits.md +529 -0
  111. massgen/tool/workflow_toolkits/__init__.py +57 -0
  112. massgen/tool/workflow_toolkits/base.py +55 -0
  113. massgen/tool/workflow_toolkits/new_answer.py +126 -0
  114. massgen/tool/workflow_toolkits/vote.py +167 -0
  115. {massgen-0.1.0a3.dist-info → massgen-0.1.2.dist-info}/METADATA +87 -129
  116. {massgen-0.1.0a3.dist-info → massgen-0.1.2.dist-info}/RECORD +120 -44
  117. {massgen-0.1.0a3.dist-info → massgen-0.1.2.dist-info}/WHEEL +0 -0
  118. {massgen-0.1.0a3.dist-info → massgen-0.1.2.dist-info}/entry_points.txt +0 -0
  119. {massgen-0.1.0a3.dist-info → massgen-0.1.2.dist-info}/licenses/LICENSE +0 -0
  120. {massgen-0.1.0a3.dist-info → massgen-0.1.2.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,108 @@ 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"
881
+
882
+ # Get coordination config from YAML (if present)
883
+ coordination_settings = kwargs.get("orchestrator", {}).get("coordination", {})
884
+ if coordination_settings:
885
+ from .agent_config import CoordinationConfig
886
+
887
+ orchestrator_config.coordination_config = CoordinationConfig(
888
+ enable_planning_mode=coordination_settings.get("enable_planning_mode", False),
889
+ planning_mode_instruction=coordination_settings.get(
890
+ "planning_mode_instruction",
891
+ """During coordination, describe what you would do. Only provide concrete implementation details and execute read-only actions.
892
+ DO NOT execute any actions that have side effects (e.g., sending messages, modifying data)""",
893
+ ),
894
+ )
872
895
 
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)
896
+ print(f"\n🤖 {BRIGHT_CYAN}{mode_text}{RESET}", flush=True)
897
+ print(f"Agents: {', '.join(agents.keys())}", flush=True)
898
+ if history:
899
+ print(f"History: {len(history)//2} previous exchanges", flush=True)
900
+ print(f"Question: {question}", flush=True)
901
+ print("\n" + "=" * 60, flush=True)
879
902
 
880
- # For multi-agent with history, we need to use a different approach
881
- # that maintains coordination UI display while supporting conversation context
903
+ # For multi-agent with history, we need to use a different approach
904
+ # that maintains coordination UI display while supporting conversation context
882
905
 
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
906
+ if history and len(history) > 0:
907
+ # Use coordination UI with conversation context
908
+ # Extract current question from messages
909
+ current_question = messages[-1].get("content", question) if messages else question
887
910
 
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
- )
911
+ # Pass the full message context to the UI coordination
912
+ response_content = await ui.coordinate_with_context(orchestrator, current_question, messages)
913
+ else:
914
+ # Standard coordination for new conversations
915
+ response_content = await ui.coordinate(orchestrator, question)
916
+
917
+ # Handle session persistence if applicable
918
+ session_id_to_use, updated_turn, normalized_response = await handle_session_persistence(
919
+ orchestrator,
920
+ question,
921
+ session_info,
922
+ session_storage,
923
+ )
901
924
 
902
- # Return normalized response so conversation history has correct paths
903
- return (normalized_response or response_content, session_id_to_use, updated_turn)
925
+ # Return normalized response so conversation history has correct paths
926
+ return (normalized_response or response_content, session_id_to_use, updated_turn)
904
927
 
905
928
 
906
929
  async def run_single_question(question: str, agents: Dict[str, SingleAgent], ui_config: Dict[str, Any], **kwargs) -> str:
@@ -942,9 +965,35 @@ async def run_single_question(question: str, agents: Dict[str, SingleAgent], ui_
942
965
  if timeout_config:
943
966
  orchestrator_config.timeout_config = timeout_config
944
967
 
968
+ # Get coordination config from YAML (if present)
969
+ coordination_settings = kwargs.get("orchestrator", {}).get("coordination", {})
970
+ if coordination_settings:
971
+ from .agent_config import CoordinationConfig
972
+
973
+ orchestrator_config.coordination_config = CoordinationConfig(
974
+ enable_planning_mode=coordination_settings.get("enable_planning_mode", False),
975
+ planning_mode_instruction=coordination_settings.get(
976
+ "planning_mode_instruction",
977
+ """During coordination, describe what you would do. Only provide concrete implementation details and execute read-only actions.
978
+ DO NOT execute any actions that have side effects (e.g., sending messages, modifying data)""",
979
+ ),
980
+ )
981
+
945
982
  # Get orchestrator parameters from config
946
983
  orchestrator_cfg = kwargs.get("orchestrator", {})
947
984
 
985
+ # Apply voting sensitivity if specified
986
+ if "voting_sensitivity" in orchestrator_cfg:
987
+ orchestrator_config.voting_sensitivity = orchestrator_cfg["voting_sensitivity"]
988
+
989
+ # Apply answer count limit if specified
990
+ if "max_new_answers_per_agent" in orchestrator_cfg:
991
+ orchestrator_config.max_new_answers_per_agent = orchestrator_cfg["max_new_answers_per_agent"]
992
+
993
+ # Apply answer novelty requirement if specified
994
+ if "answer_novelty_requirement" in orchestrator_cfg:
995
+ orchestrator_config.answer_novelty_requirement = orchestrator_cfg["answer_novelty_requirement"]
996
+
948
997
  # Get context sharing parameters
949
998
  snapshot_storage = orchestrator_cfg.get("snapshot_storage")
950
999
  agent_temporary_workspace = orchestrator_cfg.get("agent_temporary_workspace")
@@ -1219,6 +1268,411 @@ def print_example_config(name: str):
1219
1268
  sys.exit(1)
1220
1269
 
1221
1270
 
1271
+ def discover_available_configs() -> Dict[str, List[Tuple[str, Path]]]:
1272
+ """Discover all available configuration files.
1273
+
1274
+ Returns:
1275
+ Dict with categories as keys and list of (display_name, path) tuples as values
1276
+ """
1277
+ configs = {
1278
+ "User Configs": [],
1279
+ "Project Configs": [],
1280
+ "Current Directory": [],
1281
+ "Package Examples": [],
1282
+ }
1283
+
1284
+ # 1. User configs (~/.config/massgen/agents/)
1285
+ user_agents_dir = Path.home() / ".config/massgen/agents"
1286
+ if user_agents_dir.exists():
1287
+ for config_file in sorted(user_agents_dir.glob("*.yaml")):
1288
+ display_name = config_file.stem
1289
+ configs["User Configs"].append((display_name, config_file))
1290
+
1291
+ # 2. Project configs (.massgen/)
1292
+ project_config_dir = Path.cwd() / ".massgen"
1293
+ if project_config_dir.exists():
1294
+ for config_file in sorted(project_config_dir.glob("*.yaml")):
1295
+ display_name = f".massgen/{config_file.name}"
1296
+ configs["Project Configs"].append((display_name, config_file))
1297
+
1298
+ # 3. Current directory (*.yaml files, excluding .massgen/ and non-massgen configs)
1299
+ # Filter out common non-massgen YAML files
1300
+ exclude_patterns = {
1301
+ ".pre-commit-config.yaml",
1302
+ ".readthedocs.yaml",
1303
+ ".github",
1304
+ "docker-compose",
1305
+ "ansible",
1306
+ "kubernetes",
1307
+ }
1308
+
1309
+ for config_file in sorted(Path.cwd().glob("*.yaml")):
1310
+ # Skip if inside .massgen/ (already covered)
1311
+ if ".massgen" in str(config_file):
1312
+ continue
1313
+
1314
+ # Skip common non-massgen config files
1315
+ file_name = config_file.name.lower()
1316
+ if any(pattern in file_name for pattern in exclude_patterns):
1317
+ continue
1318
+
1319
+ display_name = config_file.name
1320
+ configs["Current Directory"].append((display_name, config_file))
1321
+
1322
+ # 4. Package examples (massgen/configs/)
1323
+ try:
1324
+ from importlib.resources import files
1325
+
1326
+ configs_root = files("massgen") / "configs"
1327
+
1328
+ # Organize by subdirectory
1329
+ for config_file in sorted(configs_root.rglob("*.yaml")):
1330
+ # Get relative path from configs root
1331
+ rel_path = str(config_file).replace(str(configs_root) + "/", "")
1332
+ # Skip README and docs
1333
+ if "README" in rel_path or "BACKEND_CONFIGURATION" in rel_path:
1334
+ continue
1335
+ # Use relative path as display name
1336
+ display_name = rel_path.replace(".yaml", "")
1337
+ configs["Package Examples"].append((display_name, Path(str(config_file))))
1338
+
1339
+ except Exception as e:
1340
+ logger.warning(f"Could not load package examples: {e}")
1341
+
1342
+ # Remove empty categories
1343
+ configs = {k: v for k, v in configs.items() if v}
1344
+
1345
+ return configs
1346
+
1347
+
1348
+ def interactive_config_selector() -> Optional[str]:
1349
+ """Interactively select a configuration file.
1350
+
1351
+ Shows user/project/current directory configs directly in a flat list.
1352
+ Package examples are shown hierarchically (category → config).
1353
+
1354
+ Returns:
1355
+ Path to selected config file, or None if cancelled
1356
+ """
1357
+ # Create console instance for rich output
1358
+ selector_console = Console()
1359
+
1360
+ # Discover all available configs
1361
+ configs = discover_available_configs()
1362
+
1363
+ if not configs:
1364
+ selector_console.print(
1365
+ "\n[yellow]⚠️ No configurations found![/yellow]",
1366
+ )
1367
+ selector_console.print("[dim]Create one with: massgen --init[/dim]\n")
1368
+ return None
1369
+
1370
+ # Create a summary table showing what's available
1371
+ summary_table = Table(
1372
+ show_header=True,
1373
+ header_style="bold bright_white",
1374
+ border_style="bright_black",
1375
+ box=None,
1376
+ padding=(0, 1),
1377
+ width=88,
1378
+ )
1379
+ summary_table.add_column("Category", style="bright_cyan", no_wrap=True, width=25)
1380
+ summary_table.add_column("Count", justify="center", style="bright_yellow", width=10)
1381
+ summary_table.add_column("Location", style="dim")
1382
+
1383
+ # Build summary and choices
1384
+ choices = []
1385
+
1386
+ # Build summary table (overview only - no duplication)
1387
+ # User configs
1388
+ if "User Configs" in configs and configs["User Configs"]:
1389
+ summary_table.add_row(
1390
+ "👤 Your Configs",
1391
+ str(len(configs["User Configs"])),
1392
+ "~/.config/massgen/agents/",
1393
+ )
1394
+ choices.append(questionary.Separator("\n─────────────────────────────────"))
1395
+ for display_name, path in configs["User Configs"]:
1396
+ choices.append(
1397
+ questionary.Choice(
1398
+ title=f" 👤 {display_name}",
1399
+ value=str(path),
1400
+ ),
1401
+ )
1402
+
1403
+ # Project configs
1404
+ if "Project Configs" in configs and configs["Project Configs"]:
1405
+ summary_table.add_row(
1406
+ "📁 Project Configs",
1407
+ str(len(configs["Project Configs"])),
1408
+ ".massgen/",
1409
+ )
1410
+ if choices:
1411
+ choices.append(questionary.Separator("\n─────────────────────────────────"))
1412
+ else:
1413
+ choices.append(questionary.Separator("\n─────────────────────────────────"))
1414
+ for display_name, path in configs["Project Configs"]:
1415
+ choices.append(
1416
+ questionary.Choice(
1417
+ title=f" 📁 {display_name}",
1418
+ value=str(path),
1419
+ ),
1420
+ )
1421
+
1422
+ # Current directory configs
1423
+ if "Current Directory" in configs and configs["Current Directory"]:
1424
+ summary_table.add_row(
1425
+ "📂 Current Directory",
1426
+ str(len(configs["Current Directory"])),
1427
+ f"*.yaml in {Path.cwd().name}/",
1428
+ )
1429
+ if choices:
1430
+ choices.append(questionary.Separator("\n─────────────────────────────────"))
1431
+ else:
1432
+ choices.append(questionary.Separator("\n─────────────────────────────────"))
1433
+ for display_name, path in configs["Current Directory"]:
1434
+ choices.append(
1435
+ questionary.Choice(
1436
+ title=f" 📂 {display_name}",
1437
+ value=str(path),
1438
+ ),
1439
+ )
1440
+
1441
+ # Package examples
1442
+ if "Package Examples" in configs and configs["Package Examples"]:
1443
+ summary_table.add_row(
1444
+ "📦 Package Examples",
1445
+ str(len(configs["Package Examples"])),
1446
+ "Built-in examples (hierarchical browser)",
1447
+ )
1448
+ if choices:
1449
+ choices.append(questionary.Separator("\n─────────────────────────────────"))
1450
+ choices.append(
1451
+ questionary.Choice(
1452
+ title=f" 📦 Browse {len(configs['Package Examples'])} example configs →",
1453
+ value="__browse_examples__",
1454
+ ),
1455
+ )
1456
+
1457
+ # Display summary table in a panel
1458
+ selector_console.print()
1459
+ selector_console.print(
1460
+ Panel(
1461
+ summary_table,
1462
+ title="[bold bright_cyan]🚀 Select a Configuration[/bold bright_cyan]",
1463
+ border_style="bright_cyan",
1464
+ padding=(0, 1),
1465
+ width=90,
1466
+ ),
1467
+ )
1468
+
1469
+ # Add cancel option
1470
+ choices.append(questionary.Separator("\n─────────────────────────────────"))
1471
+ choices.append(questionary.Choice(title=" ❌ Cancel", value="__cancel__"))
1472
+
1473
+ # Show the selector
1474
+ selector_console.print()
1475
+ selected = questionary.select(
1476
+ "Select a configuration:",
1477
+ choices=choices,
1478
+ use_shortcuts=True,
1479
+ use_arrow_keys=True,
1480
+ style=MASSGEN_QUESTIONARY_STYLE,
1481
+ pointer="▸",
1482
+ ).ask()
1483
+
1484
+ if selected is None or selected == "__cancel__":
1485
+ selector_console.print("\n[yellow]⚠️ Selection cancelled[/yellow]\n")
1486
+ return None
1487
+
1488
+ # If user wants to browse package examples, show hierarchical navigation
1489
+ if selected == "__browse_examples__":
1490
+ return _select_package_example(configs["Package Examples"], selector_console)
1491
+
1492
+ # Otherwise, return the selected config path
1493
+ selector_console.print(f"\n[bold green]✓ Selected:[/bold green] [cyan]{selected}[/cyan]\n")
1494
+ return selected
1495
+
1496
+
1497
+ def _select_package_example(examples: List[Tuple[str, Path]], console: Console) -> Optional[str]:
1498
+ """Show hierarchical navigation for package examples.
1499
+
1500
+ Args:
1501
+ examples: List of (display_name, path) tuples
1502
+ console: Rich console for output
1503
+
1504
+ Returns:
1505
+ Path to selected config, or None if cancelled/back
1506
+ """
1507
+ # Organize examples by category (first directory in path)
1508
+ categories = {}
1509
+ for display_name, path in examples:
1510
+ # Extract category from display name (e.g., "basic/multi/config" -> "basic")
1511
+ parts = display_name.split("/")
1512
+ category = parts[0] if len(parts) > 1 else "other"
1513
+
1514
+ if category not in categories:
1515
+ categories[category] = []
1516
+ categories[category].append((display_name, path))
1517
+
1518
+ # Emoji mapping for categories
1519
+ category_emojis = {
1520
+ "basic": "🎯",
1521
+ "tools": "🛠️",
1522
+ "providers": "🌐",
1523
+ "configs": "⚙️",
1524
+ "other": "📋",
1525
+ }
1526
+
1527
+ # Create category summary table
1528
+ category_table = Table(
1529
+ show_header=True,
1530
+ header_style="bold bright_white",
1531
+ border_style="bright_black",
1532
+ box=None,
1533
+ padding=(0, 1),
1534
+ width=88,
1535
+ )
1536
+ category_table.add_column("Category", style="bright_cyan", no_wrap=True, width=20)
1537
+ category_table.add_column("Count", justify="center", style="bright_yellow", width=10)
1538
+ category_table.add_column("Description", style="dim")
1539
+
1540
+ # Category descriptions
1541
+ category_descriptions = {
1542
+ "basic": "Simple configurations for getting started",
1543
+ "tools": "Configs demonstrating tool integrations",
1544
+ "providers": "Provider-specific example configs",
1545
+ "configs": "Advanced configuration examples",
1546
+ "other": "Miscellaneous configurations",
1547
+ }
1548
+
1549
+ # Build category table and choices
1550
+ category_choices = []
1551
+ for category in sorted(categories.keys()):
1552
+ count = len(categories[category])
1553
+ emoji = category_emojis.get(category, "📁")
1554
+ description = category_descriptions.get(category, "Example configurations")
1555
+
1556
+ category_table.add_row(
1557
+ f"{emoji} {category.title()}",
1558
+ str(count),
1559
+ description,
1560
+ )
1561
+
1562
+ category_choices.append(
1563
+ questionary.Choice(
1564
+ title=f" {emoji} {category.title()} ({count} config{'s' if count != 1 else ''})",
1565
+ value=category,
1566
+ ),
1567
+ )
1568
+
1569
+ # Display category summary in a panel
1570
+ console.print()
1571
+ console.print(
1572
+ Panel(
1573
+ category_table,
1574
+ title="[bold bright_yellow]📦 Package Examples - Select Category[/bold bright_yellow]",
1575
+ border_style="bright_yellow",
1576
+ padding=(0, 1),
1577
+ width=90,
1578
+ ),
1579
+ )
1580
+
1581
+ # Add back option
1582
+ category_choices.append(questionary.Separator("\n─────────────────────────────────"))
1583
+ category_choices.append(questionary.Choice(title=" ← Back to main menu", value="__back__"))
1584
+
1585
+ # Step 1: Select category
1586
+ console.print()
1587
+ selected_category = questionary.select(
1588
+ "Select a category:",
1589
+ choices=category_choices,
1590
+ use_shortcuts=True,
1591
+ use_arrow_keys=True,
1592
+ style=MASSGEN_QUESTIONARY_STYLE,
1593
+ pointer="▸",
1594
+ ).ask()
1595
+
1596
+ if selected_category is None or selected_category == "__cancel__":
1597
+ console.print("\n[yellow]⚠️ Selection cancelled[/yellow]\n")
1598
+ return None
1599
+
1600
+ if selected_category == "__back__":
1601
+ # Go back to main selector
1602
+ return interactive_config_selector()
1603
+
1604
+ # Create configs table
1605
+ emoji = category_emojis.get(selected_category, "📁")
1606
+ configs_table = Table(
1607
+ show_header=True,
1608
+ header_style="bold bright_white",
1609
+ border_style="bright_black",
1610
+ box=None,
1611
+ padding=(0, 1),
1612
+ width=88,
1613
+ )
1614
+ configs_table.add_column("#", style="dim", width=5, justify="right")
1615
+ configs_table.add_column("Configuration", style="bright_cyan")
1616
+
1617
+ # Build config choices and table
1618
+ config_choices = []
1619
+ for idx, (display_name, path) in enumerate(sorted(categories[selected_category]), 1):
1620
+ # Show relative path within category
1621
+ short_name = display_name.replace(f"{selected_category}/", "")
1622
+ configs_table.add_row(str(idx), short_name)
1623
+ config_choices.append(
1624
+ questionary.Choice(
1625
+ title=f" {idx:2d}. {short_name}",
1626
+ value=str(path),
1627
+ ),
1628
+ )
1629
+
1630
+ # Display configs in a panel
1631
+ console.print()
1632
+ console.print(
1633
+ Panel(
1634
+ configs_table,
1635
+ title=f"[bold bright_green]{emoji} {selected_category.title()} Configurations[/bold bright_green]",
1636
+ border_style="bright_green",
1637
+ padding=(0, 1),
1638
+ width=90,
1639
+ ),
1640
+ )
1641
+
1642
+ # Add back option
1643
+ config_choices.append(questionary.Separator("\n─────────────────────────────────"))
1644
+ config_choices.append(questionary.Choice(title=" ← Back to categories", value="__back__"))
1645
+
1646
+ # Step 2: Select config
1647
+ # For large lists: disable shortcuts (max 36) and enable search filter for better UX
1648
+ # Note: When search filter is enabled, j/k keys must be disabled (they conflict with search)
1649
+ use_shortcuts = len(config_choices) <= 36
1650
+ use_search_filter = len(config_choices) > 36
1651
+ console.print()
1652
+ selected_config = questionary.select(
1653
+ "Select a configuration:",
1654
+ choices=config_choices,
1655
+ use_shortcuts=use_shortcuts,
1656
+ use_arrow_keys=True,
1657
+ use_search_filter=use_search_filter,
1658
+ use_jk_keys=not use_search_filter,
1659
+ style=MASSGEN_QUESTIONARY_STYLE,
1660
+ pointer="▸",
1661
+ ).ask()
1662
+
1663
+ if selected_config is None or selected_config == "__cancel__":
1664
+ console.print("\n[yellow]⚠️ Selection cancelled[/yellow]\n")
1665
+ return None
1666
+
1667
+ if selected_config == "__back__":
1668
+ # Recursively call to go back to category selection
1669
+ return _select_package_example(examples, console)
1670
+
1671
+ # Return the selected config path
1672
+ console.print(f"\n[bold green]✓ Selected:[/bold green] [cyan]{selected_config}[/cyan]\n")
1673
+ return selected_config
1674
+
1675
+
1222
1676
  def should_run_builder() -> bool:
1223
1677
  """Check if config builder should run automatically.
1224
1678
 
@@ -1511,6 +1965,14 @@ async def run_interactive_mode(
1511
1965
  setup_logging(debug=_DEBUG_MODE, turn=next_turn)
1512
1966
  logger.info(f"Starting turn {next_turn}")
1513
1967
 
1968
+ # Save execution metadata for this turn (original_config already has pre-relocation paths)
1969
+ save_execution_metadata(
1970
+ query=question,
1971
+ config_path=config_path,
1972
+ config_content=original_config, # This is the pre-relocation config passed from main()
1973
+ cli_args={"mode": "interactive", "turn": next_turn, "session_id": session_id},
1974
+ )
1975
+
1514
1976
  # Pass session state for multi-turn filesystem support
1515
1977
  session_info = {
1516
1978
  "session_id": session_id,
@@ -1612,6 +2074,9 @@ async def main(args):
1612
2074
  logger.debug(f"Created simple config with backend: {backend}, model: {model}")
1613
2075
  logger.debug(f"Config content: {json.dumps(config, indent=2)}")
1614
2076
 
2077
+ # Save original config before relocation (for execution_metadata.yaml)
2078
+ original_config_for_metadata = copy.deepcopy(config)
2079
+
1615
2080
  # Validate that all context paths exist before proceeding
1616
2081
  validate_context_paths(config)
1617
2082
 
@@ -1695,6 +2160,16 @@ async def main(args):
1695
2160
  if "orchestrator" in config:
1696
2161
  kwargs["orchestrator"] = config["orchestrator"]
1697
2162
 
2163
+ # Save execution metadata for debugging and reconstruction
2164
+ if args.question:
2165
+ # For single question mode, save metadata now (use original config before .massgen/ relocation)
2166
+ save_execution_metadata(
2167
+ query=args.question,
2168
+ config_path=str(resolved_path) if args.config and "resolved_path" in locals() else None,
2169
+ config_content=original_config_for_metadata,
2170
+ cli_args=vars(args),
2171
+ )
2172
+
1698
2173
  # Run mode based on whether question was provided
1699
2174
  try:
1700
2175
  if args.question:
@@ -1734,23 +2209,27 @@ def cli_main():
1734
2209
  epilog="""
1735
2210
  Examples:
1736
2211
  # Use configuration file
1737
- python -m massgen.cli --config config.yaml "What is machine learning?"
2212
+ massgen --config config.yaml "What is machine learning?"
1738
2213
 
1739
2214
  # 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"
2215
+ massgen --backend openai --model gpt-4o-mini "Explain quantum computing"
2216
+ massgen --backend claude --model claude-sonnet-4-20250514 "Analyze this data"
1742
2217
 
1743
2218
  # 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?"
2219
+ massgen --backend chatcompletion --model gpt-oss-120b --base-url https://api.cerebras.ai/v1/chat/completions "What is 2+2?"
1745
2220
 
1746
2221
  # Interactive mode
1747
- python -m massgen.cli --config config.yaml
2222
+ massgen --config config.yaml
2223
+ massgen # Uses default config if available
1748
2224
 
1749
2225
  # Timeout control examples
1750
- python -m massgen.cli --config config.yaml --orchestrator-timeout 600 "Complex task"
2226
+ massgen --config config.yaml --orchestrator-timeout 600 "Complex task"
1751
2227
 
1752
- # Create sample configurations
1753
- python -m massgen.cli --create-samples
2228
+ # Configuration management
2229
+ massgen --init # Create new configuration interactively
2230
+ massgen --select # Choose from available configurations
2231
+ massgen --setup # Set up API keys
2232
+ massgen --list-examples # View example configurations
1754
2233
 
1755
2234
  Environment Variables:
1756
2235
  OPENAI_API_KEY - Required for OpenAI backend
@@ -1782,6 +2261,11 @@ Environment Variables:
1782
2261
  # Configuration options
1783
2262
  config_group = parser.add_mutually_exclusive_group()
1784
2263
  config_group.add_argument("--config", type=str, help="Path to YAML/JSON configuration file or @examples/NAME")
2264
+ config_group.add_argument(
2265
+ "--select",
2266
+ action="store_true",
2267
+ help="Interactively select from available configurations",
2268
+ )
1785
2269
  config_group.add_argument(
1786
2270
  "--backend",
1787
2271
  type=str,
@@ -1825,7 +2309,7 @@ Environment Variables:
1825
2309
  help="Launch interactive configuration builder to create config file",
1826
2310
  )
1827
2311
  parser.add_argument(
1828
- "--setup-keys",
2312
+ "--setup",
1829
2313
  action="store_true",
1830
2314
  help="Launch interactive API key setup wizard to configure credentials",
1831
2315
  )
@@ -1888,7 +2372,7 @@ Environment Variables:
1888
2372
  return
1889
2373
 
1890
2374
  # Launch interactive API key setup if requested
1891
- if args.setup_keys:
2375
+ if args.setup:
1892
2376
  from .config_builder import ConfigBuilder
1893
2377
 
1894
2378
  builder = ConfigBuilder()
@@ -1899,9 +2383,20 @@ Environment Variables:
1899
2383
  print(f"{BRIGHT_CYAN}💡 You can now use MassGen with these providers{RESET}\n")
1900
2384
  else:
1901
2385
  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")
2386
+ print(f"{BRIGHT_CYAN}💡 You can run 'massgen --setup' anytime to set them up{RESET}\n")
1903
2387
  return
1904
2388
 
2389
+ # Launch interactive config selector if requested
2390
+ if args.select:
2391
+ selected_config = interactive_config_selector()
2392
+ if selected_config:
2393
+ # Update args to use the selected config
2394
+ args.config = selected_config
2395
+ # Continue to main() with the selected config
2396
+ else:
2397
+ # User cancelled selection
2398
+ return
2399
+
1905
2400
  # Launch interactive config builder if requested
1906
2401
  if args.init:
1907
2402
  from .config_builder import ConfigBuilder
@@ -1918,7 +2413,7 @@ Environment Variables:
1918
2413
  elif filepath:
1919
2414
  # Config created but user chose not to run
1920
2415
  print(f"\n✅ Configuration saved to: {filepath}")
1921
- print(f'Run with: python -m massgen.cli --config {filepath} "Your question"')
2416
+ print(f'Run with: massgen --config {filepath} "Your question"')
1922
2417
  return
1923
2418
  else:
1924
2419
  # User cancelled