massgen 0.0.3__py3-none-any.whl → 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of massgen might be problematic. Click here for more details.
- massgen/__init__.py +142 -8
- massgen/adapters/__init__.py +29 -0
- massgen/adapters/ag2_adapter.py +483 -0
- massgen/adapters/base.py +183 -0
- massgen/adapters/tests/__init__.py +0 -0
- massgen/adapters/tests/test_ag2_adapter.py +439 -0
- massgen/adapters/tests/test_agent_adapter.py +128 -0
- massgen/adapters/utils/__init__.py +2 -0
- massgen/adapters/utils/ag2_utils.py +236 -0
- massgen/adapters/utils/tests/__init__.py +0 -0
- massgen/adapters/utils/tests/test_ag2_utils.py +138 -0
- massgen/agent_config.py +329 -55
- massgen/api_params_handler/__init__.py +10 -0
- massgen/api_params_handler/_api_params_handler_base.py +99 -0
- massgen/api_params_handler/_chat_completions_api_params_handler.py +176 -0
- massgen/api_params_handler/_claude_api_params_handler.py +113 -0
- massgen/api_params_handler/_response_api_params_handler.py +130 -0
- massgen/backend/__init__.py +39 -4
- massgen/backend/azure_openai.py +385 -0
- massgen/backend/base.py +341 -69
- massgen/backend/base_with_mcp.py +1102 -0
- massgen/backend/capabilities.py +386 -0
- massgen/backend/chat_completions.py +577 -130
- massgen/backend/claude.py +1033 -537
- massgen/backend/claude_code.py +1203 -0
- massgen/backend/cli_base.py +209 -0
- massgen/backend/docs/BACKEND_ARCHITECTURE.md +126 -0
- massgen/backend/{CLAUDE_API_RESEARCH.md → docs/CLAUDE_API_RESEARCH.md} +18 -18
- massgen/backend/{GEMINI_API_DOCUMENTATION.md → docs/GEMINI_API_DOCUMENTATION.md} +9 -9
- massgen/backend/docs/Gemini MCP Integration Analysis.md +1050 -0
- massgen/backend/docs/MCP_IMPLEMENTATION_CLAUDE_BACKEND.md +177 -0
- massgen/backend/docs/MCP_INTEGRATION_RESPONSE_BACKEND.md +352 -0
- massgen/backend/docs/OPENAI_GPT5_MODELS.md +211 -0
- massgen/backend/{OPENAI_RESPONSES_API_FORMAT.md → docs/OPENAI_RESPONSE_API_TOOL_CALLS.md} +3 -3
- massgen/backend/docs/OPENAI_response_streaming.md +20654 -0
- massgen/backend/docs/inference_backend.md +257 -0
- massgen/backend/docs/permissions_and_context_files.md +1085 -0
- massgen/backend/external.py +126 -0
- massgen/backend/gemini.py +1850 -241
- massgen/backend/grok.py +40 -156
- massgen/backend/inference.py +156 -0
- massgen/backend/lmstudio.py +171 -0
- massgen/backend/response.py +1095 -322
- massgen/chat_agent.py +131 -113
- massgen/cli.py +1560 -275
- massgen/config_builder.py +2396 -0
- massgen/configs/BACKEND_CONFIGURATION.md +458 -0
- massgen/configs/README.md +559 -216
- massgen/configs/ag2/ag2_case_study.yaml +27 -0
- massgen/configs/ag2/ag2_coder.yaml +34 -0
- massgen/configs/ag2/ag2_coder_case_study.yaml +36 -0
- massgen/configs/ag2/ag2_gemini.yaml +27 -0
- massgen/configs/ag2/ag2_groupchat.yaml +108 -0
- massgen/configs/ag2/ag2_groupchat_gpt.yaml +118 -0
- massgen/configs/ag2/ag2_single_agent.yaml +21 -0
- massgen/configs/basic/multi/fast_timeout_example.yaml +37 -0
- massgen/configs/basic/multi/gemini_4o_claude.yaml +31 -0
- massgen/configs/basic/multi/gemini_gpt5nano_claude.yaml +36 -0
- massgen/configs/{gemini_4o_claude.yaml → basic/multi/geminicode_4o_claude.yaml} +3 -3
- massgen/configs/basic/multi/geminicode_gpt5nano_claude.yaml +36 -0
- massgen/configs/basic/multi/glm_gemini_claude.yaml +25 -0
- massgen/configs/basic/multi/gpt4o_audio_generation.yaml +30 -0
- massgen/configs/basic/multi/gpt4o_image_generation.yaml +31 -0
- massgen/configs/basic/multi/gpt5nano_glm_qwen.yaml +26 -0
- massgen/configs/basic/multi/gpt5nano_image_understanding.yaml +26 -0
- massgen/configs/{three_agents_default.yaml → basic/multi/three_agents_default.yaml} +8 -4
- massgen/configs/basic/multi/three_agents_opensource.yaml +27 -0
- massgen/configs/basic/multi/three_agents_vllm.yaml +20 -0
- massgen/configs/basic/multi/two_agents_gemini.yaml +19 -0
- massgen/configs/{two_agents.yaml → basic/multi/two_agents_gpt5.yaml} +14 -6
- massgen/configs/basic/multi/two_agents_opensource_lmstudio.yaml +31 -0
- massgen/configs/basic/multi/two_qwen_vllm_sglang.yaml +28 -0
- massgen/configs/{single_agent.yaml → basic/single/single_agent.yaml} +1 -1
- massgen/configs/{single_flash2.5.yaml → basic/single/single_flash2.5.yaml} +1 -2
- massgen/configs/basic/single/single_gemini2.5pro.yaml +16 -0
- massgen/configs/basic/single/single_gpt4o_audio_generation.yaml +22 -0
- massgen/configs/basic/single/single_gpt4o_image_generation.yaml +22 -0
- massgen/configs/basic/single/single_gpt4o_video_generation.yaml +24 -0
- massgen/configs/basic/single/single_gpt5nano.yaml +20 -0
- massgen/configs/basic/single/single_gpt5nano_file_search.yaml +18 -0
- massgen/configs/basic/single/single_gpt5nano_image_understanding.yaml +17 -0
- massgen/configs/basic/single/single_gptoss120b.yaml +15 -0
- massgen/configs/basic/single/single_openrouter_audio_understanding.yaml +15 -0
- massgen/configs/basic/single/single_qwen_video_understanding.yaml +15 -0
- massgen/configs/debug/code_execution/command_filtering_blacklist.yaml +29 -0
- massgen/configs/debug/code_execution/command_filtering_whitelist.yaml +28 -0
- massgen/configs/debug/code_execution/docker_verification.yaml +29 -0
- massgen/configs/debug/skip_coordination_test.yaml +27 -0
- massgen/configs/debug/test_sdk_migration.yaml +17 -0
- massgen/configs/docs/DISCORD_MCP_SETUP.md +208 -0
- massgen/configs/docs/TWITTER_MCP_ENESCINAR_SETUP.md +82 -0
- massgen/configs/providers/azure/azure_openai_multi.yaml +21 -0
- massgen/configs/providers/azure/azure_openai_single.yaml +19 -0
- massgen/configs/providers/claude/claude.yaml +14 -0
- massgen/configs/providers/gemini/gemini_gpt5nano.yaml +28 -0
- massgen/configs/providers/local/lmstudio.yaml +11 -0
- massgen/configs/providers/openai/gpt5.yaml +46 -0
- massgen/configs/providers/openai/gpt5_nano.yaml +46 -0
- massgen/configs/providers/others/grok_single_agent.yaml +19 -0
- massgen/configs/providers/others/zai_coding_team.yaml +108 -0
- massgen/configs/providers/others/zai_glm45.yaml +12 -0
- massgen/configs/{creative_team.yaml → teams/creative/creative_team.yaml} +16 -6
- massgen/configs/{travel_planning.yaml → teams/creative/travel_planning.yaml} +16 -6
- massgen/configs/{news_analysis.yaml → teams/research/news_analysis.yaml} +16 -6
- massgen/configs/{research_team.yaml → teams/research/research_team.yaml} +15 -7
- massgen/configs/{technical_analysis.yaml → teams/research/technical_analysis.yaml} +16 -6
- massgen/configs/tools/code-execution/basic_command_execution.yaml +25 -0
- massgen/configs/tools/code-execution/code_execution_use_case_simple.yaml +41 -0
- massgen/configs/tools/code-execution/docker_claude_code.yaml +32 -0
- massgen/configs/tools/code-execution/docker_multi_agent.yaml +32 -0
- massgen/configs/tools/code-execution/docker_simple.yaml +29 -0
- massgen/configs/tools/code-execution/docker_with_resource_limits.yaml +32 -0
- massgen/configs/tools/code-execution/multi_agent_playwright_automation.yaml +57 -0
- massgen/configs/tools/filesystem/cc_gpt5_gemini_filesystem.yaml +34 -0
- massgen/configs/tools/filesystem/claude_code_context_sharing.yaml +68 -0
- massgen/configs/tools/filesystem/claude_code_flash2.5.yaml +43 -0
- massgen/configs/tools/filesystem/claude_code_flash2.5_gptoss.yaml +49 -0
- massgen/configs/tools/filesystem/claude_code_gpt5nano.yaml +31 -0
- massgen/configs/tools/filesystem/claude_code_single.yaml +40 -0
- massgen/configs/tools/filesystem/fs_permissions_test.yaml +87 -0
- massgen/configs/tools/filesystem/gemini_gemini_workspace_cleanup.yaml +54 -0
- massgen/configs/tools/filesystem/gemini_gpt5_filesystem_casestudy.yaml +30 -0
- massgen/configs/tools/filesystem/gemini_gpt5nano_file_context_path.yaml +43 -0
- massgen/configs/tools/filesystem/gemini_gpt5nano_protected_paths.yaml +45 -0
- massgen/configs/tools/filesystem/gpt5mini_cc_fs_context_path.yaml +31 -0
- massgen/configs/tools/filesystem/grok4_gpt5_gemini_filesystem.yaml +32 -0
- massgen/configs/tools/filesystem/multiturn/grok4_gpt5_claude_code_filesystem_multiturn.yaml +58 -0
- massgen/configs/tools/filesystem/multiturn/grok4_gpt5_gemini_filesystem_multiturn.yaml +58 -0
- massgen/configs/tools/filesystem/multiturn/two_claude_code_filesystem_multiturn.yaml +47 -0
- massgen/configs/tools/filesystem/multiturn/two_gemini_flash_filesystem_multiturn.yaml +48 -0
- massgen/configs/tools/mcp/claude_code_discord_mcp_example.yaml +27 -0
- massgen/configs/tools/mcp/claude_code_simple_mcp.yaml +35 -0
- massgen/configs/tools/mcp/claude_code_twitter_mcp_example.yaml +32 -0
- massgen/configs/tools/mcp/claude_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/claude_mcp_test.yaml +27 -0
- massgen/configs/tools/mcp/five_agents_travel_mcp_test.yaml +157 -0
- massgen/configs/tools/mcp/five_agents_weather_mcp_test.yaml +103 -0
- massgen/configs/tools/mcp/gemini_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/gemini_mcp_filesystem_test.yaml +23 -0
- massgen/configs/tools/mcp/gemini_mcp_filesystem_test_sharing.yaml +23 -0
- massgen/configs/tools/mcp/gemini_mcp_filesystem_test_single_agent.yaml +17 -0
- massgen/configs/tools/mcp/gemini_mcp_filesystem_test_with_claude_code.yaml +24 -0
- massgen/configs/tools/mcp/gemini_mcp_test.yaml +27 -0
- massgen/configs/tools/mcp/gemini_notion_mcp.yaml +52 -0
- massgen/configs/tools/mcp/gpt5_nano_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/gpt5_nano_mcp_test.yaml +27 -0
- massgen/configs/tools/mcp/gpt5mini_claude_code_discord_mcp_example.yaml +38 -0
- massgen/configs/tools/mcp/gpt_oss_mcp_example.yaml +25 -0
- massgen/configs/tools/mcp/gpt_oss_mcp_test.yaml +28 -0
- massgen/configs/tools/mcp/grok3_mini_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/grok3_mini_mcp_test.yaml +27 -0
- massgen/configs/tools/mcp/multimcp_gemini.yaml +111 -0
- massgen/configs/tools/mcp/qwen_api_mcp_example.yaml +25 -0
- massgen/configs/tools/mcp/qwen_api_mcp_test.yaml +28 -0
- massgen/configs/tools/mcp/qwen_local_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/qwen_local_mcp_test.yaml +27 -0
- massgen/configs/tools/planning/five_agents_discord_mcp_planning_mode.yaml +140 -0
- massgen/configs/tools/planning/five_agents_filesystem_mcp_planning_mode.yaml +151 -0
- massgen/configs/tools/planning/five_agents_notion_mcp_planning_mode.yaml +151 -0
- massgen/configs/tools/planning/five_agents_twitter_mcp_planning_mode.yaml +155 -0
- massgen/configs/tools/planning/gpt5_mini_case_study_mcp_planning_mode.yaml +73 -0
- massgen/configs/tools/web-search/claude_streamable_http_test.yaml +43 -0
- massgen/configs/tools/web-search/gemini_streamable_http_test.yaml +43 -0
- massgen/configs/tools/web-search/gpt5_mini_streamable_http_test.yaml +43 -0
- massgen/configs/tools/web-search/gpt_oss_streamable_http_test.yaml +44 -0
- massgen/configs/tools/web-search/grok3_mini_streamable_http_test.yaml +43 -0
- massgen/configs/tools/web-search/qwen_api_streamable_http_test.yaml +44 -0
- massgen/configs/tools/web-search/qwen_local_streamable_http_test.yaml +43 -0
- massgen/coordination_tracker.py +708 -0
- massgen/docker/README.md +462 -0
- massgen/filesystem_manager/__init__.py +21 -0
- massgen/filesystem_manager/_base.py +9 -0
- massgen/filesystem_manager/_code_execution_server.py +545 -0
- massgen/filesystem_manager/_docker_manager.py +477 -0
- massgen/filesystem_manager/_file_operation_tracker.py +248 -0
- massgen/filesystem_manager/_filesystem_manager.py +813 -0
- massgen/filesystem_manager/_path_permission_manager.py +1261 -0
- massgen/filesystem_manager/_workspace_tools_server.py +1815 -0
- massgen/formatter/__init__.py +10 -0
- massgen/formatter/_chat_completions_formatter.py +284 -0
- massgen/formatter/_claude_formatter.py +235 -0
- massgen/formatter/_formatter_base.py +156 -0
- massgen/formatter/_response_formatter.py +263 -0
- massgen/frontend/__init__.py +1 -2
- massgen/frontend/coordination_ui.py +471 -286
- massgen/frontend/displays/base_display.py +56 -11
- massgen/frontend/displays/create_coordination_table.py +1956 -0
- massgen/frontend/displays/rich_terminal_display.py +1259 -619
- massgen/frontend/displays/simple_display.py +9 -4
- massgen/frontend/displays/terminal_display.py +27 -68
- massgen/logger_config.py +681 -0
- massgen/mcp_tools/README.md +232 -0
- massgen/mcp_tools/__init__.py +105 -0
- massgen/mcp_tools/backend_utils.py +1035 -0
- massgen/mcp_tools/circuit_breaker.py +195 -0
- massgen/mcp_tools/client.py +894 -0
- massgen/mcp_tools/config_validator.py +138 -0
- massgen/mcp_tools/docs/circuit_breaker.md +646 -0
- massgen/mcp_tools/docs/client.md +950 -0
- massgen/mcp_tools/docs/config_validator.md +478 -0
- massgen/mcp_tools/docs/exceptions.md +1165 -0
- massgen/mcp_tools/docs/security.md +854 -0
- massgen/mcp_tools/exceptions.py +338 -0
- massgen/mcp_tools/hooks.py +212 -0
- massgen/mcp_tools/security.py +780 -0
- massgen/message_templates.py +342 -64
- massgen/orchestrator.py +1515 -241
- massgen/stream_chunk/__init__.py +35 -0
- massgen/stream_chunk/base.py +92 -0
- massgen/stream_chunk/multimodal.py +237 -0
- massgen/stream_chunk/text.py +162 -0
- massgen/tests/mcp_test_server.py +150 -0
- massgen/tests/multi_turn_conversation_design.md +0 -8
- massgen/tests/test_azure_openai_backend.py +156 -0
- massgen/tests/test_backend_capabilities.py +262 -0
- massgen/tests/test_backend_event_loop_all.py +179 -0
- massgen/tests/test_chat_completions_refactor.py +142 -0
- massgen/tests/test_claude_backend.py +15 -28
- massgen/tests/test_claude_code.py +268 -0
- massgen/tests/test_claude_code_context_sharing.py +233 -0
- massgen/tests/test_claude_code_orchestrator.py +175 -0
- massgen/tests/test_cli_backends.py +180 -0
- massgen/tests/test_code_execution.py +679 -0
- massgen/tests/test_external_agent_backend.py +134 -0
- massgen/tests/test_final_presentation_fallback.py +237 -0
- massgen/tests/test_gemini_planning_mode.py +351 -0
- massgen/tests/test_grok_backend.py +7 -10
- massgen/tests/test_http_mcp_server.py +42 -0
- massgen/tests/test_integration_simple.py +198 -0
- massgen/tests/test_mcp_blocking.py +125 -0
- massgen/tests/test_message_context_building.py +29 -47
- massgen/tests/test_orchestrator_final_presentation.py +48 -0
- massgen/tests/test_path_permission_manager.py +2087 -0
- massgen/tests/test_rich_terminal_display.py +14 -13
- massgen/tests/test_timeout.py +133 -0
- massgen/tests/test_v3_3agents.py +11 -12
- massgen/tests/test_v3_simple.py +8 -13
- massgen/tests/test_v3_three_agents.py +11 -18
- massgen/tests/test_v3_two_agents.py +8 -13
- massgen/token_manager/__init__.py +7 -0
- massgen/token_manager/token_manager.py +400 -0
- massgen/utils.py +52 -16
- massgen/v1/agent.py +45 -91
- massgen/v1/agents.py +18 -53
- massgen/v1/backends/gemini.py +50 -153
- massgen/v1/backends/grok.py +21 -54
- massgen/v1/backends/oai.py +39 -111
- massgen/v1/cli.py +36 -93
- massgen/v1/config.py +8 -12
- massgen/v1/logging.py +43 -127
- massgen/v1/main.py +18 -32
- massgen/v1/orchestrator.py +68 -209
- massgen/v1/streaming_display.py +62 -163
- massgen/v1/tools.py +8 -12
- massgen/v1/types.py +9 -23
- massgen/v1/utils.py +5 -23
- massgen-0.1.0.dist-info/METADATA +1245 -0
- massgen-0.1.0.dist-info/RECORD +273 -0
- massgen-0.1.0.dist-info/entry_points.txt +2 -0
- massgen/frontend/logging/__init__.py +0 -9
- massgen/frontend/logging/realtime_logger.py +0 -197
- massgen-0.0.3.dist-info/METADATA +0 -568
- massgen-0.0.3.dist-info/RECORD +0 -76
- massgen-0.0.3.dist-info/entry_points.txt +0 -2
- /massgen/backend/{Function calling openai responses.md → docs/Function calling openai responses.md} +0 -0
- {massgen-0.0.3.dist-info → massgen-0.1.0.dist-info}/WHEEL +0 -0
- {massgen-0.0.3.dist-info → massgen-0.1.0.dist-info}/licenses/LICENSE +0 -0
- {massgen-0.0.3.dist-info → massgen-0.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Docker Container Manager for MassGen
|
|
4
|
+
|
|
5
|
+
Manages Docker containers for isolated command execution.
|
|
6
|
+
Provides strong filesystem isolation by executing commands inside containers
|
|
7
|
+
while keeping MCP servers on the host.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
# Check if docker is available
|
|
19
|
+
try:
|
|
20
|
+
import docker
|
|
21
|
+
from docker.errors import DockerException, ImageNotFound, NotFound
|
|
22
|
+
from docker.models.containers import Container
|
|
23
|
+
|
|
24
|
+
DOCKER_AVAILABLE = True
|
|
25
|
+
except ImportError:
|
|
26
|
+
DOCKER_AVAILABLE = False
|
|
27
|
+
logger.warning("Docker Python library not available. Install with: pip install docker")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DockerManager:
|
|
31
|
+
"""
|
|
32
|
+
Manages Docker containers for isolated command execution.
|
|
33
|
+
|
|
34
|
+
Each agent gets a persistent container for the orchestration session:
|
|
35
|
+
- Volume mounts for workspace and context paths
|
|
36
|
+
- Network isolation (configurable)
|
|
37
|
+
- Resource limits (CPU, memory)
|
|
38
|
+
- Commands executed via docker exec
|
|
39
|
+
- State persists across turns (packages stay installed)
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
image: str = "massgen/mcp-runtime:latest",
|
|
45
|
+
network_mode: str = "none",
|
|
46
|
+
memory_limit: Optional[str] = None,
|
|
47
|
+
cpu_limit: Optional[float] = None,
|
|
48
|
+
):
|
|
49
|
+
"""
|
|
50
|
+
Initialize Docker manager.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
image: Docker image to use for containers
|
|
54
|
+
network_mode: Network mode (none/bridge/host)
|
|
55
|
+
memory_limit: Memory limit (e.g., "2g", "512m")
|
|
56
|
+
cpu_limit: CPU limit (e.g., 2.0 for 2 CPUs)
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
RuntimeError: If Docker is not available or cannot connect
|
|
60
|
+
"""
|
|
61
|
+
if not DOCKER_AVAILABLE:
|
|
62
|
+
raise RuntimeError("Docker Python library not available. Install with: pip install docker")
|
|
63
|
+
|
|
64
|
+
self.image = image
|
|
65
|
+
self.network_mode = network_mode
|
|
66
|
+
self.memory_limit = memory_limit
|
|
67
|
+
self.cpu_limit = cpu_limit
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
self.client = docker.from_env()
|
|
71
|
+
# Test connection
|
|
72
|
+
self.client.ping()
|
|
73
|
+
|
|
74
|
+
# Get Docker version info for logging
|
|
75
|
+
version_info = self.client.version()
|
|
76
|
+
docker_version = version_info.get("Version", "unknown")
|
|
77
|
+
api_version = version_info.get("ApiVersion", "unknown")
|
|
78
|
+
|
|
79
|
+
logger.info("🐳 [Docker] Client initialized successfully")
|
|
80
|
+
logger.info(f" Docker version: {docker_version}")
|
|
81
|
+
logger.info(f" API version: {api_version}")
|
|
82
|
+
except DockerException as e:
|
|
83
|
+
logger.error(f"❌ [Docker] Failed to connect to Docker daemon: {e}")
|
|
84
|
+
raise RuntimeError(f"Failed to connect to Docker: {e}")
|
|
85
|
+
|
|
86
|
+
self.containers: Dict[str, Container] = {} # agent_id -> container
|
|
87
|
+
|
|
88
|
+
def ensure_image_exists(self) -> None:
|
|
89
|
+
"""
|
|
90
|
+
Ensure the Docker image exists locally.
|
|
91
|
+
|
|
92
|
+
Pulls the image if not found locally.
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
RuntimeError: If image cannot be pulled
|
|
96
|
+
"""
|
|
97
|
+
try:
|
|
98
|
+
self.client.images.get(self.image)
|
|
99
|
+
logger.info(f"✅ [Docker] Image '{self.image}' found locally")
|
|
100
|
+
except ImageNotFound:
|
|
101
|
+
logger.info(f"📥 [Docker] Image '{self.image}' not found locally, pulling...")
|
|
102
|
+
try:
|
|
103
|
+
self.client.images.pull(self.image)
|
|
104
|
+
logger.info(f"✅ [Docker] Successfully pulled image '{self.image}'")
|
|
105
|
+
except DockerException as e:
|
|
106
|
+
raise RuntimeError(f"Failed to pull Docker image '{self.image}': {e}")
|
|
107
|
+
|
|
108
|
+
def create_container(
|
|
109
|
+
self,
|
|
110
|
+
agent_id: str,
|
|
111
|
+
workspace_path: Path,
|
|
112
|
+
temp_workspace_path: Optional[Path] = None,
|
|
113
|
+
context_paths: Optional[List[Dict[str, Any]]] = None,
|
|
114
|
+
) -> str:
|
|
115
|
+
"""
|
|
116
|
+
Create and start a persistent Docker container for an agent.
|
|
117
|
+
|
|
118
|
+
The container runs for the entire orchestration session and maintains state
|
|
119
|
+
across command executions (installed packages, generated files, etc.).
|
|
120
|
+
|
|
121
|
+
IMPORTANT: Paths are mounted at the SAME location as on the host to maintain
|
|
122
|
+
path transparency. The LLM sees identical paths whether in Docker or local mode.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
agent_id: Unique identifier for the agent
|
|
126
|
+
workspace_path: Path to agent's workspace (mounted at same path, read-write)
|
|
127
|
+
temp_workspace_path: Path to shared temp workspace (mounted at same path, read-only)
|
|
128
|
+
context_paths: List of context path dicts with 'path', 'permission', and optional 'name' keys
|
|
129
|
+
(each mounted at its host path)
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Container ID
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
RuntimeError: If container creation fails
|
|
136
|
+
"""
|
|
137
|
+
if agent_id in self.containers:
|
|
138
|
+
logger.warning(f"⚠️ [Docker] Container for agent {agent_id} already exists")
|
|
139
|
+
return self.containers[agent_id].id
|
|
140
|
+
|
|
141
|
+
# Ensure image exists
|
|
142
|
+
self.ensure_image_exists()
|
|
143
|
+
|
|
144
|
+
# Check for and remove any existing container with the same name
|
|
145
|
+
container_name = f"massgen-{agent_id}"
|
|
146
|
+
try:
|
|
147
|
+
existing = self.client.containers.get(container_name)
|
|
148
|
+
logger.warning(
|
|
149
|
+
f"🔄 [Docker] Found existing container '{container_name}' (id: {existing.short_id}), removing it",
|
|
150
|
+
)
|
|
151
|
+
existing.remove(force=True)
|
|
152
|
+
except NotFound:
|
|
153
|
+
# No existing container, this is expected
|
|
154
|
+
pass
|
|
155
|
+
except DockerException as e:
|
|
156
|
+
logger.warning(f"⚠️ [Docker] Error checking for existing container '{container_name}': {e}")
|
|
157
|
+
|
|
158
|
+
logger.info(f"🐳 [Docker] Creating container for agent '{agent_id}'")
|
|
159
|
+
logger.info(f" Image: {self.image}")
|
|
160
|
+
logger.info(f" Network: {self.network_mode}")
|
|
161
|
+
if self.memory_limit:
|
|
162
|
+
logger.info(f" Memory limit: {self.memory_limit}")
|
|
163
|
+
if self.cpu_limit:
|
|
164
|
+
logger.info(f" CPU limit: {self.cpu_limit} cores")
|
|
165
|
+
|
|
166
|
+
# Build volume mounts
|
|
167
|
+
# IMPORTANT: Mount paths at the SAME location as on host to avoid path confusion
|
|
168
|
+
# This makes Docker completely transparent to the LLM - it sees identical paths
|
|
169
|
+
volumes = {}
|
|
170
|
+
mount_info = []
|
|
171
|
+
|
|
172
|
+
# Mount agent workspace (read-write) at the SAME path as host
|
|
173
|
+
workspace_path = workspace_path.resolve()
|
|
174
|
+
volumes[str(workspace_path)] = {"bind": str(workspace_path), "mode": "rw"}
|
|
175
|
+
mount_info.append(f" {workspace_path} ← {workspace_path} (rw)")
|
|
176
|
+
|
|
177
|
+
# Mount temp workspace (read-only) at the SAME path as host
|
|
178
|
+
if temp_workspace_path:
|
|
179
|
+
temp_workspace_path = temp_workspace_path.resolve()
|
|
180
|
+
volumes[str(temp_workspace_path)] = {"bind": str(temp_workspace_path), "mode": "ro"}
|
|
181
|
+
mount_info.append(f" {temp_workspace_path} ← {temp_workspace_path} (ro)")
|
|
182
|
+
|
|
183
|
+
# Mount context paths at the SAME paths as host
|
|
184
|
+
if context_paths:
|
|
185
|
+
for ctx_path_config in context_paths:
|
|
186
|
+
ctx_path = Path(ctx_path_config["path"]).resolve()
|
|
187
|
+
permission = ctx_path_config.get("permission", "read")
|
|
188
|
+
mode = "rw" if permission == "write" else "ro"
|
|
189
|
+
|
|
190
|
+
volumes[str(ctx_path)] = {"bind": str(ctx_path), "mode": mode}
|
|
191
|
+
mount_info.append(f" {ctx_path} ← {ctx_path} ({mode})")
|
|
192
|
+
|
|
193
|
+
# Log volume mounts
|
|
194
|
+
if mount_info:
|
|
195
|
+
logger.info(" Volume mounts:")
|
|
196
|
+
for mount_line in mount_info:
|
|
197
|
+
logger.info(mount_line)
|
|
198
|
+
|
|
199
|
+
# Build resource limits
|
|
200
|
+
resource_config = {}
|
|
201
|
+
if self.memory_limit:
|
|
202
|
+
resource_config["mem_limit"] = self.memory_limit
|
|
203
|
+
if self.cpu_limit:
|
|
204
|
+
resource_config["nano_cpus"] = int(self.cpu_limit * 1e9)
|
|
205
|
+
|
|
206
|
+
# Container configuration
|
|
207
|
+
container_config = {
|
|
208
|
+
"image": self.image,
|
|
209
|
+
"name": container_name,
|
|
210
|
+
"command": ["tail", "-f", "/dev/null"], # Keep container running
|
|
211
|
+
"detach": True,
|
|
212
|
+
"volumes": volumes,
|
|
213
|
+
"working_dir": str(workspace_path), # Use host workspace path
|
|
214
|
+
"network_mode": self.network_mode,
|
|
215
|
+
"auto_remove": False, # Manual cleanup for better control
|
|
216
|
+
"stdin_open": True,
|
|
217
|
+
"tty": True,
|
|
218
|
+
**resource_config,
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
# Create and start container
|
|
223
|
+
container = self.client.containers.run(**container_config)
|
|
224
|
+
self.containers[agent_id] = container
|
|
225
|
+
|
|
226
|
+
# Get container info for logging
|
|
227
|
+
container.reload() # Refresh container state
|
|
228
|
+
status = container.status
|
|
229
|
+
|
|
230
|
+
logger.info("✅ [Docker] Container created successfully")
|
|
231
|
+
logger.info(f" Container ID: {container.short_id}")
|
|
232
|
+
logger.info(f" Container name: {container_name}")
|
|
233
|
+
logger.info(f" Status: {status}")
|
|
234
|
+
|
|
235
|
+
# Show how to inspect the container
|
|
236
|
+
logger.debug(f"💡 [Docker] Inspect container: docker inspect {container.short_id}")
|
|
237
|
+
logger.debug(f"💡 [Docker] View logs: docker logs {container.short_id}")
|
|
238
|
+
logger.debug(f"💡 [Docker] Execute commands: docker exec -it {container.short_id} /bin/bash")
|
|
239
|
+
|
|
240
|
+
return container.id
|
|
241
|
+
|
|
242
|
+
except DockerException as e:
|
|
243
|
+
logger.error(f"❌ [Docker] Failed to create container for agent {agent_id}: {e}")
|
|
244
|
+
raise RuntimeError(f"Failed to create Docker container for agent {agent_id}: {e}")
|
|
245
|
+
|
|
246
|
+
def get_container(self, agent_id: str) -> Optional[Container]:
|
|
247
|
+
"""
|
|
248
|
+
Get container for an agent.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
agent_id: Agent identifier
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Container object or None if not found
|
|
255
|
+
"""
|
|
256
|
+
return self.containers.get(agent_id)
|
|
257
|
+
|
|
258
|
+
def exec_command(
|
|
259
|
+
self,
|
|
260
|
+
agent_id: str,
|
|
261
|
+
command: str,
|
|
262
|
+
workdir: Optional[str] = None,
|
|
263
|
+
timeout: Optional[int] = None,
|
|
264
|
+
) -> Dict[str, Any]:
|
|
265
|
+
"""
|
|
266
|
+
Execute a command inside the agent's container.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
agent_id: Agent identifier
|
|
270
|
+
command: Command to execute (as string, will be run in shell)
|
|
271
|
+
workdir: Working directory (uses host path - same path is mounted in container)
|
|
272
|
+
timeout: Command timeout in seconds (implemented using threading)
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Dictionary with:
|
|
276
|
+
- success: bool (True if exit_code == 0)
|
|
277
|
+
- exit_code: int
|
|
278
|
+
- stdout: str
|
|
279
|
+
- stderr: str (combined with stdout in Docker exec)
|
|
280
|
+
- execution_time: float
|
|
281
|
+
- command: str
|
|
282
|
+
- work_dir: str
|
|
283
|
+
|
|
284
|
+
Raises:
|
|
285
|
+
ValueError: If container not found
|
|
286
|
+
RuntimeError: If execution fails
|
|
287
|
+
"""
|
|
288
|
+
container = self.containers.get(agent_id)
|
|
289
|
+
if not container:
|
|
290
|
+
raise ValueError(f"No container found for agent {agent_id}")
|
|
291
|
+
|
|
292
|
+
# Default workdir is the container's default working dir (set to workspace_path at creation)
|
|
293
|
+
effective_workdir = workdir if workdir else None
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
# Run command through shell to support pipes, redirects, etc.
|
|
297
|
+
exec_config = {
|
|
298
|
+
"cmd": ["/bin/sh", "-c", command],
|
|
299
|
+
"stdout": True,
|
|
300
|
+
"stderr": True,
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if effective_workdir:
|
|
304
|
+
exec_config["workdir"] = effective_workdir
|
|
305
|
+
|
|
306
|
+
logger.debug(f"🔧 [Docker] Executing in container {container.short_id}: {command}")
|
|
307
|
+
|
|
308
|
+
start_time = time.time()
|
|
309
|
+
|
|
310
|
+
# Handle timeout using threading
|
|
311
|
+
if timeout:
|
|
312
|
+
result_container = {}
|
|
313
|
+
exception_container = {}
|
|
314
|
+
|
|
315
|
+
def run_exec():
|
|
316
|
+
try:
|
|
317
|
+
result_container["data"] = container.exec_run(**exec_config)
|
|
318
|
+
except Exception as e:
|
|
319
|
+
exception_container["error"] = e
|
|
320
|
+
|
|
321
|
+
thread = threading.Thread(target=run_exec)
|
|
322
|
+
thread.daemon = True
|
|
323
|
+
thread.start()
|
|
324
|
+
thread.join(timeout=timeout)
|
|
325
|
+
|
|
326
|
+
execution_time = time.time() - start_time
|
|
327
|
+
|
|
328
|
+
if thread.is_alive():
|
|
329
|
+
# Timeout occurred
|
|
330
|
+
logger.warning(f"⚠️ [Docker] Command timed out after {timeout}s: {command}")
|
|
331
|
+
return {
|
|
332
|
+
"success": False,
|
|
333
|
+
"exit_code": -1,
|
|
334
|
+
"stdout": "",
|
|
335
|
+
"stderr": f"Command timed out after {timeout} seconds",
|
|
336
|
+
"execution_time": execution_time,
|
|
337
|
+
"command": command,
|
|
338
|
+
"work_dir": effective_workdir or "(container default)",
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if "error" in exception_container:
|
|
342
|
+
raise exception_container["error"]
|
|
343
|
+
|
|
344
|
+
exit_code, output = result_container["data"]
|
|
345
|
+
else:
|
|
346
|
+
# No timeout - execute directly
|
|
347
|
+
exit_code, output = container.exec_run(**exec_config)
|
|
348
|
+
execution_time = time.time() - start_time
|
|
349
|
+
|
|
350
|
+
# Docker exec_run combines stdout and stderr
|
|
351
|
+
output_str = output.decode("utf-8") if isinstance(output, bytes) else output
|
|
352
|
+
|
|
353
|
+
if exit_code != 0:
|
|
354
|
+
logger.debug(f"⚠️ [Docker] Command exited with code {exit_code}")
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
"success": exit_code == 0,
|
|
358
|
+
"exit_code": exit_code,
|
|
359
|
+
"stdout": output_str,
|
|
360
|
+
"stderr": "", # Docker exec_run combines stdout/stderr
|
|
361
|
+
"execution_time": execution_time,
|
|
362
|
+
"command": command,
|
|
363
|
+
"work_dir": effective_workdir or "(container default)",
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
except DockerException as e:
|
|
367
|
+
logger.error(f"❌ [Docker] Failed to execute command in container: {e}")
|
|
368
|
+
raise RuntimeError(f"Failed to execute command in container: {e}")
|
|
369
|
+
|
|
370
|
+
def stop_container(self, agent_id: str, timeout: int = 10) -> None:
|
|
371
|
+
"""
|
|
372
|
+
Stop a container gracefully.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
agent_id: Agent identifier
|
|
376
|
+
timeout: Seconds to wait before killing
|
|
377
|
+
|
|
378
|
+
Raises:
|
|
379
|
+
ValueError: If container not found
|
|
380
|
+
"""
|
|
381
|
+
container = self.containers.get(agent_id)
|
|
382
|
+
if not container:
|
|
383
|
+
raise ValueError(f"No container found for agent {agent_id}")
|
|
384
|
+
|
|
385
|
+
try:
|
|
386
|
+
logger.info(f"🛑 [Docker] Stopping container {container.short_id} for agent {agent_id}")
|
|
387
|
+
container.stop(timeout=timeout)
|
|
388
|
+
logger.info("✅ [Docker] Container stopped successfully")
|
|
389
|
+
except DockerException as e:
|
|
390
|
+
logger.error(f"❌ [Docker] Failed to stop container for agent {agent_id}: {e}")
|
|
391
|
+
|
|
392
|
+
def remove_container(self, agent_id: str, force: bool = False) -> None:
|
|
393
|
+
"""
|
|
394
|
+
Remove a container.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
agent_id: Agent identifier
|
|
398
|
+
force: Force removal even if running
|
|
399
|
+
|
|
400
|
+
Raises:
|
|
401
|
+
ValueError: If container not found
|
|
402
|
+
"""
|
|
403
|
+
container = self.containers.get(agent_id)
|
|
404
|
+
if not container:
|
|
405
|
+
raise ValueError(f"No container found for agent {agent_id}")
|
|
406
|
+
|
|
407
|
+
try:
|
|
408
|
+
container_id = container.short_id
|
|
409
|
+
logger.info(f"🗑️ [Docker] Removing container {container_id} for agent {agent_id}")
|
|
410
|
+
container.remove(force=force)
|
|
411
|
+
del self.containers[agent_id]
|
|
412
|
+
logger.info("✅ [Docker] Container removed successfully")
|
|
413
|
+
except DockerException as e:
|
|
414
|
+
logger.error(f"❌ [Docker] Failed to remove container for agent {agent_id}: {e}")
|
|
415
|
+
|
|
416
|
+
def cleanup(self, agent_id: Optional[str] = None) -> None:
|
|
417
|
+
"""
|
|
418
|
+
Clean up containers.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
agent_id: If provided, cleanup specific agent. Otherwise cleanup all.
|
|
422
|
+
"""
|
|
423
|
+
if agent_id:
|
|
424
|
+
# Cleanup specific agent
|
|
425
|
+
if agent_id in self.containers:
|
|
426
|
+
logger.info(f"🧹 [Docker] Cleaning up container for agent {agent_id}")
|
|
427
|
+
try:
|
|
428
|
+
self.stop_container(agent_id)
|
|
429
|
+
self.remove_container(agent_id, force=True)
|
|
430
|
+
except Exception as e:
|
|
431
|
+
logger.error(f"❌ [Docker] Error cleaning up container for agent {agent_id}: {e}")
|
|
432
|
+
else:
|
|
433
|
+
# Cleanup all containers
|
|
434
|
+
if self.containers:
|
|
435
|
+
logger.info(f"🧹 [Docker] Cleaning up {len(self.containers)} container(s)")
|
|
436
|
+
for aid in list(self.containers.keys()):
|
|
437
|
+
try:
|
|
438
|
+
self.stop_container(aid)
|
|
439
|
+
self.remove_container(aid, force=True)
|
|
440
|
+
except Exception as e:
|
|
441
|
+
logger.error(f"❌ [Docker] Error cleaning up container for agent {aid}: {e}")
|
|
442
|
+
|
|
443
|
+
def log_container_info(self, agent_id: str) -> None:
|
|
444
|
+
"""
|
|
445
|
+
Log detailed container information (useful for debugging).
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
agent_id: Agent identifier
|
|
449
|
+
"""
|
|
450
|
+
container = self.containers.get(agent_id)
|
|
451
|
+
if not container:
|
|
452
|
+
logger.warning(f"⚠️ [Docker] No container found for agent {agent_id}")
|
|
453
|
+
return
|
|
454
|
+
|
|
455
|
+
try:
|
|
456
|
+
container.reload() # Refresh state
|
|
457
|
+
|
|
458
|
+
logger.info(f"📊 [Docker] Container information for agent '{agent_id}':")
|
|
459
|
+
logger.info(f" ID: {container.short_id}")
|
|
460
|
+
logger.info(f" Name: {container.name}")
|
|
461
|
+
logger.info(f" Status: {container.status}")
|
|
462
|
+
logger.info(f" Network: {self.network_mode}")
|
|
463
|
+
if self.memory_limit:
|
|
464
|
+
logger.info(f" Memory limit: {self.memory_limit}")
|
|
465
|
+
if self.cpu_limit:
|
|
466
|
+
logger.info(f" CPU limit: {self.cpu_limit} cores")
|
|
467
|
+
except Exception as e:
|
|
468
|
+
logger.warning(f"⚠️ [Docker] Could not log container info: {e}")
|
|
469
|
+
|
|
470
|
+
def __del__(self):
|
|
471
|
+
"""Cleanup all containers on deletion."""
|
|
472
|
+
try:
|
|
473
|
+
if hasattr(self, "containers") and self.containers:
|
|
474
|
+
self.cleanup()
|
|
475
|
+
except Exception:
|
|
476
|
+
# Silently fail during cleanup - already logged in cleanup()
|
|
477
|
+
pass
|