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.
- massgen/__init__.py +1 -1
- massgen/agent_config.py +33 -7
- massgen/api_params_handler/_api_params_handler_base.py +3 -0
- massgen/backend/azure_openai.py +9 -1
- massgen/backend/base.py +4 -0
- massgen/backend/claude_code.py +9 -1
- massgen/backend/gemini.py +35 -6
- massgen/backend/gemini_utils.py +30 -0
- massgen/chat_agent.py +9 -3
- massgen/cli.py +291 -43
- massgen/config_builder.py +163 -18
- massgen/configs/README.md +52 -6
- massgen/configs/debug/restart_test_controlled.yaml +60 -0
- massgen/configs/debug/restart_test_controlled_filesystem.yaml +73 -0
- massgen/configs/tools/code-execution/docker_with_sudo.yaml +35 -0
- massgen/configs/tools/custom_tools/computer_use_browser_example.yaml +56 -0
- massgen/configs/tools/custom_tools/computer_use_docker_example.yaml +65 -0
- massgen/configs/tools/custom_tools/computer_use_example.yaml +50 -0
- massgen/configs/tools/custom_tools/crawl4ai_mcp_example.yaml +67 -0
- massgen/configs/tools/custom_tools/crawl4ai_multi_agent_example.yaml +68 -0
- massgen/configs/tools/custom_tools/multimodal_tools/playwright_with_img_understanding.yaml +98 -0
- massgen/configs/tools/custom_tools/multimodal_tools/understand_audio.yaml +33 -0
- massgen/configs/tools/custom_tools/multimodal_tools/understand_file.yaml +34 -0
- massgen/configs/tools/custom_tools/multimodal_tools/understand_image.yaml +33 -0
- massgen/configs/tools/custom_tools/multimodal_tools/understand_video.yaml +34 -0
- massgen/configs/tools/custom_tools/multimodal_tools/understand_video_example.yaml +54 -0
- massgen/configs/tools/custom_tools/multimodal_tools/youtube_video_analysis.yaml +59 -0
- massgen/configs/tools/memory/README.md +199 -0
- massgen/configs/tools/memory/gpt5mini_gemini_context_window_management.yaml +131 -0
- massgen/configs/tools/memory/gpt5mini_gemini_no_persistent_memory.yaml +133 -0
- massgen/configs/tools/memory/test_context_window_management.py +286 -0
- massgen/configs/tools/multimodal/gpt5mini_gpt5nano_documentation_evolution.yaml +97 -0
- massgen/docker/README.md +83 -0
- massgen/filesystem_manager/_code_execution_server.py +22 -7
- massgen/filesystem_manager/_docker_manager.py +21 -1
- massgen/filesystem_manager/_filesystem_manager.py +8 -0
- massgen/filesystem_manager/_workspace_tools_server.py +0 -997
- massgen/formatter/_gemini_formatter.py +73 -0
- massgen/frontend/coordination_ui.py +175 -257
- massgen/frontend/displays/base_display.py +29 -0
- massgen/frontend/displays/rich_terminal_display.py +155 -9
- massgen/frontend/displays/simple_display.py +21 -0
- massgen/frontend/displays/terminal_display.py +22 -2
- massgen/logger_config.py +50 -6
- massgen/message_templates.py +123 -3
- massgen/orchestrator.py +319 -38
- massgen/tests/test_code_execution.py +178 -0
- massgen/tests/test_orchestration_restart.py +204 -0
- massgen/tool/__init__.py +4 -0
- massgen/tool/_multimodal_tools/understand_audio.py +193 -0
- massgen/tool/_multimodal_tools/understand_file.py +550 -0
- massgen/tool/_multimodal_tools/understand_image.py +212 -0
- massgen/tool/_multimodal_tools/understand_video.py +313 -0
- massgen/tool/docs/multimodal_tools.md +779 -0
- massgen/tool/workflow_toolkits/__init__.py +26 -0
- massgen/tool/workflow_toolkits/post_evaluation.py +216 -0
- massgen/utils.py +1 -0
- {massgen-0.1.2.dist-info → massgen-0.1.3.dist-info}/METADATA +8 -3
- {massgen-0.1.2.dist-info → massgen-0.1.3.dist-info}/RECORD +63 -36
- {massgen-0.1.2.dist-info → massgen-0.1.3.dist-info}/WHEEL +0 -0
- {massgen-0.1.2.dist-info → massgen-0.1.3.dist-info}/entry_points.txt +0 -0
- {massgen-0.1.2.dist-info → massgen-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
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: {
|
|
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
|
|
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
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
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
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
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
|