massgen 0.1.0a2__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 +643 -132
  20. massgen/config_builder.py +381 -32
  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.0a2.dist-info → massgen-0.1.1.dist-info}/METADATA +89 -131
  107. {massgen-0.1.0a2.dist-info → massgen-0.1.1.dist-info}/RECORD +111 -36
  108. {massgen-0.1.0a2.dist-info → massgen-0.1.1.dist-info}/WHEEL +0 -0
  109. {massgen-0.1.0a2.dist-info → massgen-0.1.1.dist-info}/entry_points.txt +0 -0
  110. {massgen-0.1.0a2.dist-info → massgen-0.1.1.dist-info}/licenses/LICENSE +0 -0
  111. {massgen-0.1.0a2.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."""
@@ -291,29 +311,33 @@ def create_backend(backend_type: str, **kwargs) -> Any:
291
311
  if backend_type == "openai":
292
312
  api_key = kwargs.get("api_key") or os.getenv("OPENAI_API_KEY")
293
313
  if not api_key:
294
- print("āš ļø Warning: OpenAI API key not found. Set OPENAI_API_KEY environment variable or add to .env file.", flush=True)
295
- print(" .env file locations: current directory, or ~/.massgen/.env", flush=True)
314
+ raise ConfigurationError(
315
+ "OpenAI API key not found. Set OPENAI_API_KEY environment variable.\n" "You can add it to a .env file in:\n" " - Current directory: .env\n" " - Global config: ~/.massgen/.env",
316
+ )
296
317
  return ResponseBackend(api_key=api_key, **kwargs)
297
318
 
298
319
  elif backend_type == "grok":
299
320
  api_key = kwargs.get("api_key") or os.getenv("XAI_API_KEY")
300
321
  if not api_key:
301
- print("āš ļø Warning: Grok API key not found. Set XAI_API_KEY environment variable or add to .env file.", flush=True)
302
- print(" .env file locations: current directory, or ~/.massgen/.env", flush=True)
322
+ raise ConfigurationError(
323
+ "Grok API key not found. Set XAI_API_KEY environment variable.\n" "You can add it to a .env file in:\n" " - Current directory: .env\n" " - Global config: ~/.massgen/.env",
324
+ )
303
325
  return GrokBackend(api_key=api_key, **kwargs)
304
326
 
305
327
  elif backend_type == "claude":
306
328
  api_key = kwargs.get("api_key") or os.getenv("ANTHROPIC_API_KEY")
307
329
  if not api_key:
308
- print("āš ļø Warning: Claude API key not found. Set ANTHROPIC_API_KEY environment variable or add to .env file.", flush=True)
309
- print(" .env file locations: current directory, or ~/.massgen/.env", flush=True)
330
+ raise ConfigurationError(
331
+ "Claude API key not found. Set ANTHROPIC_API_KEY environment variable.\n" "You can add it to a .env file in:\n" " - Current directory: .env\n" " - Global config: ~/.massgen/.env",
332
+ )
310
333
  return ClaudeBackend(api_key=api_key, **kwargs)
311
334
 
312
335
  elif backend_type == "gemini":
313
336
  api_key = kwargs.get("api_key") or os.getenv("GOOGLE_API_KEY") or os.getenv("GEMINI_API_KEY")
314
337
  if not api_key:
315
- print("āš ļø Warning: Gemini API key not found. Set GOOGLE_API_KEY environment variable or add to .env file.", flush=True)
316
- print(" .env file locations: current directory, or ~/.massgen/.env", flush=True)
338
+ raise ConfigurationError(
339
+ "Gemini API key not found. Set GOOGLE_API_KEY environment variable.\n" "You can add it to a .env file in:\n" " - Current directory: .env\n" " - Global config: ~/.massgen/.env",
340
+ )
317
341
  return GeminiBackend(api_key=api_key, **kwargs)
318
342
 
319
343
  elif backend_type == "chatcompletion":
@@ -325,43 +349,81 @@ def create_backend(backend_type: str, **kwargs) -> Any:
325
349
  if base_url and "cerebras.ai" in base_url:
326
350
  api_key = os.getenv("CEREBRAS_API_KEY")
327
351
  if not api_key:
328
- raise ConfigurationError("Cerebras AI API key not found. Set CEREBRAS_API_KEY or provide in config.")
352
+ raise ConfigurationError(
353
+ "Cerebras AI API key not found. Set CEREBRAS_API_KEY environment variable.\n"
354
+ "You can add it to a .env file in:\n"
355
+ " - Current directory: .env\n"
356
+ " - Global config: ~/.massgen/.env",
357
+ )
329
358
  elif base_url and "together.xyz" in base_url:
330
359
  api_key = os.getenv("TOGETHER_API_KEY")
331
360
  if not api_key:
332
- raise ConfigurationError("Together AI API key not found. Set TOGETHER_API_KEY or provide in config.")
361
+ raise ConfigurationError(
362
+ "Together AI API key not found. Set TOGETHER_API_KEY environment variable.\n"
363
+ "You can add it to a .env file in:\n"
364
+ " - Current directory: .env\n"
365
+ " - Global config: ~/.massgen/.env",
366
+ )
333
367
  elif base_url and "fireworks.ai" in base_url:
334
368
  api_key = os.getenv("FIREWORKS_API_KEY")
335
369
  if not api_key:
336
- raise ConfigurationError("Fireworks AI API key not found. Set FIREWORKS_API_KEY or provide in config.")
370
+ raise ConfigurationError(
371
+ "Fireworks AI API key not found. Set FIREWORKS_API_KEY environment variable.\n"
372
+ "You can add it to a .env file in:\n"
373
+ " - Current directory: .env\n"
374
+ " - Global config: ~/.massgen/.env",
375
+ )
337
376
  elif base_url and "groq.com" in base_url:
338
377
  api_key = os.getenv("GROQ_API_KEY")
339
378
  if not api_key:
340
- raise ConfigurationError("Groq API key not found. Set GROQ_API_KEY or provide in config.")
379
+ raise ConfigurationError(
380
+ "Groq API key not found. Set GROQ_API_KEY environment variable.\n" "You can add it to a .env file in:\n" " - Current directory: .env\n" " - Global config: ~/.massgen/.env",
381
+ )
341
382
  elif base_url and "nebius.com" in base_url:
342
383
  api_key = os.getenv("NEBIUS_API_KEY")
343
384
  if not api_key:
344
- raise ConfigurationError("Nebius AI Studio API key not found. Set NEBIUS_API_KEY or provide in config.")
385
+ raise ConfigurationError(
386
+ "Nebius AI Studio API key not found. Set NEBIUS_API_KEY environment variable.\n"
387
+ "You can add it to a .env file in:\n"
388
+ " - Current directory: .env\n"
389
+ " - Global config: ~/.massgen/.env",
390
+ )
345
391
  elif base_url and "openrouter.ai" in base_url:
346
392
  api_key = os.getenv("OPENROUTER_API_KEY")
347
393
  if not api_key:
348
- raise ConfigurationError("OpenRouter API key not found. Set OPENROUTER_API_KEY or provide in config.")
394
+ raise ConfigurationError(
395
+ "OpenRouter API key not found. Set OPENROUTER_API_KEY environment variable.\n"
396
+ "You can add it to a .env file in:\n"
397
+ " - Current directory: .env\n"
398
+ " - Global config: ~/.massgen/.env",
399
+ )
349
400
  elif base_url and ("z.ai" in base_url or "bigmodel.cn" in base_url):
350
401
  api_key = os.getenv("ZAI_API_KEY")
351
402
  if not api_key:
352
- raise ConfigurationError("ZAI API key not found. Set ZAI_API_KEY or provide in config.")
403
+ raise ConfigurationError(
404
+ "ZAI API key not found. Set ZAI_API_KEY environment variable.\n" "You can add it to a .env file in:\n" " - Current directory: .env\n" " - Global config: ~/.massgen/.env",
405
+ )
353
406
  elif base_url and ("moonshot.ai" in base_url or "moonshot.cn" in base_url):
354
407
  api_key = os.getenv("MOONSHOT_API_KEY") or os.getenv("KIMI_API_KEY")
355
408
  if not api_key:
356
- raise ConfigurationError("Kimi/Moonshot API key not found. Set MOONSHOT_API_KEY or KIMI_API_KEY or provide in config.")
409
+ raise ConfigurationError(
410
+ "Kimi/Moonshot API key not found. Set MOONSHOT_API_KEY or KIMI_API_KEY environment variable.\n"
411
+ "You can add it to a .env file in:\n"
412
+ " - Current directory: .env\n"
413
+ " - Global config: ~/.massgen/.env",
414
+ )
357
415
  elif base_url and "poe.com" in base_url:
358
416
  api_key = os.getenv("POE_API_KEY")
359
417
  if not api_key:
360
- raise ConfigurationError("POE API key not found. Set POE_API_KEY or provide in config.")
418
+ raise ConfigurationError(
419
+ "POE API key not found. Set POE_API_KEY environment variable.\n" "You can add it to a .env file in:\n" " - Current directory: .env\n" " - Global config: ~/.massgen/.env",
420
+ )
361
421
  elif base_url and "aliyuncs.com" in base_url:
362
422
  api_key = os.getenv("QWEN_API_KEY")
363
423
  if not api_key:
364
- raise ConfigurationError("Qwen API key not found. Set QWEN_API_KEY or provide in config.")
424
+ raise ConfigurationError(
425
+ "Qwen API key not found. Set QWEN_API_KEY environment variable.\n" "You can add it to a .env file in:\n" " - Current directory: .env\n" " - Global config: ~/.massgen/.env",
426
+ )
365
427
 
366
428
  return ChatCompletionsBackend(api_key=api_key, **kwargs)
367
429
 
@@ -370,7 +432,9 @@ def create_backend(backend_type: str, **kwargs) -> Any:
370
432
  # Supports both global (z.ai) and China (bigmodel.cn) endpoints
371
433
  api_key = kwargs.get("api_key") or os.getenv("ZAI_API_KEY")
372
434
  if not api_key:
373
- raise ConfigurationError("ZAI API key not found. Set ZAI_API_KEY or provide in config.")
435
+ raise ConfigurationError(
436
+ "ZAI API key not found. Set ZAI_API_KEY environment variable.\n" "You can add it to a .env file in:\n" " - Current directory: .env\n" " - Global config: ~/.massgen/.env",
437
+ )
374
438
  return ChatCompletionsBackend(api_key=api_key, **kwargs)
375
439
 
376
440
  elif backend_type == "lmstudio":
@@ -758,105 +822,94 @@ async def run_question_with_history(
758
822
  messages = history.copy()
759
823
  messages.append({"role": "user", "content": question})
760
824
 
761
- # Check if we should use orchestrator for single agents (default: False for backward compatibility)
762
- use_orchestrator_for_single = ui_config.get("use_orchestrator_for_single_agent", True)
763
-
764
- if len(agents) == 1 and not use_orchestrator_for_single:
765
- # Single agent mode with history
766
- agent = next(iter(agents.values()))
767
- print(f"\nšŸ¤– {BRIGHT_CYAN}Single Agent Mode{RESET}", flush=True)
768
- print(f"Agent: {agent.agent_id}", flush=True)
769
- if history:
770
- print(f"History: {len(history)//2} previous exchanges", flush=True)
771
- print(f"Question: {question}", flush=True)
772
- print("\n" + "=" * 60, flush=True)
773
-
774
- response_content = ""
775
-
776
- async for chunk in agent.chat(messages):
777
- if chunk.type == "content" and chunk.content:
778
- response_content += chunk.content
779
- print(chunk.content, end="", flush=True)
780
- elif chunk.type == "builtin_tool_results":
781
- # Skip builtin_tool_results to avoid duplication with real-time streaming
782
- # The backends already show tool status during execution
783
- continue
784
- elif chunk.type == "error":
785
- print(f"\nāŒ Error: {chunk.error}", flush=True)
786
- return ("", session_info.get("session_id"), session_info.get("current_turn", 0))
787
-
788
- print("\n" + "=" * 60, flush=True)
789
- # Single agent mode doesn't use session storage
790
- 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
+ )
791
875
 
876
+ # Determine display mode text
877
+ if len(agents) == 1:
878
+ mode_text = "Single Agent (Orchestrator)"
792
879
  else:
793
- # Multi-agent mode with history
794
- # Create orchestrator config with timeout settings
795
- timeout_config = kwargs.get("timeout_config")
796
- orchestrator_config = AgentConfig()
797
- if timeout_config:
798
- orchestrator_config.timeout_config = timeout_config
880
+ mode_text = "Multi-Agent"
799
881
 
800
- # Get orchestrator parameters from config
801
- orchestrator_cfg = kwargs.get("orchestrator", {})
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)
802
888
 
803
- # Get context sharing parameters
804
- snapshot_storage = orchestrator_cfg.get("snapshot_storage")
805
- agent_temporary_workspace = orchestrator_cfg.get("agent_temporary_workspace")
806
- session_storage = orchestrator_cfg.get("session_storage", "sessions") # Default to "sessions"
889
+ # For multi-agent with history, we need to use a different approach
890
+ # that maintains coordination UI display while supporting conversation context
807
891
 
808
- # Get debug/test parameters
809
- if orchestrator_cfg.get("skip_coordination_rounds", False):
810
- orchestrator_config.skip_coordination_rounds = True
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
811
896
 
812
- # Load previous turns from session storage for multi-turn conversations
813
- previous_turns = load_previous_turns(session_info, session_storage)
814
-
815
- orchestrator = Orchestrator(
816
- agents=agents,
817
- config=orchestrator_config,
818
- snapshot_storage=snapshot_storage,
819
- agent_temporary_workspace=agent_temporary_workspace,
820
- previous_turns=previous_turns,
821
- )
822
- # Create a fresh UI instance for each question to ensure clean state
823
- ui = CoordinationUI(
824
- display_type=ui_config.get("display_type", "rich_terminal"),
825
- logging_enabled=ui_config.get("logging_enabled", True),
826
- enable_final_presentation=True, # Required for multi-turn: ensures final answer is saved
827
- )
828
-
829
- print(f"\nšŸ¤– {BRIGHT_CYAN}Multi-Agent Mode{RESET}", flush=True)
830
- print(f"Agents: {', '.join(agents.keys())}", flush=True)
831
- if history:
832
- print(f"History: {len(history)//2} previous exchanges", flush=True)
833
- print(f"Question: {question}", flush=True)
834
- print("\n" + "=" * 60, flush=True)
835
-
836
- # For multi-agent with history, we need to use a different approach
837
- # that maintains coordination UI display while supporting conversation context
838
-
839
- if history and len(history) > 0:
840
- # Use coordination UI with conversation context
841
- # Extract current question from messages
842
- current_question = messages[-1].get("content", question) if messages else question
843
-
844
- # Pass the full message context to the UI coordination
845
- response_content = await ui.coordinate_with_context(orchestrator, current_question, messages)
846
- else:
847
- # Standard coordination for new conversations
848
- response_content = await ui.coordinate(orchestrator, question)
849
-
850
- # Handle session persistence if applicable
851
- session_id_to_use, updated_turn, normalized_response = await handle_session_persistence(
852
- orchestrator,
853
- question,
854
- session_info,
855
- session_storage,
856
- )
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
+ )
857
910
 
858
- # Return normalized response so conversation history has correct paths
859
- 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)
860
913
 
861
914
 
862
915
  async def run_single_question(question: str, agents: Dict[str, SingleAgent], ui_config: Dict[str, Any], **kwargs) -> str:
@@ -901,6 +954,18 @@ async def run_single_question(question: str, agents: Dict[str, SingleAgent], ui_
901
954
  # Get orchestrator parameters from config
902
955
  orchestrator_cfg = kwargs.get("orchestrator", {})
903
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
+
904
969
  # Get context sharing parameters
905
970
  snapshot_storage = orchestrator_cfg.get("snapshot_storage")
906
971
  agent_temporary_workspace = orchestrator_cfg.get("agent_temporary_workspace")
@@ -1175,6 +1240,411 @@ def print_example_config(name: str):
1175
1240
  sys.exit(1)
1176
1241
 
1177
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
+
1178
1648
  def should_run_builder() -> bool:
1179
1649
  """Check if config builder should run automatically.
1180
1650
 
@@ -1219,13 +1689,13 @@ async def run_interactive_mode(
1219
1689
  rich_console.clear()
1220
1690
 
1221
1691
  # ASCII art for interactive multi-agent mode
1222
- ascii_art = """[bold cyan]
1692
+ ascii_art = """[bold #4A90E2]
1223
1693
  ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•—
1224
1694
  ā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•”ā•ā•ā•ā•ā•ā–ˆā–ˆā•”ā•ā•ā•ā•ā•ā–ˆā–ˆā•”ā•ā•ā•ā•ā• ā–ˆā–ˆā•”ā•ā•ā•ā•ā•ā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•‘
1225
1695
  ā–ˆā–ˆā•”ā–ˆā–ˆā–ˆā–ˆā•”ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•‘ ā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•”ā–ˆā–ˆā•— ā–ˆā–ˆā•‘
1226
1696
  ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā•”ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•‘ā•šā•ā•ā•ā•ā–ˆā–ˆā•‘ā•šā•ā•ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā• ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā•—ā–ˆā–ˆā•‘
1227
1697
  ā–ˆā–ˆā•‘ ā•šā•ā• ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘ā•šā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•‘ ā•šā–ˆā–ˆā–ˆā–ˆā•‘
1228
- ā•šā•ā• ā•šā•ā•ā•šā•ā• ā•šā•ā•ā•šā•ā•ā•ā•ā•ā•ā•ā•šā•ā•ā•ā•ā•ā•ā• ā•šā•ā•ā•ā•ā•ā• ā•šā•ā•ā•ā•ā•ā•ā•ā•šā•ā• ā•šā•ā•ā•ā•[/bold cyan]
1698
+ ā•šā•ā• ā•šā•ā•ā•šā•ā• ā•šā•ā•ā•šā•ā•ā•ā•ā•ā•ā•ā•šā•ā•ā•ā•ā•ā•ā• ā•šā•ā•ā•ā•ā•ā• ā•šā•ā•ā•ā•ā•ā•ā•ā•šā•ā• ā•šā•ā•ā•ā•[/bold #4A90E2]
1229
1699
 
1230
1700
  [dim] šŸ¤– šŸ¤– šŸ¤– → šŸ’¬ collaborate → šŸŽÆ winner → šŸ“¢ final[/dim]
1231
1701
  """
@@ -1233,7 +1703,7 @@ async def run_interactive_mode(
1233
1703
  # Wrap ASCII art in a panel
1234
1704
  ascii_panel = Panel(
1235
1705
  ascii_art,
1236
- border_style="bold cyan",
1706
+ border_style="bold #4A90E2",
1237
1707
  padding=(0, 2),
1238
1708
  width=80,
1239
1709
  )
@@ -1467,6 +1937,14 @@ async def run_interactive_mode(
1467
1937
  setup_logging(debug=_DEBUG_MODE, turn=next_turn)
1468
1938
  logger.info(f"Starting turn {next_turn}")
1469
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
+
1470
1948
  # Pass session state for multi-turn filesystem support
1471
1949
  session_info = {
1472
1950
  "session_id": session_id,
@@ -1568,6 +2046,9 @@ async def main(args):
1568
2046
  logger.debug(f"Created simple config with backend: {backend}, model: {model}")
1569
2047
  logger.debug(f"Config content: {json.dumps(config, indent=2)}")
1570
2048
 
2049
+ # Save original config before relocation (for execution_metadata.yaml)
2050
+ original_config_for_metadata = copy.deepcopy(config)
2051
+
1571
2052
  # Validate that all context paths exist before proceeding
1572
2053
  validate_context_paths(config)
1573
2054
 
@@ -1651,6 +2132,16 @@ async def main(args):
1651
2132
  if "orchestrator" in config:
1652
2133
  kwargs["orchestrator"] = config["orchestrator"]
1653
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
+
1654
2145
  # Run mode based on whether question was provided
1655
2146
  try:
1656
2147
  if args.question:
@@ -1690,23 +2181,27 @@ def cli_main():
1690
2181
  epilog="""
1691
2182
  Examples:
1692
2183
  # Use configuration file
1693
- python -m massgen.cli --config config.yaml "What is machine learning?"
2184
+ massgen --config config.yaml "What is machine learning?"
1694
2185
 
1695
2186
  # Quick single agent setup
1696
- python -m massgen.cli --backend openai --model gpt-4o-mini "Explain quantum computing"
1697
- 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"
1698
2189
 
1699
2190
  # Use ChatCompletion backend with custom base URL
1700
- 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?"
1701
2192
 
1702
2193
  # Interactive mode
1703
- python -m massgen.cli --config config.yaml
2194
+ massgen --config config.yaml
2195
+ massgen # Uses default config if available
1704
2196
 
1705
2197
  # Timeout control examples
1706
- python -m massgen.cli --config config.yaml --orchestrator-timeout 600 "Complex task"
2198
+ massgen --config config.yaml --orchestrator-timeout 600 "Complex task"
1707
2199
 
1708
- # Create sample configurations
1709
- 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
1710
2205
 
1711
2206
  Environment Variables:
1712
2207
  OPENAI_API_KEY - Required for OpenAI backend
@@ -1738,6 +2233,11 @@ Environment Variables:
1738
2233
  # Configuration options
1739
2234
  config_group = parser.add_mutually_exclusive_group()
1740
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
+ )
1741
2241
  config_group.add_argument(
1742
2242
  "--backend",
1743
2243
  type=str,
@@ -1781,7 +2281,7 @@ Environment Variables:
1781
2281
  help="Launch interactive configuration builder to create config file",
1782
2282
  )
1783
2283
  parser.add_argument(
1784
- "--setup-keys",
2284
+ "--setup",
1785
2285
  action="store_true",
1786
2286
  help="Launch interactive API key setup wizard to configure credentials",
1787
2287
  )
@@ -1844,7 +2344,7 @@ Environment Variables:
1844
2344
  return
1845
2345
 
1846
2346
  # Launch interactive API key setup if requested
1847
- if args.setup_keys:
2347
+ if args.setup:
1848
2348
  from .config_builder import ConfigBuilder
1849
2349
 
1850
2350
  builder = ConfigBuilder()
@@ -1855,9 +2355,20 @@ Environment Variables:
1855
2355
  print(f"{BRIGHT_CYAN}šŸ’” You can now use MassGen with these providers{RESET}\n")
1856
2356
  else:
1857
2357
  print(f"\n{BRIGHT_YELLOW}āš ļø No API keys configured{RESET}")
1858
- 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")
1859
2359
  return
1860
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
+
1861
2372
  # Launch interactive config builder if requested
1862
2373
  if args.init:
1863
2374
  from .config_builder import ConfigBuilder
@@ -1874,7 +2385,7 @@ Environment Variables:
1874
2385
  elif filepath:
1875
2386
  # Config created but user chose not to run
1876
2387
  print(f"\nāœ… Configuration saved to: {filepath}")
1877
- print(f'Run with: python -m massgen.cli --config {filepath} "Your question"')
2388
+ print(f'Run with: massgen --config {filepath} "Your question"')
1878
2389
  return
1879
2390
  else:
1880
2391
  # User cancelled