massgen 0.1.3__py3-none-any.whl → 0.1.4__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.

Files changed (58) hide show
  1. massgen/__init__.py +1 -1
  2. massgen/api_params_handler/_chat_completions_api_params_handler.py +4 -0
  3. massgen/api_params_handler/_claude_api_params_handler.py +4 -0
  4. massgen/api_params_handler/_gemini_api_params_handler.py +4 -0
  5. massgen/api_params_handler/_response_api_params_handler.py +4 -0
  6. massgen/backend/base_with_custom_tool_and_mcp.py +25 -5
  7. massgen/backend/docs/permissions_and_context_files.md +2 -2
  8. massgen/backend/response.py +2 -0
  9. massgen/configs/README.md +49 -40
  10. massgen/configs/tools/custom_tools/crawl4ai_example.yaml +55 -0
  11. massgen/configs/tools/custom_tools/multimodal_tools/text_to_file_generation_multi.yaml +61 -0
  12. massgen/configs/tools/custom_tools/multimodal_tools/text_to_file_generation_single.yaml +29 -0
  13. massgen/configs/tools/custom_tools/multimodal_tools/text_to_image_generation_multi.yaml +51 -0
  14. massgen/configs/tools/custom_tools/multimodal_tools/text_to_image_generation_single.yaml +33 -0
  15. massgen/configs/tools/custom_tools/multimodal_tools/text_to_speech_generation_multi.yaml +55 -0
  16. massgen/configs/tools/custom_tools/multimodal_tools/text_to_speech_generation_single.yaml +33 -0
  17. massgen/configs/tools/custom_tools/multimodal_tools/text_to_video_generation_multi.yaml +47 -0
  18. massgen/configs/tools/custom_tools/multimodal_tools/text_to_video_generation_single.yaml +29 -0
  19. massgen/configs/tools/custom_tools/multimodal_tools/understand_audio.yaml +1 -1
  20. massgen/configs/tools/custom_tools/multimodal_tools/understand_file.yaml +1 -1
  21. massgen/configs/tools/custom_tools/multimodal_tools/understand_image.yaml +1 -1
  22. massgen/configs/tools/custom_tools/multimodal_tools/understand_video.yaml +1 -1
  23. massgen/configs/tools/custom_tools/multimodal_tools/youtube_video_analysis.yaml +1 -1
  24. massgen/filesystem_manager/_filesystem_manager.py +1 -0
  25. massgen/filesystem_manager/_path_permission_manager.py +148 -0
  26. massgen/message_templates.py +160 -12
  27. massgen/orchestrator.py +16 -0
  28. massgen/tests/test_binary_file_blocking.py +274 -0
  29. massgen/tests/test_case_studies.md +12 -12
  30. massgen/tests/test_multimodal_size_limits.py +407 -0
  31. massgen/tool/_manager.py +7 -2
  32. massgen/tool/_multimodal_tools/image_to_image_generation.py +293 -0
  33. massgen/tool/_multimodal_tools/text_to_file_generation.py +455 -0
  34. massgen/tool/_multimodal_tools/text_to_image_generation.py +222 -0
  35. massgen/tool/_multimodal_tools/text_to_speech_continue_generation.py +226 -0
  36. massgen/tool/_multimodal_tools/text_to_speech_transcription_generation.py +217 -0
  37. massgen/tool/_multimodal_tools/text_to_video_generation.py +223 -0
  38. massgen/tool/_multimodal_tools/understand_audio.py +19 -1
  39. massgen/tool/_multimodal_tools/understand_file.py +6 -1
  40. massgen/tool/_multimodal_tools/understand_image.py +112 -8
  41. massgen/tool/_multimodal_tools/understand_video.py +32 -5
  42. massgen/tool/_web_tools/crawl4ai_tool.py +718 -0
  43. massgen/tool/docs/multimodal_tools.md +589 -0
  44. {massgen-0.1.3.dist-info → massgen-0.1.4.dist-info}/METADATA +96 -69
  45. {massgen-0.1.3.dist-info → massgen-0.1.4.dist-info}/RECORD +49 -40
  46. massgen/configs/tools/custom_tools/crawl4ai_mcp_example.yaml +0 -67
  47. massgen/configs/tools/custom_tools/crawl4ai_multi_agent_example.yaml +0 -68
  48. massgen/configs/tools/custom_tools/multimodal_tools/playwright_with_img_understanding.yaml +0 -98
  49. massgen/configs/tools/custom_tools/multimodal_tools/understand_video_example.yaml +0 -54
  50. massgen/configs/tools/memory/README.md +0 -199
  51. massgen/configs/tools/memory/gpt5mini_gemini_context_window_management.yaml +0 -131
  52. massgen/configs/tools/memory/gpt5mini_gemini_no_persistent_memory.yaml +0 -133
  53. massgen/configs/tools/memory/test_context_window_management.py +0 -286
  54. massgen/configs/tools/multimodal/gpt5mini_gpt5nano_documentation_evolution.yaml +0 -97
  55. {massgen-0.1.3.dist-info → massgen-0.1.4.dist-info}/WHEEL +0 -0
  56. {massgen-0.1.3.dist-info → massgen-0.1.4.dist-info}/entry_points.txt +0 -0
  57. {massgen-0.1.3.dist-info → massgen-0.1.4.dist-info}/licenses/LICENSE +0 -0
  58. {massgen-0.1.3.dist-info → massgen-0.1.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,407 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Tests for size and dimension limits in multimodal tools (image, video, audio).
5
+
6
+ This test suite generates fake media files to test:
7
+ - understand_image: 18MB file size + 768px × 2000px dimension limits
8
+ - understand_video: Frame dimension limits (768px × 2000px per frame)
9
+ - understand_audio: 25MB file size limit
10
+
11
+ All test files are created in temporary directories and cleaned up after tests.
12
+ """
13
+
14
+ import tempfile
15
+ from pathlib import Path
16
+
17
+ import pytest
18
+
19
+
20
+ class TestImageSizeLimits:
21
+ """Test suite for understand_image size and dimension limits."""
22
+
23
+ @pytest.fixture
24
+ def temp_dir(self):
25
+ """Create a temporary directory for test files."""
26
+ with tempfile.TemporaryDirectory() as tmpdir:
27
+ yield Path(tmpdir)
28
+
29
+ def _create_test_image(self, width: int, height: int, output_path: Path, format: str = "PNG"):
30
+ """
31
+ Create a test image with specified dimensions.
32
+
33
+ Args:
34
+ width: Image width in pixels
35
+ height: Image height in pixels
36
+ output_path: Path to save the image
37
+ format: Image format (PNG or JPEG)
38
+ """
39
+ import numpy as np
40
+ from PIL import Image
41
+
42
+ # Create a simple gradient image
43
+ img_array = np.zeros((height, width, 3), dtype=np.uint8)
44
+ for i in range(height):
45
+ img_array[i, :, 0] = int((i / height) * 255) # Red gradient
46
+ for j in range(width):
47
+ img_array[:, j, 1] = int((j / width) * 255) # Green gradient
48
+
49
+ img = Image.fromarray(img_array, "RGB")
50
+ img.save(output_path, format=format)
51
+
52
+ def _create_large_image(self, output_path: Path, target_size_mb: float = 20):
53
+ """
54
+ Create a large image file exceeding size limits.
55
+
56
+ Args:
57
+ output_path: Path to save the image
58
+ target_size_mb: Target size in megabytes
59
+ """
60
+ import numpy as np
61
+ from PIL import Image
62
+
63
+ # Calculate dimensions to achieve target file size
64
+ # PNG compression varies, so we'll create a large uncompressed image
65
+ # Rough estimate: width * height * 3 (RGB) should exceed target
66
+ pixels_needed = int((target_size_mb * 1024 * 1024) / 3)
67
+ side = int(pixels_needed**0.5)
68
+
69
+ # Create random noise image (doesn't compress well)
70
+ img_array = np.random.randint(0, 256, (side, side, 3), dtype=np.uint8)
71
+ img = Image.fromarray(img_array, "RGB")
72
+ img.save(output_path, format="PNG")
73
+
74
+ @pytest.mark.asyncio
75
+ async def test_image_within_limits(self, temp_dir):
76
+ """Test that images within size and dimension limits are processed without resizing."""
77
+ from massgen.tool._multimodal_tools.understand_image import understand_image
78
+
79
+ # Create a small image within limits (512x512)
80
+ img_path = temp_dir / "small_image.png"
81
+ self._create_test_image(512, 512, img_path, format="PNG")
82
+
83
+ # Use real OpenAI API
84
+ result = await understand_image(str(img_path), prompt="Describe this test image in one sentence.")
85
+
86
+ # Check that it succeeded
87
+ assert result.output_blocks is not None
88
+ assert len(result.output_blocks) > 0
89
+
90
+ # Parse result JSON
91
+ import json
92
+
93
+ result_data = json.loads(result.output_blocks[0].data)
94
+
95
+ print("\n" + "=" * 80)
96
+ print("TEST: Image Within Limits (512x512)")
97
+ print("=" * 80)
98
+ print(json.dumps(result_data, indent=2))
99
+ print("=" * 80 + "\n")
100
+
101
+ assert result_data["success"] is True
102
+
103
+ @pytest.mark.asyncio
104
+ async def test_image_dimension_limit(self, temp_dir):
105
+ """Test that images exceeding dimension limits are resized."""
106
+ from massgen.tool._multimodal_tools.understand_image import understand_image
107
+
108
+ # Create an image exceeding dimension limits (3000x4000)
109
+ img_path = temp_dir / "large_dimensions.jpg"
110
+ self._create_test_image(3000, 4000, img_path, format="JPEG")
111
+
112
+ # Check original size
113
+ from PIL import Image
114
+
115
+ with Image.open(img_path) as img:
116
+ original_width, original_height = img.size
117
+ assert original_width == 3000
118
+ assert original_height == 4000
119
+
120
+ # Use real OpenAI API - should resize internally and succeed
121
+ result = await understand_image(str(img_path), prompt="Describe this test image in one sentence.")
122
+
123
+ # Check that it succeeded (image was resized internally)
124
+ assert result.output_blocks is not None
125
+ import json
126
+
127
+ result_data = json.loads(result.output_blocks[0].data)
128
+
129
+ print("\n" + "=" * 80)
130
+ print("TEST: Image Exceeding Dimension Limits (3000x4000)")
131
+ print("=" * 80)
132
+ print(json.dumps(result_data, indent=2))
133
+ print("=" * 80 + "\n")
134
+
135
+ assert result_data["success"] is True
136
+
137
+ def test_image_dimension_calculation(self, temp_dir):
138
+ """Test dimension limit calculation logic directly."""
139
+ # Test that we correctly identify when resizing is needed
140
+ max_short_side = 768
141
+ max_long_side = 2000
142
+
143
+ test_cases = [
144
+ # (width, height, needs_resize)
145
+ (512, 512, False), # Within limits
146
+ (768, 2000, False), # Exactly at limits
147
+ (2000, 768, False), # Rotated, exactly at limits
148
+ (800, 1000, True), # Short side exceeds
149
+ (1000, 2500, True), # Long side exceeds
150
+ (3000, 4000, True), # Both exceed
151
+ ]
152
+
153
+ for width, height, expected_resize in test_cases:
154
+ short_side = min(width, height)
155
+ long_side = max(width, height)
156
+ needs_resize = short_side > max_short_side or long_side > max_long_side
157
+
158
+ assert needs_resize == expected_resize, f"Dimension check failed for {width}x{height}: expected resize={expected_resize}, got {needs_resize}"
159
+
160
+
161
+ class TestVideoFrameLimits:
162
+ """Test suite for understand_video frame dimension limits."""
163
+
164
+ @pytest.fixture
165
+ def temp_dir(self):
166
+ """Create a temporary directory for test files."""
167
+ with tempfile.TemporaryDirectory() as tmpdir:
168
+ yield Path(tmpdir)
169
+
170
+ def _create_test_video(self, width: int, height: int, output_path: Path, num_frames: int = 30):
171
+ """
172
+ Create a test video with specified dimensions.
173
+
174
+ Args:
175
+ width: Video width in pixels
176
+ height: Video height in pixels
177
+ output_path: Path to save the video
178
+ num_frames: Number of frames to generate
179
+ """
180
+ import cv2
181
+ import numpy as np
182
+
183
+ # Define the codec and create VideoWriter object
184
+ fourcc = cv2.VideoWriter_fourcc(*"mp4v")
185
+ fps = 10.0
186
+ video = cv2.VideoWriter(str(output_path), fourcc, fps, (width, height))
187
+
188
+ try:
189
+ for i in range(num_frames):
190
+ # Create a frame with gradient (changes over time)
191
+ frame = np.zeros((height, width, 3), dtype=np.uint8)
192
+ intensity = int((i / num_frames) * 255)
193
+ frame[:, :, 0] = intensity # Blue channel varies by frame
194
+ frame[: height // 2, :, 1] = 128 # Green in top half
195
+ frame[height // 2 :, :, 2] = 128 # Red in bottom half
196
+
197
+ video.write(frame)
198
+ finally:
199
+ video.release()
200
+
201
+ @pytest.mark.asyncio
202
+ async def test_video_with_large_frames(self, temp_dir):
203
+ """Test that video with large frame dimensions processes correctly (frames are resized)."""
204
+ try:
205
+ import cv2 # noqa: F401
206
+ except ImportError:
207
+ pytest.skip("opencv-python not installed")
208
+
209
+ from massgen.tool._multimodal_tools.understand_video import understand_video
210
+
211
+ # Create a video with large dimensions (3000x4000)
212
+ video_path = temp_dir / "large_video.mp4"
213
+ self._create_test_video(3000, 4000, video_path, num_frames=10)
214
+
215
+ # Use real OpenAI API - should resize frames internally and succeed
216
+ result = await understand_video(
217
+ str(video_path),
218
+ num_frames=3,
219
+ prompt="Describe what you see in this test video in one sentence.",
220
+ )
221
+
222
+ # Check that it succeeded
223
+ assert result.output_blocks is not None
224
+ import json
225
+
226
+ result_data = json.loads(result.output_blocks[0].data)
227
+
228
+ print("\n" + "=" * 80)
229
+ print("TEST: Video With Large Frames (3000x4000) - Frames Should Be Resized")
230
+ print("=" * 80)
231
+ print(json.dumps(result_data, indent=2))
232
+ print("=" * 80 + "\n")
233
+
234
+ assert result_data["success"] is True
235
+
236
+ @pytest.mark.asyncio
237
+ async def test_video_with_small_frames(self, temp_dir):
238
+ """Test that video with small frame dimensions processes without resizing."""
239
+ try:
240
+ import cv2 # noqa: F401
241
+ except ImportError:
242
+ pytest.skip("opencv-python not installed")
243
+
244
+ from massgen.tool._multimodal_tools.understand_video import understand_video
245
+
246
+ # Create a video with small dimensions (640x480)
247
+ video_path = temp_dir / "small_video.mp4"
248
+ self._create_test_video(640, 480, video_path, num_frames=10)
249
+
250
+ # Use real OpenAI API
251
+ result = await understand_video(
252
+ str(video_path),
253
+ num_frames=3,
254
+ prompt="Describe what you see in this test video in one sentence.",
255
+ )
256
+
257
+ # Check that it succeeded
258
+ assert result.output_blocks is not None
259
+ import json
260
+
261
+ result_data = json.loads(result.output_blocks[0].data)
262
+
263
+ print("\n" + "=" * 80)
264
+ print("TEST: Video With Small Frames (640x480) - No Resize Needed")
265
+ print("=" * 80)
266
+ print(json.dumps(result_data, indent=2))
267
+ print("=" * 80 + "\n")
268
+
269
+ assert result_data["success"] is True
270
+
271
+
272
+ class TestAudioSizeLimits:
273
+ """Test suite for understand_audio file size limits."""
274
+
275
+ @pytest.fixture
276
+ def temp_dir(self):
277
+ """Create a temporary directory for test files."""
278
+ with tempfile.TemporaryDirectory() as tmpdir:
279
+ yield Path(tmpdir)
280
+
281
+ def _create_test_audio(self, output_path: Path, duration_seconds: float = 1.0, sample_rate: int = 44100):
282
+ """
283
+ Create a test audio file (WAV format).
284
+
285
+ Args:
286
+ output_path: Path to save the audio file
287
+ duration_seconds: Duration in seconds
288
+ sample_rate: Sample rate in Hz
289
+ """
290
+ import wave
291
+
292
+ import numpy as np
293
+
294
+ # Generate a simple sine wave
295
+ frequency = 440.0 # A4 note
296
+ num_samples = int(sample_rate * duration_seconds)
297
+ t = np.linspace(0, duration_seconds, num_samples, False)
298
+ audio_data = np.sin(2 * np.pi * frequency * t)
299
+
300
+ # Convert to 16-bit PCM
301
+ audio_data = (audio_data * 32767).astype(np.int16)
302
+
303
+ # Write WAV file
304
+ with wave.open(str(output_path), "w") as wav_file:
305
+ wav_file.setnchannels(1) # Mono
306
+ wav_file.setsampwidth(2) # 16-bit
307
+ wav_file.setframerate(sample_rate)
308
+ wav_file.writeframes(audio_data.tobytes())
309
+
310
+ def _create_large_audio(self, output_path: Path, target_size_mb: float = 30):
311
+ """
312
+ Create a large audio file exceeding size limits.
313
+
314
+ Args:
315
+ output_path: Path to save the audio file
316
+ target_size_mb: Target size in megabytes
317
+ """
318
+ # Calculate duration needed to achieve target size
319
+ # WAV: sample_rate * duration * 2 bytes (16-bit) * channels
320
+ sample_rate = 44100
321
+ bytes_per_second = sample_rate * 2 # 16-bit mono
322
+ duration_seconds = (target_size_mb * 1024 * 1024) / bytes_per_second
323
+
324
+ self._create_test_audio(output_path, duration_seconds=duration_seconds, sample_rate=sample_rate)
325
+
326
+ @pytest.mark.asyncio
327
+ async def test_audio_within_size_limit(self, temp_dir):
328
+ """Test that audio files within size limit are accepted."""
329
+ from massgen.tool._multimodal_tools.understand_audio import understand_audio
330
+
331
+ # Create a small audio file (~1 second, ~88KB)
332
+ audio_path = temp_dir / "small_audio.wav"
333
+ self._create_test_audio(audio_path, duration_seconds=1.0)
334
+
335
+ file_size = audio_path.stat().st_size
336
+ assert file_size < 25 * 1024 * 1024, "Test audio should be under 25MB"
337
+
338
+ # Use real OpenAI API
339
+ result = await understand_audio([str(audio_path)])
340
+
341
+ # Check that it succeeded
342
+ assert result.output_blocks is not None
343
+ import json
344
+
345
+ result_data = json.loads(result.output_blocks[0].data)
346
+
347
+ print("\n" + "=" * 80)
348
+ print(f"TEST: Audio Within Size Limit (~{file_size/1024/1024:.2f}MB)")
349
+ print("=" * 80)
350
+ print(json.dumps(result_data, indent=2))
351
+ print("=" * 80 + "\n")
352
+
353
+ assert result_data["success"] is True
354
+
355
+ @pytest.mark.asyncio
356
+ async def test_audio_exceeds_size_limit(self, temp_dir):
357
+ """Test that audio files exceeding 25MB limit are rejected."""
358
+ from massgen.tool._multimodal_tools.understand_audio import understand_audio
359
+
360
+ # Create a large audio file (~30MB)
361
+ audio_path = temp_dir / "large_audio.wav"
362
+ self._create_large_audio(audio_path, target_size_mb=30)
363
+
364
+ file_size = audio_path.stat().st_size
365
+ assert file_size > 25 * 1024 * 1024, f"Test audio should exceed 25MB, got {file_size / 1024 / 1024:.1f}MB"
366
+
367
+ # This should fail validation before calling OpenAI
368
+ result = await understand_audio([str(audio_path)])
369
+
370
+ # Check that it failed due to size limit
371
+ assert result.output_blocks is not None
372
+ import json
373
+
374
+ result_data = json.loads(result.output_blocks[0].data)
375
+
376
+ print("\n" + "=" * 80)
377
+ print(f"TEST: Audio Exceeds Size Limit ({file_size/1024/1024:.1f}MB > 25MB)")
378
+ print("=" * 80)
379
+ print(json.dumps(result_data, indent=2))
380
+ print("=" * 80 + "\n")
381
+
382
+ assert result_data["success"] is False
383
+ assert "too large" in result_data["error"].lower()
384
+ assert "25MB" in result_data["error"]
385
+
386
+ def test_audio_size_check(self, temp_dir):
387
+ """Test audio file size checking logic."""
388
+ # Create audio files of different sizes
389
+ test_cases = [
390
+ (1.0, True), # 1 second (~88KB) - should pass
391
+ (10.0, True), # 10 seconds (~880KB) - should pass
392
+ ]
393
+
394
+ for duration, should_pass in test_cases:
395
+ audio_path = temp_dir / f"audio_{duration}s.wav"
396
+ self._create_test_audio(audio_path, duration_seconds=duration)
397
+
398
+ file_size = audio_path.stat().st_size
399
+ max_size = 25 * 1024 * 1024
400
+
401
+ passes = file_size <= max_size
402
+
403
+ assert passes == should_pass, f"Size check failed for {duration}s audio ({file_size / 1024 / 1024:.1f}MB): " f"expected pass={should_pass}, got {passes}"
404
+
405
+
406
+ if __name__ == "__main__":
407
+ pytest.main([__file__, "-v"])
massgen/tool/_manager.py CHANGED
@@ -312,9 +312,14 @@ class ToolManager:
312
312
  return
313
313
 
314
314
  tool_entry = self.registered_tools[tool_name]
315
+
316
+ # Merge parameters: model input first, then preset params override
317
+ # This ensures preset_params (like agent_cwd) always take precedence
318
+ # and won't be overridden by null values from model
319
+ model_input = tool_request.get("input", {}) or {}
315
320
  exec_kwargs = {
316
- **tool_entry.preset_params,
317
- **(tool_request.get("input", {}) or {}),
321
+ **model_input,
322
+ **tool_entry.preset_params, # preset_params override model input
318
323
  }
319
324
 
320
325
  # Prepare post-processor if exists