massgen 0.1.2__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 (63) 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/backend/azure_openai.py +9 -1
  5. massgen/backend/base.py +4 -0
  6. massgen/backend/claude_code.py +9 -1
  7. massgen/backend/gemini.py +35 -6
  8. massgen/backend/gemini_utils.py +30 -0
  9. massgen/chat_agent.py +9 -3
  10. massgen/cli.py +291 -43
  11. massgen/config_builder.py +163 -18
  12. massgen/configs/README.md +52 -6
  13. massgen/configs/debug/restart_test_controlled.yaml +60 -0
  14. massgen/configs/debug/restart_test_controlled_filesystem.yaml +73 -0
  15. massgen/configs/tools/code-execution/docker_with_sudo.yaml +35 -0
  16. massgen/configs/tools/custom_tools/computer_use_browser_example.yaml +56 -0
  17. massgen/configs/tools/custom_tools/computer_use_docker_example.yaml +65 -0
  18. massgen/configs/tools/custom_tools/computer_use_example.yaml +50 -0
  19. massgen/configs/tools/custom_tools/crawl4ai_mcp_example.yaml +67 -0
  20. massgen/configs/tools/custom_tools/crawl4ai_multi_agent_example.yaml +68 -0
  21. massgen/configs/tools/custom_tools/multimodal_tools/playwright_with_img_understanding.yaml +98 -0
  22. massgen/configs/tools/custom_tools/multimodal_tools/understand_audio.yaml +33 -0
  23. massgen/configs/tools/custom_tools/multimodal_tools/understand_file.yaml +34 -0
  24. massgen/configs/tools/custom_tools/multimodal_tools/understand_image.yaml +33 -0
  25. massgen/configs/tools/custom_tools/multimodal_tools/understand_video.yaml +34 -0
  26. massgen/configs/tools/custom_tools/multimodal_tools/understand_video_example.yaml +54 -0
  27. massgen/configs/tools/custom_tools/multimodal_tools/youtube_video_analysis.yaml +59 -0
  28. massgen/configs/tools/memory/README.md +199 -0
  29. massgen/configs/tools/memory/gpt5mini_gemini_context_window_management.yaml +131 -0
  30. massgen/configs/tools/memory/gpt5mini_gemini_no_persistent_memory.yaml +133 -0
  31. massgen/configs/tools/memory/test_context_window_management.py +286 -0
  32. massgen/configs/tools/multimodal/gpt5mini_gpt5nano_documentation_evolution.yaml +97 -0
  33. massgen/docker/README.md +83 -0
  34. massgen/filesystem_manager/_code_execution_server.py +22 -7
  35. massgen/filesystem_manager/_docker_manager.py +21 -1
  36. massgen/filesystem_manager/_filesystem_manager.py +8 -0
  37. massgen/filesystem_manager/_workspace_tools_server.py +0 -997
  38. massgen/formatter/_gemini_formatter.py +73 -0
  39. massgen/frontend/coordination_ui.py +175 -257
  40. massgen/frontend/displays/base_display.py +29 -0
  41. massgen/frontend/displays/rich_terminal_display.py +155 -9
  42. massgen/frontend/displays/simple_display.py +21 -0
  43. massgen/frontend/displays/terminal_display.py +22 -2
  44. massgen/logger_config.py +50 -6
  45. massgen/message_templates.py +123 -3
  46. massgen/orchestrator.py +319 -38
  47. massgen/tests/test_code_execution.py +178 -0
  48. massgen/tests/test_orchestration_restart.py +204 -0
  49. massgen/tool/__init__.py +4 -0
  50. massgen/tool/_multimodal_tools/understand_audio.py +193 -0
  51. massgen/tool/_multimodal_tools/understand_file.py +550 -0
  52. massgen/tool/_multimodal_tools/understand_image.py +212 -0
  53. massgen/tool/_multimodal_tools/understand_video.py +313 -0
  54. massgen/tool/docs/multimodal_tools.md +779 -0
  55. massgen/tool/workflow_toolkits/__init__.py +26 -0
  56. massgen/tool/workflow_toolkits/post_evaluation.py +216 -0
  57. massgen/utils.py +1 -0
  58. {massgen-0.1.2.dist-info → massgen-0.1.3.dist-info}/METADATA +8 -3
  59. {massgen-0.1.2.dist-info → massgen-0.1.3.dist-info}/RECORD +63 -36
  60. {massgen-0.1.2.dist-info → massgen-0.1.3.dist-info}/WHEEL +0 -0
  61. {massgen-0.1.2.dist-info → massgen-0.1.3.dist-info}/entry_points.txt +0 -0
  62. {massgen-0.1.2.dist-info → massgen-0.1.3.dist-info}/licenses/LICENSE +0 -0
  63. {massgen-0.1.2.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
 
@@ -903,16 +943,73 @@ async def run_question_with_history(
903
943
  # For multi-agent with history, we need to use a different approach
904
944
  # that maintains coordination UI display while supporting conversation context
905
945
 
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
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
910
953
 
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)
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}")
916
1013
 
917
1014
  # Handle session persistence if applicable
918
1015
  session_id_to_use, updated_turn, normalized_response = await handle_session_persistence(
@@ -1002,6 +1099,23 @@ async def run_single_question(question: str, agents: Dict[str, SingleAgent], ui_
1002
1099
  if orchestrator_cfg.get("skip_coordination_rounds", False):
1003
1100
  orchestrator_config.skip_coordination_rounds = True
1004
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
+
1005
1119
  orchestrator = Orchestrator(
1006
1120
  agents=agents,
1007
1121
  config=orchestrator_config,
@@ -1020,7 +1134,70 @@ async def run_single_question(question: str, agents: Dict[str, SingleAgent], ui_
1020
1134
  print(f"Question: {question}", flush=True)
1021
1135
  print("\n" + "=" * 60, flush=True)
1022
1136
 
1023
- 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
+
1024
1201
  return final_response
1025
1202
 
1026
1203
 
@@ -1795,7 +1972,7 @@ async def run_interactive_mode(
1795
1972
  config_modified = prompt_for_context_paths(original_config, orchestrator_cfg)
1796
1973
  if config_modified:
1797
1974
  # Recreate agents with updated context paths
1798
- agents = create_agents_from_config(original_config, orchestrator_cfg)
1975
+ agents = create_agents_from_config(original_config, orchestrator_cfg, config_path=config_path)
1799
1976
  print(f" {BRIGHT_GREEN}✓ Agents reloaded with updated context paths{RESET}", flush=True)
1800
1977
  print()
1801
1978
 
@@ -1853,7 +2030,7 @@ async def run_interactive_mode(
1853
2030
  backend_config["context_paths"] = existing_context_paths + [new_turn_config]
1854
2031
 
1855
2032
  # Recreate agents from modified config
1856
- agents = create_agents_from_config(modified_config, orchestrator_cfg)
2033
+ agents = create_agents_from_config(modified_config, orchestrator_cfg, config_path=config_path)
1857
2034
  logger.info(f"[CLI] Successfully recreated {len(agents)} agents with turn {current_turn} path as read-only context")
1858
2035
 
1859
2036
  question = input(f"\n{BRIGHT_BLUE}👤 User:{RESET} ").strip()
@@ -2042,6 +2219,9 @@ async def main(args):
2042
2219
  print("❌ Configuration error: Either --config, --model, or --backend must be specified", flush=True)
2043
2220
  sys.exit(1)
2044
2221
 
2222
+ # Track config path for error messages
2223
+ resolved_path = None
2224
+
2045
2225
  try:
2046
2226
  # Load or create configuration
2047
2227
  if args.config:
@@ -2142,7 +2322,7 @@ async def main(args):
2142
2322
  ' agent_temporary_workspace: "your_temp_dir" # Directory for temporary agent workspaces',
2143
2323
  )
2144
2324
 
2145
- 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)
2146
2326
 
2147
2327
  if not agents:
2148
2328
  raise ConfigurationError("No agents configured")
@@ -2373,8 +2553,6 @@ Environment Variables:
2373
2553
 
2374
2554
  # Launch interactive API key setup if requested
2375
2555
  if args.setup:
2376
- from .config_builder import ConfigBuilder
2377
-
2378
2556
  builder = ConfigBuilder()
2379
2557
  api_keys = builder.interactive_api_key_setup()
2380
2558
 
@@ -2399,8 +2577,6 @@ Environment Variables:
2399
2577
 
2400
2578
  # Launch interactive config builder if requested
2401
2579
  if args.init:
2402
- from .config_builder import ConfigBuilder
2403
-
2404
2580
  builder = ConfigBuilder()
2405
2581
  result = builder.run()
2406
2582
 
@@ -2422,7 +2598,7 @@ Environment Variables:
2422
2598
  # Builder returned None (cancelled or error)
2423
2599
  return
2424
2600
 
2425
- # 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
2426
2602
  if not args.question and not args.config and not args.model and not args.backend:
2427
2603
  if should_run_builder():
2428
2604
  print()
@@ -2431,27 +2607,99 @@ Environment Variables:
2431
2607
  print(f"{BRIGHT_CYAN} 👋 Welcome to MassGen!{RESET}")
2432
2608
  print(f"{BRIGHT_CYAN}{'=' * 60}{RESET}")
2433
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
2434
2637
  print(" Let's set up your default configuration...")
2435
2638
  print()
2436
2639
 
2437
- from .config_builder import ConfigBuilder
2438
-
2439
- builder = ConfigBuilder(default_mode=True)
2440
2640
  result = builder.run()
2441
2641
 
2442
2642
  if result and len(result) == 2:
2443
2643
  filepath, question = result
2444
2644
  if filepath:
2645
+ # Set the config path
2445
2646
  args.config = filepath
2647
+
2648
+ # If user provided a question, set it
2446
2649
  if question:
2447
2650
  args.question = question
2651
+ # Will run single question mode
2448
2652
  else:
2449
- print("\n✅ Configuration saved! You can now run queries.")
2450
- print('Example: massgen "Your question here"')
2451
- 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
2452
2698
  else:
2699
+ # No filepath - user cancelled
2453
2700
  return
2454
2701
  else:
2702
+ # Builder returned None - user cancelled
2455
2703
  return
2456
2704
 
2457
2705
  # Now call the async main with the parsed arguments