massgen 0.1.0a3__py3-none-any.whl → 0.1.2__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 +1 -1
- massgen/agent_config.py +17 -0
- massgen/api_params_handler/_api_params_handler_base.py +1 -0
- massgen/api_params_handler/_chat_completions_api_params_handler.py +15 -2
- massgen/api_params_handler/_claude_api_params_handler.py +8 -1
- massgen/api_params_handler/_gemini_api_params_handler.py +73 -0
- massgen/api_params_handler/_response_api_params_handler.py +8 -1
- massgen/backend/base.py +83 -0
- massgen/backend/{base_with_mcp.py → base_with_custom_tool_and_mcp.py} +286 -15
- massgen/backend/capabilities.py +6 -6
- massgen/backend/chat_completions.py +200 -103
- massgen/backend/claude.py +115 -18
- massgen/backend/claude_code.py +378 -14
- massgen/backend/docs/CLAUDE_API_RESEARCH.md +3 -3
- massgen/backend/gemini.py +1333 -1629
- massgen/backend/gemini_mcp_manager.py +545 -0
- massgen/backend/gemini_trackers.py +344 -0
- massgen/backend/gemini_utils.py +43 -0
- massgen/backend/grok.py +39 -6
- massgen/backend/response.py +147 -81
- massgen/cli.py +605 -110
- massgen/config_builder.py +376 -27
- massgen/configs/README.md +123 -80
- massgen/configs/basic/multi/three_agents_default.yaml +3 -3
- massgen/configs/basic/single/single_agent.yaml +1 -1
- massgen/configs/providers/openai/gpt5_nano.yaml +3 -3
- massgen/configs/tools/custom_tools/claude_code_custom_tool_example.yaml +32 -0
- massgen/configs/tools/custom_tools/claude_code_custom_tool_example_no_path.yaml +28 -0
- massgen/configs/tools/custom_tools/claude_code_custom_tool_with_mcp_example.yaml +40 -0
- massgen/configs/tools/custom_tools/claude_code_custom_tool_with_wrong_mcp_example.yaml +38 -0
- massgen/configs/tools/custom_tools/claude_code_wrong_custom_tool_with_mcp_example.yaml +38 -0
- massgen/configs/tools/custom_tools/claude_custom_tool_example.yaml +24 -0
- massgen/configs/tools/custom_tools/claude_custom_tool_example_no_path.yaml +22 -0
- massgen/configs/tools/custom_tools/claude_custom_tool_with_mcp_example.yaml +35 -0
- massgen/configs/tools/custom_tools/claude_custom_tool_with_wrong_mcp_example.yaml +33 -0
- massgen/configs/tools/custom_tools/claude_wrong_custom_tool_with_mcp_example.yaml +33 -0
- massgen/configs/tools/custom_tools/gemini_custom_tool_example.yaml +24 -0
- massgen/configs/tools/custom_tools/gemini_custom_tool_example_no_path.yaml +22 -0
- massgen/configs/tools/custom_tools/gemini_custom_tool_with_mcp_example.yaml +35 -0
- massgen/configs/tools/custom_tools/gemini_custom_tool_with_wrong_mcp_example.yaml +33 -0
- massgen/configs/tools/custom_tools/gemini_wrong_custom_tool_with_mcp_example.yaml +33 -0
- massgen/configs/tools/custom_tools/github_issue_market_analysis.yaml +94 -0
- massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_example.yaml +24 -0
- massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_example_no_path.yaml +22 -0
- massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_with_mcp_example.yaml +35 -0
- massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_with_wrong_mcp_example.yaml +33 -0
- massgen/configs/tools/custom_tools/gpt5_nano_wrong_custom_tool_with_mcp_example.yaml +33 -0
- massgen/configs/tools/custom_tools/gpt_oss_custom_tool_example.yaml +25 -0
- massgen/configs/tools/custom_tools/gpt_oss_custom_tool_example_no_path.yaml +23 -0
- massgen/configs/tools/custom_tools/gpt_oss_custom_tool_with_mcp_example.yaml +34 -0
- massgen/configs/tools/custom_tools/gpt_oss_custom_tool_with_wrong_mcp_example.yaml +34 -0
- massgen/configs/tools/custom_tools/gpt_oss_wrong_custom_tool_with_mcp_example.yaml +34 -0
- massgen/configs/tools/custom_tools/grok3_mini_custom_tool_example.yaml +24 -0
- massgen/configs/tools/custom_tools/grok3_mini_custom_tool_example_no_path.yaml +22 -0
- massgen/configs/tools/custom_tools/grok3_mini_custom_tool_with_mcp_example.yaml +35 -0
- massgen/configs/tools/custom_tools/grok3_mini_custom_tool_with_wrong_mcp_example.yaml +33 -0
- massgen/configs/tools/custom_tools/grok3_mini_wrong_custom_tool_with_mcp_example.yaml +33 -0
- massgen/configs/tools/custom_tools/qwen_api_custom_tool_example.yaml +25 -0
- massgen/configs/tools/custom_tools/qwen_api_custom_tool_example_no_path.yaml +23 -0
- massgen/configs/tools/custom_tools/qwen_api_custom_tool_with_mcp_example.yaml +36 -0
- massgen/configs/tools/custom_tools/qwen_api_custom_tool_with_wrong_mcp_example.yaml +34 -0
- massgen/configs/tools/custom_tools/qwen_api_wrong_custom_tool_with_mcp_example.yaml +34 -0
- massgen/configs/tools/custom_tools/qwen_local_custom_tool_example.yaml +24 -0
- massgen/configs/tools/custom_tools/qwen_local_custom_tool_example_no_path.yaml +22 -0
- massgen/configs/tools/custom_tools/qwen_local_custom_tool_with_mcp_example.yaml +35 -0
- massgen/configs/tools/custom_tools/qwen_local_custom_tool_with_wrong_mcp_example.yaml +33 -0
- massgen/configs/tools/custom_tools/qwen_local_wrong_custom_tool_with_mcp_example.yaml +33 -0
- massgen/configs/tools/filesystem/claude_code_context_sharing.yaml +1 -1
- 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/configs/voting/gemini_gpt_voting_sensitivity.yaml +67 -0
- massgen/formatter/_chat_completions_formatter.py +104 -0
- massgen/formatter/_claude_formatter.py +120 -0
- massgen/formatter/_gemini_formatter.py +448 -0
- massgen/formatter/_response_formatter.py +88 -0
- massgen/frontend/coordination_ui.py +4 -2
- massgen/logger_config.py +35 -3
- massgen/message_templates.py +56 -6
- massgen/orchestrator.py +512 -16
- massgen/stream_chunk/base.py +3 -0
- massgen/tests/custom_tools_example.py +392 -0
- massgen/tests/mcp_test_server.py +17 -7
- massgen/tests/test_config_builder.py +423 -0
- massgen/tests/test_custom_tools.py +401 -0
- massgen/tests/test_intelligent_planning_mode.py +643 -0
- massgen/tests/test_tools.py +127 -0
- massgen/token_manager/token_manager.py +13 -4
- massgen/tool/README.md +935 -0
- massgen/tool/__init__.py +39 -0
- massgen/tool/_async_helpers.py +70 -0
- massgen/tool/_basic/__init__.py +8 -0
- massgen/tool/_basic/_two_num_tool.py +24 -0
- massgen/tool/_code_executors/__init__.py +10 -0
- massgen/tool/_code_executors/_python_executor.py +74 -0
- massgen/tool/_code_executors/_shell_executor.py +61 -0
- massgen/tool/_exceptions.py +39 -0
- massgen/tool/_file_handlers/__init__.py +10 -0
- massgen/tool/_file_handlers/_file_operations.py +218 -0
- massgen/tool/_manager.py +634 -0
- massgen/tool/_registered_tool.py +88 -0
- massgen/tool/_result.py +66 -0
- massgen/tool/_self_evolution/_github_issue_analyzer.py +369 -0
- massgen/tool/docs/builtin_tools.md +681 -0
- massgen/tool/docs/exceptions.md +794 -0
- massgen/tool/docs/execution_results.md +691 -0
- massgen/tool/docs/manager.md +887 -0
- massgen/tool/docs/workflow_toolkits.md +529 -0
- massgen/tool/workflow_toolkits/__init__.py +57 -0
- massgen/tool/workflow_toolkits/base.py +55 -0
- massgen/tool/workflow_toolkits/new_answer.py +126 -0
- massgen/tool/workflow_toolkits/vote.py +167 -0
- {massgen-0.1.0a3.dist-info → massgen-0.1.2.dist-info}/METADATA +87 -129
- {massgen-0.1.0a3.dist-info → massgen-0.1.2.dist-info}/RECORD +120 -44
- {massgen-0.1.0a3.dist-info → massgen-0.1.2.dist-info}/WHEEL +0 -0
- {massgen-0.1.0a3.dist-info → massgen-0.1.2.dist-info}/entry_points.txt +0 -0
- {massgen-0.1.0a3.dist-info → massgen-0.1.2.dist-info}/licenses/LICENSE +0 -0
- {massgen-0.1.0a3.dist-info → massgen-0.1.2.dist-info}/top_level.txt +0 -0
massgen/backend/claude_code.py
CHANGED
|
@@ -73,7 +73,9 @@ from ..logger_config import (
|
|
|
73
73
|
log_backend_activity,
|
|
74
74
|
log_backend_agent_message,
|
|
75
75
|
log_stream_chunk,
|
|
76
|
+
logger,
|
|
76
77
|
)
|
|
78
|
+
from ..tool import ToolManager
|
|
77
79
|
from .base import FilesystemSupport, LLMBackend, StreamChunk
|
|
78
80
|
|
|
79
81
|
|
|
@@ -148,6 +150,38 @@ class ClaudeCodeBackend(LLMBackend):
|
|
|
148
150
|
|
|
149
151
|
self._pending_system_prompt: Optional[str] = None # Windows-only workaround
|
|
150
152
|
|
|
153
|
+
# Custom tools support - initialize ToolManager if custom_tools are provided
|
|
154
|
+
self._custom_tool_manager: Optional[ToolManager] = None
|
|
155
|
+
custom_tools = kwargs.get("custom_tools", [])
|
|
156
|
+
if custom_tools:
|
|
157
|
+
self._custom_tool_manager = ToolManager()
|
|
158
|
+
self._register_custom_tools(custom_tools)
|
|
159
|
+
|
|
160
|
+
# Create SDK MCP Server from custom tools and inject into mcp_servers
|
|
161
|
+
sdk_mcp_server = self._create_sdk_mcp_server_from_custom_tools()
|
|
162
|
+
if sdk_mcp_server:
|
|
163
|
+
# Ensure mcp_servers exists in config
|
|
164
|
+
if "mcp_servers" not in self.config:
|
|
165
|
+
self.config["mcp_servers"] = {}
|
|
166
|
+
|
|
167
|
+
# Add SDK MCP server (convert to list format if dict format is used)
|
|
168
|
+
if isinstance(self.config["mcp_servers"], dict):
|
|
169
|
+
# Already in dict format
|
|
170
|
+
self.config["mcp_servers"]["massgen_custom_tools"] = sdk_mcp_server
|
|
171
|
+
elif isinstance(self.config["mcp_servers"], list):
|
|
172
|
+
# List format - add as special entry with SDK server marker
|
|
173
|
+
self.config["mcp_servers"].append(
|
|
174
|
+
{
|
|
175
|
+
"name": "massgen_custom_tools",
|
|
176
|
+
"__sdk_server__": sdk_mcp_server,
|
|
177
|
+
},
|
|
178
|
+
)
|
|
179
|
+
else:
|
|
180
|
+
# Initialize as dict with SDK server
|
|
181
|
+
self.config["mcp_servers"] = {"massgen_custom_tools": sdk_mcp_server}
|
|
182
|
+
|
|
183
|
+
logger.info(f"Registered SDK MCP server with {len(self._custom_tool_manager.registered_tools)} custom tools")
|
|
184
|
+
|
|
151
185
|
def _setup_windows_subprocess_cleanup_suppression(self):
|
|
152
186
|
"""Comprehensive Windows subprocess cleanup warning suppression."""
|
|
153
187
|
# All warning filters
|
|
@@ -405,6 +439,327 @@ class ClaudeCodeBackend(LLMBackend):
|
|
|
405
439
|
# # Will integrate with PermissionManager
|
|
406
440
|
# pass
|
|
407
441
|
|
|
442
|
+
def _register_custom_tools(self, custom_tools: List[Dict[str, Any]]) -> None:
|
|
443
|
+
"""Register custom tools with the tool manager.
|
|
444
|
+
|
|
445
|
+
Supports flexible configuration:
|
|
446
|
+
- function: str | List[str]
|
|
447
|
+
- description: str (shared) | List[str] (1-to-1 mapping)
|
|
448
|
+
- preset_args: dict (shared) | List[dict] (1-to-1 mapping)
|
|
449
|
+
|
|
450
|
+
Examples:
|
|
451
|
+
# Single function
|
|
452
|
+
function: "my_func"
|
|
453
|
+
description: "My description"
|
|
454
|
+
|
|
455
|
+
# Multiple functions with shared description
|
|
456
|
+
function: ["func1", "func2"]
|
|
457
|
+
description: "Shared description"
|
|
458
|
+
|
|
459
|
+
# Multiple functions with individual descriptions
|
|
460
|
+
function: ["func1", "func2"]
|
|
461
|
+
description: ["Description 1", "Description 2"]
|
|
462
|
+
|
|
463
|
+
# Multiple functions with mixed (shared desc, individual args)
|
|
464
|
+
function: ["func1", "func2"]
|
|
465
|
+
description: "Shared description"
|
|
466
|
+
preset_args: [{"arg1": "val1"}, {"arg1": "val2"}]
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
custom_tools: List of custom tool configurations
|
|
470
|
+
"""
|
|
471
|
+
if not self._custom_tool_manager:
|
|
472
|
+
logger.warning("Custom tool manager not initialized, cannot register tools")
|
|
473
|
+
return
|
|
474
|
+
|
|
475
|
+
# Collect unique categories and create them if needed
|
|
476
|
+
categories = set()
|
|
477
|
+
for tool_config in custom_tools:
|
|
478
|
+
if isinstance(tool_config, dict):
|
|
479
|
+
category = tool_config.get("category", "default")
|
|
480
|
+
if category != "default":
|
|
481
|
+
categories.add(category)
|
|
482
|
+
|
|
483
|
+
# Create categories that don't exist
|
|
484
|
+
for category in categories:
|
|
485
|
+
if category not in self._custom_tool_manager.tool_categories:
|
|
486
|
+
self._custom_tool_manager.setup_category(
|
|
487
|
+
category_name=category,
|
|
488
|
+
description=f"Custom {category} tools",
|
|
489
|
+
enabled=True,
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
# Register each custom tool
|
|
493
|
+
for tool_config in custom_tools:
|
|
494
|
+
try:
|
|
495
|
+
if isinstance(tool_config, dict):
|
|
496
|
+
# Extract base configuration
|
|
497
|
+
path = tool_config.get("path")
|
|
498
|
+
category = tool_config.get("category", "default")
|
|
499
|
+
|
|
500
|
+
# Normalize function field to list
|
|
501
|
+
func_field = tool_config.get("function")
|
|
502
|
+
if isinstance(func_field, str):
|
|
503
|
+
functions = [func_field]
|
|
504
|
+
elif isinstance(func_field, list):
|
|
505
|
+
functions = func_field
|
|
506
|
+
else:
|
|
507
|
+
logger.error(
|
|
508
|
+
f"Invalid function field type: {type(func_field)}. " f"Must be str or List[str].",
|
|
509
|
+
)
|
|
510
|
+
continue
|
|
511
|
+
|
|
512
|
+
if not functions:
|
|
513
|
+
logger.error("Empty function list in tool config")
|
|
514
|
+
continue
|
|
515
|
+
|
|
516
|
+
num_functions = len(functions)
|
|
517
|
+
|
|
518
|
+
# Process name field (can be str or List[str])
|
|
519
|
+
name_field = tool_config.get("name")
|
|
520
|
+
names = self._process_field_for_functions(
|
|
521
|
+
name_field,
|
|
522
|
+
num_functions,
|
|
523
|
+
"name",
|
|
524
|
+
)
|
|
525
|
+
if names is None:
|
|
526
|
+
continue # Validation error, skip this tool
|
|
527
|
+
|
|
528
|
+
# Process description field (can be str or List[str])
|
|
529
|
+
desc_field = tool_config.get("description")
|
|
530
|
+
descriptions = self._process_field_for_functions(
|
|
531
|
+
desc_field,
|
|
532
|
+
num_functions,
|
|
533
|
+
"description",
|
|
534
|
+
)
|
|
535
|
+
if descriptions is None:
|
|
536
|
+
continue # Validation error, skip this tool
|
|
537
|
+
|
|
538
|
+
# Process preset_args field (can be dict or List[dict])
|
|
539
|
+
preset_field = tool_config.get("preset_args")
|
|
540
|
+
preset_args_list = self._process_field_for_functions(
|
|
541
|
+
preset_field,
|
|
542
|
+
num_functions,
|
|
543
|
+
"preset_args",
|
|
544
|
+
)
|
|
545
|
+
if preset_args_list is None:
|
|
546
|
+
continue # Validation error, skip this tool
|
|
547
|
+
|
|
548
|
+
# Register each function with its corresponding values
|
|
549
|
+
for i, func in enumerate(functions):
|
|
550
|
+
# Load the function first if custom name is needed
|
|
551
|
+
if names[i] and names[i] != func:
|
|
552
|
+
# Need to load function and apply custom name
|
|
553
|
+
if path:
|
|
554
|
+
loaded_func = self._custom_tool_manager._load_function_from_path(path, func)
|
|
555
|
+
else:
|
|
556
|
+
loaded_func = self._custom_tool_manager._load_builtin_function(func)
|
|
557
|
+
|
|
558
|
+
if loaded_func is None:
|
|
559
|
+
logger.error(f"Could not load function '{func}' from path: {path}")
|
|
560
|
+
continue
|
|
561
|
+
|
|
562
|
+
# Apply custom name by modifying __name__ attribute
|
|
563
|
+
loaded_func.__name__ = names[i]
|
|
564
|
+
|
|
565
|
+
# Register with loaded function (no path needed)
|
|
566
|
+
self._custom_tool_manager.add_tool_function(
|
|
567
|
+
path=None,
|
|
568
|
+
func=loaded_func,
|
|
569
|
+
category=category,
|
|
570
|
+
preset_args=preset_args_list[i],
|
|
571
|
+
description=descriptions[i],
|
|
572
|
+
)
|
|
573
|
+
else:
|
|
574
|
+
# No custom name or same as function name, use normal registration
|
|
575
|
+
self._custom_tool_manager.add_tool_function(
|
|
576
|
+
path=path,
|
|
577
|
+
func=func,
|
|
578
|
+
category=category,
|
|
579
|
+
preset_args=preset_args_list[i],
|
|
580
|
+
description=descriptions[i],
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
# Use custom name for logging if provided
|
|
584
|
+
registered_name = names[i] if names[i] else func
|
|
585
|
+
logger.info(
|
|
586
|
+
f"Registered custom tool: {registered_name} from {path} " f"(category: {category}, " f"desc: '{descriptions[i][:50] if descriptions[i] else 'None'}...')",
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
except Exception as e:
|
|
590
|
+
func_name = tool_config.get("function", "unknown")
|
|
591
|
+
logger.error(
|
|
592
|
+
f"Failed to register custom tool {func_name}: {e}",
|
|
593
|
+
exc_info=True,
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
def _process_field_for_functions(
|
|
597
|
+
self,
|
|
598
|
+
field_value: Any,
|
|
599
|
+
num_functions: int,
|
|
600
|
+
field_name: str,
|
|
601
|
+
) -> Optional[List[Any]]:
|
|
602
|
+
"""Process a config field that can be a single value or list.
|
|
603
|
+
|
|
604
|
+
Conversion rules:
|
|
605
|
+
- None → [None, None, ...] (repeated num_functions times)
|
|
606
|
+
- Single value (not list) → [value, value, ...] (shared)
|
|
607
|
+
- List with matching length → use as-is (1-to-1 mapping)
|
|
608
|
+
- List with wrong length → ERROR (return None)
|
|
609
|
+
|
|
610
|
+
Args:
|
|
611
|
+
field_value: The field value from config
|
|
612
|
+
num_functions: Number of functions being registered
|
|
613
|
+
field_name: Name of the field (for error messages)
|
|
614
|
+
|
|
615
|
+
Returns:
|
|
616
|
+
List of values (one per function), or None if validation fails
|
|
617
|
+
|
|
618
|
+
Examples:
|
|
619
|
+
_process_field_for_functions(None, 3, "desc")
|
|
620
|
+
→ [None, None, None]
|
|
621
|
+
|
|
622
|
+
_process_field_for_functions("shared", 3, "desc")
|
|
623
|
+
→ ["shared", "shared", "shared"]
|
|
624
|
+
|
|
625
|
+
_process_field_for_functions(["a", "b", "c"], 3, "desc")
|
|
626
|
+
→ ["a", "b", "c"]
|
|
627
|
+
|
|
628
|
+
_process_field_for_functions(["a", "b"], 3, "desc")
|
|
629
|
+
→ None (error logged)
|
|
630
|
+
"""
|
|
631
|
+
# Case 1: None or missing field → use None for all functions
|
|
632
|
+
if field_value is None:
|
|
633
|
+
return [None] * num_functions
|
|
634
|
+
|
|
635
|
+
# Case 2: Single value (not a list) → share across all functions
|
|
636
|
+
if not isinstance(field_value, list):
|
|
637
|
+
return [field_value] * num_functions
|
|
638
|
+
|
|
639
|
+
# Case 3: List value → must match function count exactly
|
|
640
|
+
if len(field_value) == num_functions:
|
|
641
|
+
return field_value
|
|
642
|
+
else:
|
|
643
|
+
# Length mismatch → validation error
|
|
644
|
+
logger.error(
|
|
645
|
+
f"Configuration error: {field_name} is a list with "
|
|
646
|
+
f"{len(field_value)} items, but there are {num_functions} functions. "
|
|
647
|
+
f"Either use a single value (shared) or a list with exactly "
|
|
648
|
+
f"{num_functions} items (1-to-1 mapping).",
|
|
649
|
+
)
|
|
650
|
+
return None
|
|
651
|
+
|
|
652
|
+
async def _execute_massgen_custom_tool(self, tool_name: str, args: dict) -> dict:
|
|
653
|
+
"""Execute a MassGen custom tool and convert result to MCP format.
|
|
654
|
+
|
|
655
|
+
Args:
|
|
656
|
+
tool_name: Name of the custom tool to execute
|
|
657
|
+
args: Arguments for the tool
|
|
658
|
+
|
|
659
|
+
Returns:
|
|
660
|
+
MCP-formatted response with content blocks
|
|
661
|
+
"""
|
|
662
|
+
if not self._custom_tool_manager:
|
|
663
|
+
return {
|
|
664
|
+
"content": [
|
|
665
|
+
{"type": "text", "text": "Error: Custom tool manager not initialized"},
|
|
666
|
+
],
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
tool_request = {
|
|
670
|
+
"name": tool_name,
|
|
671
|
+
"input": args,
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
result_text = ""
|
|
675
|
+
try:
|
|
676
|
+
async for result in self._custom_tool_manager.execute_tool(tool_request):
|
|
677
|
+
# Accumulate ExecutionResult blocks
|
|
678
|
+
if hasattr(result, "output_blocks"):
|
|
679
|
+
for block in result.output_blocks:
|
|
680
|
+
if hasattr(block, "data"):
|
|
681
|
+
result_text += str(block.data)
|
|
682
|
+
elif hasattr(block, "content"):
|
|
683
|
+
result_text += str(block.content)
|
|
684
|
+
elif hasattr(result, "content"):
|
|
685
|
+
result_text += str(result.content)
|
|
686
|
+
else:
|
|
687
|
+
result_text += str(result)
|
|
688
|
+
except Exception as e:
|
|
689
|
+
logger.error(f"Error executing custom tool {tool_name}: {e}")
|
|
690
|
+
result_text = f"Error: {str(e)}"
|
|
691
|
+
|
|
692
|
+
# Return MCP format response
|
|
693
|
+
return {
|
|
694
|
+
"content": [
|
|
695
|
+
{"type": "text", "text": result_text or "Tool executed successfully"},
|
|
696
|
+
],
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
def _create_sdk_mcp_server_from_custom_tools(self):
|
|
700
|
+
"""Convert MassGen custom tools to SDK MCP Server.
|
|
701
|
+
|
|
702
|
+
Returns:
|
|
703
|
+
SDK MCP Server instance or None if no tools or SDK unavailable
|
|
704
|
+
"""
|
|
705
|
+
if not self._custom_tool_manager:
|
|
706
|
+
return None
|
|
707
|
+
|
|
708
|
+
try:
|
|
709
|
+
from claude_agent_sdk import create_sdk_mcp_server, tool
|
|
710
|
+
except ImportError:
|
|
711
|
+
logger.warning("claude-agent-sdk not available, custom tools will not be registered")
|
|
712
|
+
return None
|
|
713
|
+
|
|
714
|
+
# Get all registered custom tools
|
|
715
|
+
tool_schemas = self._custom_tool_manager.fetch_tool_schemas()
|
|
716
|
+
if not tool_schemas:
|
|
717
|
+
logger.info("No custom tools to register")
|
|
718
|
+
return None
|
|
719
|
+
|
|
720
|
+
# Convert each tool to MCP tool format
|
|
721
|
+
mcp_tools = []
|
|
722
|
+
for tool_schema in tool_schemas:
|
|
723
|
+
try:
|
|
724
|
+
tool_name = tool_schema["function"]["name"]
|
|
725
|
+
tool_desc = tool_schema["function"].get("description", "")
|
|
726
|
+
tool_params = tool_schema["function"]["parameters"]
|
|
727
|
+
|
|
728
|
+
# Create async wrapper for MassGen tool
|
|
729
|
+
# Use default argument to capture tool_name in closure
|
|
730
|
+
async def tool_wrapper(args, tool_name=tool_name):
|
|
731
|
+
return await self._execute_massgen_custom_tool(tool_name, args)
|
|
732
|
+
|
|
733
|
+
# Register using SDK tool decorator
|
|
734
|
+
mcp_tool = tool(
|
|
735
|
+
name=tool_name,
|
|
736
|
+
description=tool_desc,
|
|
737
|
+
input_schema=tool_params,
|
|
738
|
+
)(tool_wrapper)
|
|
739
|
+
|
|
740
|
+
mcp_tools.append(mcp_tool)
|
|
741
|
+
logger.info(f"Converted custom tool to MCP: {tool_name}")
|
|
742
|
+
|
|
743
|
+
except Exception as e:
|
|
744
|
+
logger.error(f"Failed to convert tool {tool_schema.get('function', {}).get('name', 'unknown')} to MCP: {e}")
|
|
745
|
+
|
|
746
|
+
if not mcp_tools:
|
|
747
|
+
logger.warning("No custom tools successfully converted to MCP")
|
|
748
|
+
return None
|
|
749
|
+
|
|
750
|
+
# Create SDK MCP server
|
|
751
|
+
try:
|
|
752
|
+
sdk_mcp_server = create_sdk_mcp_server(
|
|
753
|
+
name="massgen_custom_tools",
|
|
754
|
+
version="1.0.0",
|
|
755
|
+
tools=mcp_tools,
|
|
756
|
+
)
|
|
757
|
+
logger.info(f"Created SDK MCP server with {len(mcp_tools)} custom tools")
|
|
758
|
+
return sdk_mcp_server
|
|
759
|
+
except Exception as e:
|
|
760
|
+
logger.error(f"Failed to create SDK MCP server: {e}")
|
|
761
|
+
return None
|
|
762
|
+
|
|
408
763
|
def _build_system_prompt_with_workflow_tools(self, tools: List[Dict[str, Any]], base_system: Optional[str] = None) -> str:
|
|
409
764
|
"""Build system prompt that includes workflow tools information.
|
|
410
765
|
|
|
@@ -703,6 +1058,7 @@ class ClaudeCodeBackend(LLMBackend):
|
|
|
703
1058
|
"api_key",
|
|
704
1059
|
"allowed_tools",
|
|
705
1060
|
"permission_mode",
|
|
1061
|
+
"custom_tools", # Handled separately via SDK MCP server conversion
|
|
706
1062
|
}
|
|
707
1063
|
|
|
708
1064
|
# Get cwd from filesystem manager (always available since we require it in __init__)
|
|
@@ -720,10 +1076,15 @@ class ClaudeCodeBackend(LLMBackend):
|
|
|
720
1076
|
mcp_servers = options_kwargs["mcp_servers"]
|
|
721
1077
|
if isinstance(mcp_servers, list):
|
|
722
1078
|
for server in mcp_servers:
|
|
723
|
-
if isinstance(server, dict)
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
1079
|
+
if isinstance(server, dict):
|
|
1080
|
+
if "__sdk_server__" in server:
|
|
1081
|
+
# SDK MCP Server object (created via create_sdk_mcp_server)
|
|
1082
|
+
server_name = server["name"]
|
|
1083
|
+
mcp_servers_dict[server_name] = server["__sdk_server__"]
|
|
1084
|
+
elif "name" in server:
|
|
1085
|
+
# Regular dictionary configuration
|
|
1086
|
+
server_config = {k: v for k, v in server.items() if k != "name"}
|
|
1087
|
+
mcp_servers_dict[server["name"]] = server_config
|
|
727
1088
|
elif isinstance(mcp_servers, dict):
|
|
728
1089
|
# Already in dict format
|
|
729
1090
|
mcp_servers_dict = mcp_servers
|
|
@@ -805,6 +1166,19 @@ class ClaudeCodeBackend(LLMBackend):
|
|
|
805
1166
|
)
|
|
806
1167
|
# Merge constructor config with stream kwargs (stream kwargs take priority)
|
|
807
1168
|
all_params = {**self.config, **kwargs}
|
|
1169
|
+
|
|
1170
|
+
# Extract system message from messages for append mode (always do this)
|
|
1171
|
+
# This must be done BEFORE checking if we have a client to ensure workflow_system_prompt is always defined
|
|
1172
|
+
system_msg = next((msg for msg in messages if msg.get("role") == "system"), None)
|
|
1173
|
+
if system_msg:
|
|
1174
|
+
system_content = system_msg.get("content", "") # noqa: E128
|
|
1175
|
+
else:
|
|
1176
|
+
system_content = ""
|
|
1177
|
+
|
|
1178
|
+
# Build system prompt with tools information
|
|
1179
|
+
# This must be done before any conditional paths to ensure it's always defined
|
|
1180
|
+
workflow_system_prompt = self._build_system_prompt_with_workflow_tools(tools or [], system_content)
|
|
1181
|
+
|
|
808
1182
|
# Check if we already have a client
|
|
809
1183
|
if self._client is not None:
|
|
810
1184
|
client = self._client
|
|
@@ -830,16 +1204,6 @@ class ClaudeCodeBackend(LLMBackend):
|
|
|
830
1204
|
disallowed_tools.append(tool)
|
|
831
1205
|
all_params["disallowed_tools"] = disallowed_tools
|
|
832
1206
|
|
|
833
|
-
# Extract system message from messages for append mode (always do this)
|
|
834
|
-
system_msg = next((msg for msg in messages if msg.get("role") == "system"), None)
|
|
835
|
-
if system_msg:
|
|
836
|
-
system_content = system_msg.get("content", "") # noqa: E128
|
|
837
|
-
else:
|
|
838
|
-
system_content = ""
|
|
839
|
-
|
|
840
|
-
# Build system prompt with tools information
|
|
841
|
-
workflow_system_prompt = self._build_system_prompt_with_workflow_tools(tools or [], system_content)
|
|
842
|
-
|
|
843
1207
|
# Windows-specific handling: detect complex prompts that cause subprocess hang
|
|
844
1208
|
if sys.platform == "win32" and len(workflow_system_prompt) > 200:
|
|
845
1209
|
# Windows with complex prompt: use post-connection delivery to avoid hang
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
```python
|
|
62
62
|
# Fine-grained tool streaming (beta) - REQUIRES BETA CLIENT
|
|
63
63
|
stream = client.beta.messages.create(
|
|
64
|
-
model="claude-3-5-sonnet-
|
|
64
|
+
model="claude-3-5-sonnet-20250114",
|
|
65
65
|
messages=[{"role": "user", "content": "Search and analyze..."}],
|
|
66
66
|
tools=[
|
|
67
67
|
{"type": "web_search_20250305"},
|
|
@@ -96,7 +96,7 @@ async_client = anthropic.AsyncAnthropic()
|
|
|
96
96
|
# IMPORTANT: For code execution, use the beta client
|
|
97
97
|
beta_client = anthropic.AsyncAnthropic()
|
|
98
98
|
response = await beta_client.beta.messages.create(
|
|
99
|
-
model="claude-3-5-sonnet-
|
|
99
|
+
model="claude-3-5-sonnet-20250114",
|
|
100
100
|
tools=[{"type": "code_execution_20250522"}],
|
|
101
101
|
headers={"anthropic-beta": "code-execution-2025-05-22"},
|
|
102
102
|
messages=[...]
|
|
@@ -197,7 +197,7 @@ class ClaudeBackend(LLMBackend):
|
|
|
197
197
|
headers["anthropic-beta"] = "code-execution-2025-05-22"
|
|
198
198
|
|
|
199
199
|
stream = await self.client.beta.messages.create(
|
|
200
|
-
model="claude-3-5-sonnet-
|
|
200
|
+
model="claude-3-5-sonnet-20250114",
|
|
201
201
|
messages=messages,
|
|
202
202
|
tools=combined_tools,
|
|
203
203
|
headers=headers,
|