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.
- massgen/__init__.py +1 -1
- massgen/agent_config.py +33 -7
- massgen/api_params_handler/_api_params_handler_base.py +3 -0
- massgen/api_params_handler/_chat_completions_api_params_handler.py +7 -1
- massgen/backend/azure_openai.py +9 -1
- massgen/backend/base.py +56 -0
- massgen/backend/base_with_custom_tool_and_mcp.py +4 -4
- massgen/backend/capabilities.py +6 -6
- massgen/backend/chat_completions.py +18 -11
- massgen/backend/claude_code.py +9 -1
- massgen/backend/gemini.py +71 -6
- massgen/backend/gemini_utils.py +30 -0
- massgen/backend/grok.py +39 -6
- massgen/backend/response.py +18 -11
- massgen/chat_agent.py +9 -3
- massgen/cli.py +319 -43
- massgen/config_builder.py +163 -18
- massgen/configs/README.md +78 -20
- massgen/configs/basic/multi/three_agents_default.yaml +2 -2
- 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/configs/tools/planning/five_agents_discord_mcp_planning_mode.yaml +7 -29
- massgen/configs/tools/planning/five_agents_filesystem_mcp_planning_mode.yaml +5 -6
- massgen/configs/tools/planning/five_agents_notion_mcp_planning_mode.yaml +4 -4
- massgen/configs/tools/planning/five_agents_twitter_mcp_planning_mode.yaml +4 -4
- massgen/configs/tools/planning/gpt5_mini_case_study_mcp_planning_mode.yaml +2 -2
- 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 +652 -44
- massgen/tests/test_code_execution.py +178 -0
- massgen/tests/test_intelligent_planning_mode.py +643 -0
- massgen/tests/test_orchestration_restart.py +204 -0
- massgen/token_manager/token_manager.py +13 -4
- 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.1.dist-info → massgen-0.1.3.dist-info}/METADATA +57 -52
- {massgen-0.1.1.dist-info → massgen-0.1.3.dist-info}/RECORD +77 -49
- {massgen-0.1.1.dist-info → massgen-0.1.3.dist-info}/WHEEL +0 -0
- {massgen-0.1.1.dist-info → massgen-0.1.3.dist-info}/entry_points.txt +0 -0
- {massgen-0.1.1.dist-info → massgen-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
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
|
|
|
@@ -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
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
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
|