massgen 0.1.1__py3-none-any.whl → 0.1.3__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.
Files changed (77) hide show
  1. massgen/__init__.py +1 -1
  2. massgen/agent_config.py +33 -7
  3. massgen/api_params_handler/_api_params_handler_base.py +3 -0
  4. massgen/api_params_handler/_chat_completions_api_params_handler.py +7 -1
  5. massgen/backend/azure_openai.py +9 -1
  6. massgen/backend/base.py +56 -0
  7. massgen/backend/base_with_custom_tool_and_mcp.py +4 -4
  8. massgen/backend/capabilities.py +6 -6
  9. massgen/backend/chat_completions.py +18 -11
  10. massgen/backend/claude_code.py +9 -1
  11. massgen/backend/gemini.py +71 -6
  12. massgen/backend/gemini_utils.py +30 -0
  13. massgen/backend/grok.py +39 -6
  14. massgen/backend/response.py +18 -11
  15. massgen/chat_agent.py +9 -3
  16. massgen/cli.py +319 -43
  17. massgen/config_builder.py +163 -18
  18. massgen/configs/README.md +78 -20
  19. massgen/configs/basic/multi/three_agents_default.yaml +2 -2
  20. massgen/configs/debug/restart_test_controlled.yaml +60 -0
  21. massgen/configs/debug/restart_test_controlled_filesystem.yaml +73 -0
  22. massgen/configs/tools/code-execution/docker_with_sudo.yaml +35 -0
  23. massgen/configs/tools/custom_tools/computer_use_browser_example.yaml +56 -0
  24. massgen/configs/tools/custom_tools/computer_use_docker_example.yaml +65 -0
  25. massgen/configs/tools/custom_tools/computer_use_example.yaml +50 -0
  26. massgen/configs/tools/custom_tools/crawl4ai_mcp_example.yaml +67 -0
  27. massgen/configs/tools/custom_tools/crawl4ai_multi_agent_example.yaml +68 -0
  28. massgen/configs/tools/custom_tools/multimodal_tools/playwright_with_img_understanding.yaml +98 -0
  29. massgen/configs/tools/custom_tools/multimodal_tools/understand_audio.yaml +33 -0
  30. massgen/configs/tools/custom_tools/multimodal_tools/understand_file.yaml +34 -0
  31. massgen/configs/tools/custom_tools/multimodal_tools/understand_image.yaml +33 -0
  32. massgen/configs/tools/custom_tools/multimodal_tools/understand_video.yaml +34 -0
  33. massgen/configs/tools/custom_tools/multimodal_tools/understand_video_example.yaml +54 -0
  34. massgen/configs/tools/custom_tools/multimodal_tools/youtube_video_analysis.yaml +59 -0
  35. massgen/configs/tools/memory/README.md +199 -0
  36. massgen/configs/tools/memory/gpt5mini_gemini_context_window_management.yaml +131 -0
  37. massgen/configs/tools/memory/gpt5mini_gemini_no_persistent_memory.yaml +133 -0
  38. massgen/configs/tools/memory/test_context_window_management.py +286 -0
  39. massgen/configs/tools/multimodal/gpt5mini_gpt5nano_documentation_evolution.yaml +97 -0
  40. massgen/configs/tools/planning/five_agents_discord_mcp_planning_mode.yaml +7 -29
  41. massgen/configs/tools/planning/five_agents_filesystem_mcp_planning_mode.yaml +5 -6
  42. massgen/configs/tools/planning/five_agents_notion_mcp_planning_mode.yaml +4 -4
  43. massgen/configs/tools/planning/five_agents_twitter_mcp_planning_mode.yaml +4 -4
  44. massgen/configs/tools/planning/gpt5_mini_case_study_mcp_planning_mode.yaml +2 -2
  45. massgen/docker/README.md +83 -0
  46. massgen/filesystem_manager/_code_execution_server.py +22 -7
  47. massgen/filesystem_manager/_docker_manager.py +21 -1
  48. massgen/filesystem_manager/_filesystem_manager.py +8 -0
  49. massgen/filesystem_manager/_workspace_tools_server.py +0 -997
  50. massgen/formatter/_gemini_formatter.py +73 -0
  51. massgen/frontend/coordination_ui.py +175 -257
  52. massgen/frontend/displays/base_display.py +29 -0
  53. massgen/frontend/displays/rich_terminal_display.py +155 -9
  54. massgen/frontend/displays/simple_display.py +21 -0
  55. massgen/frontend/displays/terminal_display.py +22 -2
  56. massgen/logger_config.py +50 -6
  57. massgen/message_templates.py +123 -3
  58. massgen/orchestrator.py +652 -44
  59. massgen/tests/test_code_execution.py +178 -0
  60. massgen/tests/test_intelligent_planning_mode.py +643 -0
  61. massgen/tests/test_orchestration_restart.py +204 -0
  62. massgen/token_manager/token_manager.py +13 -4
  63. massgen/tool/__init__.py +4 -0
  64. massgen/tool/_multimodal_tools/understand_audio.py +193 -0
  65. massgen/tool/_multimodal_tools/understand_file.py +550 -0
  66. massgen/tool/_multimodal_tools/understand_image.py +212 -0
  67. massgen/tool/_multimodal_tools/understand_video.py +313 -0
  68. massgen/tool/docs/multimodal_tools.md +779 -0
  69. massgen/tool/workflow_toolkits/__init__.py +26 -0
  70. massgen/tool/workflow_toolkits/post_evaluation.py +216 -0
  71. massgen/utils.py +1 -0
  72. {massgen-0.1.1.dist-info → massgen-0.1.3.dist-info}/METADATA +57 -52
  73. {massgen-0.1.1.dist-info → massgen-0.1.3.dist-info}/RECORD +77 -49
  74. {massgen-0.1.1.dist-info → massgen-0.1.3.dist-info}/WHEEL +0 -0
  75. {massgen-0.1.1.dist-info → massgen-0.1.3.dist-info}/entry_points.txt +0 -0
  76. {massgen-0.1.1.dist-info → massgen-0.1.3.dist-info}/licenses/LICENSE +0 -0
  77. {massgen-0.1.1.dist-info → massgen-0.1.3.dist-info}/top_level.txt +0 -0
massgen/cli.py CHANGED
@@ -51,6 +51,7 @@ from .backend.inference import InferenceBackend
51
51
  from .backend.lmstudio import LMStudioBackend
52
52
  from .backend.response import ResponseBackend
53
53
  from .chat_agent import ConfigurableAgent, SingleAgent
54
+ from .config_builder import ConfigBuilder
54
55
  from .frontend.coordination_ui import CoordinationUI
55
56
  from .logger_config import _DEBUG_MODE, logger, save_execution_metadata, setup_logging
56
57
  from .orchestrator import Orchestrator
@@ -203,7 +204,8 @@ def resolve_config_path(config_arg: Optional[str]) -> Optional[Path]:
203
204
 
204
205
  # Try in user config directory (~/.config/massgen/agents/)
205
206
  user_agents_dir = Path.home() / ".config/massgen/agents"
206
- user_config = user_agents_dir / f"{config_arg}.yaml"
207
+ # Try with config_arg as-is first
208
+ user_config = user_agents_dir / config_arg
207
209
  if user_config.exists():
208
210
  return user_config
209
211
 
@@ -212,13 +214,15 @@ def resolve_config_path(config_arg: Optional[str]) -> Optional[Path]:
212
214
  user_config_with_ext = user_agents_dir / f"{config_arg}.yaml"
213
215
  if user_config_with_ext.exists():
214
216
  return user_config_with_ext
217
+ # For error message, show the path with .yaml extension
218
+ user_config = user_config_with_ext
215
219
 
216
220
  # Config not found anywhere
217
221
  raise ConfigurationError(
218
222
  f"Configuration file not found: {config_arg}\n"
219
223
  f"Searched in:\n"
220
224
  f" - Current directory: {Path.cwd() / config_arg}\n"
221
- f" - User configs: {user_agents_dir / config_arg}.yaml\n"
225
+ f" - User configs: {user_config}\n"
222
226
  f"Use --list-examples to see available package configs.",
223
227
  )
224
228
 
@@ -271,6 +275,21 @@ def load_config_file(config_path: str) -> Dict[str, Any]:
271
275
  raise ConfigurationError(f"Error reading config file: {e}")
272
276
 
273
277
 
278
+ def _api_key_error_message(provider_name: str, env_var: str, config_path: Optional[str] = None) -> str:
279
+ """Generate standard API key error message."""
280
+ msg = (
281
+ f"{provider_name} API key not found. Set {env_var} environment variable.\n"
282
+ "You can add it to a .env file in:\n"
283
+ " - Current directory: .env\n"
284
+ " - User config: ~/.config/massgen/.env\n"
285
+ " - Global: ~/.massgen/.env\n"
286
+ "\nOr run: massgen --setup"
287
+ )
288
+ if config_path:
289
+ msg += f"\n\n📄 Using config: {config_path}"
290
+ return msg
291
+
292
+
274
293
  def create_backend(backend_type: str, **kwargs) -> Any:
275
294
  """Create backend instance from type and parameters.
276
295
 
@@ -299,6 +318,9 @@ def create_backend(backend_type: str, **kwargs) -> Any:
299
318
  """
300
319
  backend_type = backend_type.lower()
301
320
 
321
+ # Extract config path for error messages (and remove it from kwargs so it doesn't interfere)
322
+ config_path = kwargs.pop("_config_path", None)
323
+
302
324
  # Check if this is a framework/adapter type
303
325
  from massgen.adapters import adapter_registry
304
326
 
@@ -311,33 +333,25 @@ def create_backend(backend_type: str, **kwargs) -> Any:
311
333
  if backend_type == "openai":
312
334
  api_key = kwargs.get("api_key") or os.getenv("OPENAI_API_KEY")
313
335
  if not api_key:
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
- )
336
+ raise ConfigurationError(_api_key_error_message("OpenAI", "OPENAI_API_KEY", config_path))
317
337
  return ResponseBackend(api_key=api_key, **kwargs)
318
338
 
319
339
  elif backend_type == "grok":
320
340
  api_key = kwargs.get("api_key") or os.getenv("XAI_API_KEY")
321
341
  if not api_key:
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
- )
342
+ raise ConfigurationError(_api_key_error_message("Grok", "XAI_API_KEY", config_path))
325
343
  return GrokBackend(api_key=api_key, **kwargs)
326
344
 
327
345
  elif backend_type == "claude":
328
346
  api_key = kwargs.get("api_key") or os.getenv("ANTHROPIC_API_KEY")
329
347
  if not api_key:
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
- )
348
+ raise ConfigurationError(_api_key_error_message("Claude", "ANTHROPIC_API_KEY", config_path))
333
349
  return ClaudeBackend(api_key=api_key, **kwargs)
334
350
 
335
351
  elif backend_type == "gemini":
336
352
  api_key = kwargs.get("api_key") or os.getenv("GOOGLE_API_KEY") or os.getenv("GEMINI_API_KEY")
337
353
  if not api_key:
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
- )
354
+ raise ConfigurationError(_api_key_error_message("Gemini", "GOOGLE_API_KEY", config_path))
341
355
  return GeminiBackend(api_key=api_key, **kwargs)
342
356
 
343
357
  elif backend_type == "chatcompletion":
@@ -465,7 +479,7 @@ def create_backend(backend_type: str, **kwargs) -> Any:
465
479
  api_key = kwargs.get("api_key") or os.getenv("AZURE_OPENAI_API_KEY")
466
480
  endpoint = kwargs.get("base_url") or os.getenv("AZURE_OPENAI_ENDPOINT")
467
481
  if not api_key:
468
- raise ConfigurationError("Azure OpenAI API key not found. Set AZURE_OPENAI_API_KEY or provide in config.")
482
+ raise ConfigurationError(_api_key_error_message("Azure OpenAI", "AZURE_OPENAI_API_KEY", config_path))
469
483
  if not endpoint:
470
484
  raise ConfigurationError("Azure OpenAI endpoint not found. Set AZURE_OPENAI_ENDPOINT or provide base_url in config.")
471
485
  return AzureOpenAIBackend(**kwargs)
@@ -474,7 +488,7 @@ def create_backend(backend_type: str, **kwargs) -> Any:
474
488
  raise ConfigurationError(f"Unsupported backend type: {backend_type}")
475
489
 
476
490
 
477
- def create_agents_from_config(config: Dict[str, Any], orchestrator_config: Optional[Dict[str, Any]] = None) -> Dict[str, ConfigurableAgent]:
491
+ def create_agents_from_config(config: Dict[str, Any], orchestrator_config: Optional[Dict[str, Any]] = None, config_path: Optional[str] = None) -> Dict[str, ConfigurableAgent]:
478
492
  """Create agents from configuration."""
479
493
  agents = {}
480
494
 
@@ -516,8 +530,12 @@ def create_agents_from_config(config: Dict[str, Any], orchestrator_config: Optio
516
530
 
517
531
  backend_config["context_paths"] = merged_paths
518
532
 
533
+ # Add config path for better error messages
534
+ if config_path:
535
+ backend_config["_config_path"] = config_path
536
+
519
537
  backend = create_backend(backend_type, **backend_config)
520
- backend_params = {k: v for k, v in backend_config.items() if k != "type"}
538
+ backend_params = {k: v for k, v in backend_config.items() if k not in ("type", "_config_path")}
521
539
 
522
540
  backend_type_lower = backend_type.lower()
523
541
  if backend_type_lower == "openai":
@@ -538,8 +556,12 @@ def create_agents_from_config(config: Dict[str, Any], orchestrator_config: Optio
538
556
  agent_config = AgentConfig.create_vllm_config(**backend_params)
539
557
  elif backend_type_lower == "sglang":
540
558
  agent_config = AgentConfig.create_sglang_config(**backend_params)
559
+ elif backend_type_lower == "claude_code":
560
+ agent_config = AgentConfig.create_claude_code_config(**backend_params)
561
+ elif backend_type_lower == "azure_openai":
562
+ agent_config = AgentConfig.create_azure_openai_config(**backend_params)
541
563
  else:
542
- agent_config = AgentConfig(backend_params=backend_config)
564
+ agent_config = AgentConfig(backend_params=backend_params)
543
565
 
544
566
  agent_config.agent_id = agent_data.get("id", f"agent{i}")
545
567
 
@@ -552,7 +574,8 @@ def create_agents_from_config(config: Dict[str, Any], orchestrator_config: Optio
552
574
  else:
553
575
  # For other backends, fall back to deprecated custom_system_instruction
554
576
  # TODO: Add backend-specific routing for other backends
555
- agent_config.custom_system_instruction = system_msg
577
+ # Set private attribute directly to avoid deprecation warning
578
+ agent_config._custom_system_instruction = system_msg
556
579
 
557
580
  # Timeout configuration will be applied to orchestrator instead of individual agents
558
581
 
@@ -856,6 +879,23 @@ async def run_question_with_history(
856
879
  if orchestrator_cfg.get("skip_coordination_rounds", False):
857
880
  orchestrator_config.skip_coordination_rounds = True
858
881
 
882
+ if orchestrator_cfg.get("debug_final_answer"):
883
+ orchestrator_config.debug_final_answer = orchestrator_cfg["debug_final_answer"]
884
+
885
+ # Parse coordination config if present
886
+ if "coordination" in orchestrator_cfg:
887
+ from .agent_config import CoordinationConfig
888
+
889
+ coord_cfg = orchestrator_cfg["coordination"]
890
+ orchestrator_config.coordination_config = CoordinationConfig(
891
+ enable_planning_mode=coord_cfg.get("enable_planning_mode", False),
892
+ planning_mode_instruction=coord_cfg.get(
893
+ "planning_mode_instruction",
894
+ "During coordination, describe what you would do without actually executing actions. Only provide concrete implementation details without calling external APIs or tools.",
895
+ ),
896
+ max_orchestration_restarts=coord_cfg.get("max_orchestration_restarts", 0),
897
+ )
898
+
859
899
  # Load previous turns from session storage for multi-turn conversations
860
900
  previous_turns = load_previous_turns(session_info, session_storage)
861
901
 
@@ -879,6 +919,20 @@ async def run_question_with_history(
879
919
  else:
880
920
  mode_text = "Multi-Agent"
881
921
 
922
+ # Get coordination config from YAML (if present)
923
+ coordination_settings = kwargs.get("orchestrator", {}).get("coordination", {})
924
+ if coordination_settings:
925
+ from .agent_config import CoordinationConfig
926
+
927
+ orchestrator_config.coordination_config = CoordinationConfig(
928
+ enable_planning_mode=coordination_settings.get("enable_planning_mode", False),
929
+ planning_mode_instruction=coordination_settings.get(
930
+ "planning_mode_instruction",
931
+ """During coordination, describe what you would do. Only provide concrete implementation details and execute read-only actions.
932
+ DO NOT execute any actions that have side effects (e.g., sending messages, modifying data)""",
933
+ ),
934
+ )
935
+
882
936
  print(f"\n🤖 {BRIGHT_CYAN}{mode_text}{RESET}", flush=True)
883
937
  print(f"Agents: {', '.join(agents.keys())}", flush=True)
884
938
  if history:
@@ -889,16 +943,73 @@ async def run_question_with_history(
889
943
  # For multi-agent with history, we need to use a different approach
890
944
  # that maintains coordination UI display while supporting conversation context
891
945
 
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
946
+ # Restart loop (similar to multiturn pattern) - continues until no restart pending
947
+ response_content = None
948
+ while True:
949
+ if history and len(history) > 0:
950
+ # Use coordination UI with conversation context
951
+ # Extract current question from messages
952
+ current_question = messages[-1].get("content", question) if messages else question
896
953
 
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)
954
+ # Pass the full message context to the UI coordination
955
+ response_content = await ui.coordinate_with_context(orchestrator, current_question, messages)
956
+ else:
957
+ # Standard coordination for new conversations
958
+ response_content = await ui.coordinate(orchestrator, question)
959
+
960
+ # Check if restart is needed
961
+ if hasattr(orchestrator, "restart_pending") and orchestrator.restart_pending:
962
+ # Restart needed - create fresh UI for next attempt
963
+ print(f"\n{'='*80}")
964
+ print(f"🔄 Restarting coordination - Attempt {orchestrator.current_attempt + 1}/{orchestrator.max_attempts}")
965
+ print(f"{'='*80}\n")
966
+
967
+ # Reset all agent backends to ensure clean state for next attempt
968
+ for agent_id, agent in orchestrator.agents.items():
969
+ if hasattr(agent.backend, "reset_state"):
970
+ try:
971
+ await agent.backend.reset_state()
972
+ logger.info(f"Reset backend state for {agent_id}")
973
+ except Exception as e:
974
+ logger.warning(f"Failed to reset backend for {agent_id}: {e}")
975
+
976
+ # Create fresh UI instance for next attempt
977
+ ui = CoordinationUI(
978
+ display_type=ui_config.get("display_type", "rich_terminal"),
979
+ logging_enabled=ui_config.get("logging_enabled", True),
980
+ enable_final_presentation=True,
981
+ )
982
+
983
+ # Continue to next attempt
984
+ continue
985
+ else:
986
+ # Coordination complete - exit loop
987
+ break
988
+
989
+ # Copy final results to root level for convenience
990
+ try:
991
+ import shutil
992
+
993
+ from massgen.logger_config import get_log_session_dir, get_log_session_dir_base
994
+
995
+ # Get the current attempt's final directory
996
+ attempt_final_dir = get_log_session_dir() / "final"
997
+
998
+ # Get the base directory (without attempt subdirectory)
999
+ base_dir = get_log_session_dir_base()
1000
+ root_final_dir = base_dir / "final"
1001
+
1002
+ # Copy if the attempt's final directory exists
1003
+ if attempt_final_dir.exists():
1004
+ # Remove root final dir if it already exists
1005
+ if root_final_dir.exists():
1006
+ shutil.rmtree(root_final_dir)
1007
+
1008
+ # Copy attempt's final to root final
1009
+ shutil.copytree(attempt_final_dir, root_final_dir)
1010
+ logger.info(f"Copied final results from {attempt_final_dir} to {root_final_dir}")
1011
+ except Exception as e:
1012
+ logger.warning(f"Failed to copy final results to root: {e}")
902
1013
 
903
1014
  # Handle session persistence if applicable
904
1015
  session_id_to_use, updated_turn, normalized_response = await handle_session_persistence(
@@ -951,6 +1062,20 @@ async def run_single_question(question: str, agents: Dict[str, SingleAgent], ui_
951
1062
  if timeout_config:
952
1063
  orchestrator_config.timeout_config = timeout_config
953
1064
 
1065
+ # Get coordination config from YAML (if present)
1066
+ coordination_settings = kwargs.get("orchestrator", {}).get("coordination", {})
1067
+ if coordination_settings:
1068
+ from .agent_config import CoordinationConfig
1069
+
1070
+ orchestrator_config.coordination_config = CoordinationConfig(
1071
+ enable_planning_mode=coordination_settings.get("enable_planning_mode", False),
1072
+ planning_mode_instruction=coordination_settings.get(
1073
+ "planning_mode_instruction",
1074
+ """During coordination, describe what you would do. Only provide concrete implementation details and execute read-only actions.
1075
+ DO NOT execute any actions that have side effects (e.g., sending messages, modifying data)""",
1076
+ ),
1077
+ )
1078
+
954
1079
  # Get orchestrator parameters from config
955
1080
  orchestrator_cfg = kwargs.get("orchestrator", {})
956
1081
 
@@ -974,6 +1099,23 @@ async def run_single_question(question: str, agents: Dict[str, SingleAgent], ui_
974
1099
  if orchestrator_cfg.get("skip_coordination_rounds", False):
975
1100
  orchestrator_config.skip_coordination_rounds = True
976
1101
 
1102
+ if orchestrator_cfg.get("debug_final_answer"):
1103
+ orchestrator_config.debug_final_answer = orchestrator_cfg["debug_final_answer"]
1104
+
1105
+ # Parse coordination config if present
1106
+ if "coordination" in orchestrator_cfg:
1107
+ from .agent_config import CoordinationConfig
1108
+
1109
+ coord_cfg = orchestrator_cfg["coordination"]
1110
+ orchestrator_config.coordination_config = CoordinationConfig(
1111
+ enable_planning_mode=coord_cfg.get("enable_planning_mode", False),
1112
+ planning_mode_instruction=coord_cfg.get(
1113
+ "planning_mode_instruction",
1114
+ "During coordination, describe what you would do without actually executing actions. Only provide concrete implementation details without calling external APIs or tools.",
1115
+ ),
1116
+ max_orchestration_restarts=coord_cfg.get("max_orchestration_restarts", 0),
1117
+ )
1118
+
977
1119
  orchestrator = Orchestrator(
978
1120
  agents=agents,
979
1121
  config=orchestrator_config,
@@ -992,7 +1134,70 @@ async def run_single_question(question: str, agents: Dict[str, SingleAgent], ui_
992
1134
  print(f"Question: {question}", flush=True)
993
1135
  print("\n" + "=" * 60, flush=True)
994
1136
 
995
- final_response = await ui.coordinate(orchestrator, question)
1137
+ # Restart loop (similar to multiturn pattern)
1138
+ # Continues calling coordinate() until no restart is pending
1139
+ final_response = None
1140
+ while True:
1141
+ # Call coordinate with current orchestrator state
1142
+ final_response = await ui.coordinate(orchestrator, question)
1143
+
1144
+ # Check if restart is needed
1145
+ if hasattr(orchestrator, "restart_pending") and orchestrator.restart_pending:
1146
+ # Restart needed - create fresh UI for next attempt
1147
+ print(f"\n{'='*80}")
1148
+ print(f"🔄 Restarting coordination - Attempt {orchestrator.current_attempt + 1}/{orchestrator.max_attempts}")
1149
+ print(f"{'='*80}\n")
1150
+
1151
+ # Reset all agent backends to ensure clean state for next attempt
1152
+ for agent_id, agent in orchestrator.agents.items():
1153
+ if hasattr(agent.backend, "reset_state"):
1154
+ try:
1155
+ await agent.backend.reset_state()
1156
+ logger.info(f"Reset backend state for {agent_id}")
1157
+ except Exception as e:
1158
+ logger.warning(f"Failed to reset backend for {agent_id}: {e}")
1159
+
1160
+ # Create fresh UI instance for next attempt
1161
+ ui = CoordinationUI(
1162
+ display_type=ui_config.get("display_type", "rich_terminal"),
1163
+ logging_enabled=ui_config.get("logging_enabled", True),
1164
+ enable_final_presentation=True,
1165
+ )
1166
+
1167
+ # Continue to next attempt
1168
+ continue
1169
+ else:
1170
+ # Coordination complete - exit loop
1171
+ break
1172
+
1173
+ # Copy final results to root level for convenience
1174
+ try:
1175
+ import shutil
1176
+
1177
+ from massgen.logger_config import (
1178
+ get_log_session_dir,
1179
+ get_log_session_dir_base,
1180
+ )
1181
+
1182
+ # Get the current attempt's final directory
1183
+ attempt_final_dir = get_log_session_dir() / "final"
1184
+
1185
+ # Get the base directory (without attempt subdirectory)
1186
+ base_dir = get_log_session_dir_base()
1187
+ root_final_dir = base_dir / "final"
1188
+
1189
+ # Copy if the attempt's final directory exists
1190
+ if attempt_final_dir.exists():
1191
+ # Remove root final dir if it already exists
1192
+ if root_final_dir.exists():
1193
+ shutil.rmtree(root_final_dir)
1194
+
1195
+ # Copy attempt's final to root final
1196
+ shutil.copytree(attempt_final_dir, root_final_dir)
1197
+ logger.info(f"Copied final results from {attempt_final_dir} to {root_final_dir}")
1198
+ except Exception as e:
1199
+ logger.warning(f"Failed to copy final results to root: {e}")
1200
+
996
1201
  return final_response
997
1202
 
998
1203
 
@@ -1767,7 +1972,7 @@ async def run_interactive_mode(
1767
1972
  config_modified = prompt_for_context_paths(original_config, orchestrator_cfg)
1768
1973
  if config_modified:
1769
1974
  # Recreate agents with updated context paths
1770
- agents = create_agents_from_config(original_config, orchestrator_cfg)
1975
+ agents = create_agents_from_config(original_config, orchestrator_cfg, config_path=config_path)
1771
1976
  print(f" {BRIGHT_GREEN}✓ Agents reloaded with updated context paths{RESET}", flush=True)
1772
1977
  print()
1773
1978
 
@@ -1825,7 +2030,7 @@ async def run_interactive_mode(
1825
2030
  backend_config["context_paths"] = existing_context_paths + [new_turn_config]
1826
2031
 
1827
2032
  # Recreate agents from modified config
1828
- agents = create_agents_from_config(modified_config, orchestrator_cfg)
2033
+ agents = create_agents_from_config(modified_config, orchestrator_cfg, config_path=config_path)
1829
2034
  logger.info(f"[CLI] Successfully recreated {len(agents)} agents with turn {current_turn} path as read-only context")
1830
2035
 
1831
2036
  question = input(f"\n{BRIGHT_BLUE}👤 User:{RESET} ").strip()
@@ -2014,6 +2219,9 @@ async def main(args):
2014
2219
  print("❌ Configuration error: Either --config, --model, or --backend must be specified", flush=True)
2015
2220
  sys.exit(1)
2016
2221
 
2222
+ # Track config path for error messages
2223
+ resolved_path = None
2224
+
2017
2225
  try:
2018
2226
  # Load or create configuration
2019
2227
  if args.config:
@@ -2114,7 +2322,7 @@ async def main(args):
2114
2322
  ' agent_temporary_workspace: "your_temp_dir" # Directory for temporary agent workspaces',
2115
2323
  )
2116
2324
 
2117
- agents = create_agents_from_config(config, orchestrator_cfg)
2325
+ agents = create_agents_from_config(config, orchestrator_cfg, config_path=str(resolved_path) if resolved_path else None)
2118
2326
 
2119
2327
  if not agents:
2120
2328
  raise ConfigurationError("No agents configured")
@@ -2345,8 +2553,6 @@ Environment Variables:
2345
2553
 
2346
2554
  # Launch interactive API key setup if requested
2347
2555
  if args.setup:
2348
- from .config_builder import ConfigBuilder
2349
-
2350
2556
  builder = ConfigBuilder()
2351
2557
  api_keys = builder.interactive_api_key_setup()
2352
2558
 
@@ -2371,8 +2577,6 @@ Environment Variables:
2371
2577
 
2372
2578
  # Launch interactive config builder if requested
2373
2579
  if args.init:
2374
- from .config_builder import ConfigBuilder
2375
-
2376
2580
  builder = ConfigBuilder()
2377
2581
  result = builder.run()
2378
2582
 
@@ -2394,7 +2598,7 @@ Environment Variables:
2394
2598
  # Builder returned None (cancelled or error)
2395
2599
  return
2396
2600
 
2397
- # First-run detection: auto-trigger builder if no config specified and first run
2601
+ # First-run detection: auto-trigger setup wizard and config builder if no config specified
2398
2602
  if not args.question and not args.config and not args.model and not args.backend:
2399
2603
  if should_run_builder():
2400
2604
  print()
@@ -2403,27 +2607,99 @@ Environment Variables:
2403
2607
  print(f"{BRIGHT_CYAN} 👋 Welcome to MassGen!{RESET}")
2404
2608
  print(f"{BRIGHT_CYAN}{'=' * 60}{RESET}")
2405
2609
  print()
2610
+
2611
+ # Check if API keys already exist
2612
+ builder = ConfigBuilder(default_mode=True)
2613
+ existing_api_keys = builder.detect_api_keys()
2614
+
2615
+ # Only check for cloud provider API keys (exclude local models and Claude Code)
2616
+ cloud_providers = ["openai", "anthropic", "gemini", "grok", "azure_openai"]
2617
+ has_api_keys = any(existing_api_keys.get(provider, False) for provider in cloud_providers)
2618
+
2619
+ # Step 1: API key setup (only if no keys found)
2620
+ if not has_api_keys:
2621
+ print(" Let's first set up your API keys...")
2622
+ print()
2623
+
2624
+ api_keys = builder.interactive_api_key_setup()
2625
+
2626
+ if any(api_keys.values()):
2627
+ print(f"\n{BRIGHT_GREEN}✅ API key setup complete!{RESET}")
2628
+ print(f"{BRIGHT_CYAN}💡 You can now use MassGen with these providers{RESET}\n")
2629
+ else:
2630
+ print(f"\n{BRIGHT_YELLOW}⚠️ No API keys configured{RESET}")
2631
+ print(f"{BRIGHT_CYAN}💡 You can use local models (vLLM, Ollama) without API keys{RESET}\n")
2632
+ else:
2633
+ print(f"{BRIGHT_GREEN}✅ API keys detected{RESET}")
2634
+ print()
2635
+
2636
+ # Step 2: Launch config builder
2406
2637
  print(" Let's set up your default configuration...")
2407
2638
  print()
2408
2639
 
2409
- from .config_builder import ConfigBuilder
2410
-
2411
- builder = ConfigBuilder(default_mode=True)
2412
2640
  result = builder.run()
2413
2641
 
2414
2642
  if result and len(result) == 2:
2415
2643
  filepath, question = result
2416
2644
  if filepath:
2645
+ # Set the config path
2417
2646
  args.config = filepath
2647
+
2648
+ # If user provided a question, set it
2418
2649
  if question:
2419
2650
  args.question = question
2651
+ # Will run single question mode
2420
2652
  else:
2421
- print("\n✅ Configuration saved! You can now run queries.")
2422
- print('Example: massgen "Your question here"')
2423
- return
2653
+ # No question - will launch interactive mode
2654
+ # Check if this is NOT already the default config
2655
+ default_config = Path.home() / ".config/massgen/config.yaml"
2656
+ is_default = Path(filepath).resolve() == default_config.resolve()
2657
+
2658
+ if not is_default:
2659
+ # Ask if they want to save as default (for any non-default config)
2660
+ # Determine what type of config this is for messaging
2661
+ is_example = False
2662
+ try:
2663
+ from importlib.resources import files
2664
+
2665
+ package_configs = files("massgen").joinpath("configs")
2666
+ filepath_path = Path(filepath).resolve()
2667
+ package_path = Path(str(package_configs)).resolve()
2668
+ is_example = str(filepath_path).startswith(str(package_path))
2669
+ except Exception:
2670
+ pass
2671
+
2672
+ if is_example:
2673
+ print(f"\n{BRIGHT_CYAN}📦 You selected a package example{RESET}")
2674
+ else:
2675
+ print(f"\n{BRIGHT_CYAN}📄 You selected a config{RESET}")
2676
+ print(f" {filepath}")
2677
+
2678
+ from rich.prompt import Confirm
2679
+
2680
+ save_as_default = Confirm.ask(
2681
+ "\n[prompt]Save this as your default config?[/prompt]",
2682
+ default=False,
2683
+ )
2684
+
2685
+ if save_as_default:
2686
+ # Copy to default location
2687
+ default_config.parent.mkdir(parents=True, exist_ok=True)
2688
+ shutil.copy(filepath, default_config)
2689
+ print(f"\n{BRIGHT_GREEN}✅ Config saved to: {default_config}{RESET}")
2690
+ args.config = str(default_config)
2691
+ else:
2692
+ # Just use for this session
2693
+ print(f"\n{BRIGHT_CYAN}💡 Using for this session only{RESET}")
2694
+
2695
+ # Launch into interactive mode
2696
+ print(f"\n{BRIGHT_GREEN}🚀 Launching interactive mode...{RESET}\n")
2697
+ # Don't return - continue to main() below
2424
2698
  else:
2699
+ # No filepath - user cancelled
2425
2700
  return
2426
2701
  else:
2702
+ # Builder returned None - user cancelled
2427
2703
  return
2428
2704
 
2429
2705
  # Now call the async main with the parsed arguments