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
|
@@ -17,6 +17,7 @@ from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple
|
|
|
17
17
|
import httpx
|
|
18
18
|
|
|
19
19
|
from ..logger_config import log_backend_activity, logger
|
|
20
|
+
from ..tool import ToolManager
|
|
20
21
|
from .base import LLMBackend, StreamChunk
|
|
21
22
|
|
|
22
23
|
|
|
@@ -107,13 +108,22 @@ SUPPORTED_AUDIO_MIME_TYPES = {
|
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
|
|
110
|
-
class
|
|
111
|
+
class CustomToolAndMCPBackend(LLMBackend):
|
|
111
112
|
"""Base backend class with MCP (Model Context Protocol) support."""
|
|
112
113
|
|
|
113
114
|
def __init__(self, api_key: Optional[str] = None, **kwargs):
|
|
114
115
|
"""Initialize backend with MCP support."""
|
|
115
116
|
super().__init__(api_key, **kwargs)
|
|
116
117
|
|
|
118
|
+
# Custom tools support - initialize before api_params_handler
|
|
119
|
+
self.custom_tool_manager = ToolManager()
|
|
120
|
+
self._custom_tool_names: set[str] = set()
|
|
121
|
+
|
|
122
|
+
# Register custom tools if provided
|
|
123
|
+
custom_tools = kwargs.get("custom_tools", [])
|
|
124
|
+
if custom_tools:
|
|
125
|
+
self._register_custom_tools(custom_tools)
|
|
126
|
+
|
|
117
127
|
# MCP integration (filesystem MCP server may have been injected by base class)
|
|
118
128
|
self.mcp_servers = self.config.get("mcp_servers", [])
|
|
119
129
|
self.allowed_tools = kwargs.pop("allowed_tools", None)
|
|
@@ -169,6 +179,261 @@ class MCPBackend(LLMBackend):
|
|
|
169
179
|
async def _process_stream(self, stream, all_params, agent_id: Optional[str] = None) -> AsyncGenerator[StreamChunk, None]:
|
|
170
180
|
"""Process stream."""
|
|
171
181
|
|
|
182
|
+
# Custom tools support
|
|
183
|
+
def _register_custom_tools(self, custom_tools: List[Dict[str, Any]]) -> None:
|
|
184
|
+
"""Register custom tools with the tool manager.
|
|
185
|
+
|
|
186
|
+
Supports flexible configuration:
|
|
187
|
+
- function: str | List[str]
|
|
188
|
+
- description: str (shared) | List[str] (1-to-1 mapping)
|
|
189
|
+
- preset_args: dict (shared) | List[dict] (1-to-1 mapping)
|
|
190
|
+
|
|
191
|
+
Examples:
|
|
192
|
+
# Single function
|
|
193
|
+
function: "my_func"
|
|
194
|
+
description: "My description"
|
|
195
|
+
|
|
196
|
+
# Multiple functions with shared description
|
|
197
|
+
function: ["func1", "func2"]
|
|
198
|
+
description: "Shared description"
|
|
199
|
+
|
|
200
|
+
# Multiple functions with individual descriptions
|
|
201
|
+
function: ["func1", "func2"]
|
|
202
|
+
description: ["Description 1", "Description 2"]
|
|
203
|
+
|
|
204
|
+
# Multiple functions with mixed (shared desc, individual args)
|
|
205
|
+
function: ["func1", "func2"]
|
|
206
|
+
description: "Shared description"
|
|
207
|
+
preset_args: [{"arg1": "val1"}, {"arg1": "val2"}]
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
custom_tools: List of custom tool configurations
|
|
211
|
+
"""
|
|
212
|
+
# Collect unique categories and create them if needed
|
|
213
|
+
categories = set()
|
|
214
|
+
for tool_config in custom_tools:
|
|
215
|
+
if isinstance(tool_config, dict):
|
|
216
|
+
category = tool_config.get("category", "default")
|
|
217
|
+
if category != "default":
|
|
218
|
+
categories.add(category)
|
|
219
|
+
|
|
220
|
+
# Create categories that don't exist
|
|
221
|
+
for category in categories:
|
|
222
|
+
if category not in self.custom_tool_manager.tool_categories:
|
|
223
|
+
self.custom_tool_manager.setup_category(
|
|
224
|
+
category_name=category,
|
|
225
|
+
description=f"Custom {category} tools",
|
|
226
|
+
enabled=True,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Register each custom tool
|
|
230
|
+
for tool_config in custom_tools:
|
|
231
|
+
try:
|
|
232
|
+
if isinstance(tool_config, dict):
|
|
233
|
+
# Extract base configuration
|
|
234
|
+
path = tool_config.get("path")
|
|
235
|
+
category = tool_config.get("category", "default")
|
|
236
|
+
|
|
237
|
+
# Normalize function field to list
|
|
238
|
+
func_field = tool_config.get("function")
|
|
239
|
+
if isinstance(func_field, str):
|
|
240
|
+
functions = [func_field]
|
|
241
|
+
elif isinstance(func_field, list):
|
|
242
|
+
functions = func_field
|
|
243
|
+
else:
|
|
244
|
+
logger.error(
|
|
245
|
+
f"Invalid function field type: {type(func_field)}. " f"Must be str or List[str].",
|
|
246
|
+
)
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
if not functions:
|
|
250
|
+
logger.error("Empty function list in tool config")
|
|
251
|
+
continue
|
|
252
|
+
|
|
253
|
+
num_functions = len(functions)
|
|
254
|
+
|
|
255
|
+
# Process name field (can be str or List[str])
|
|
256
|
+
name_field = tool_config.get("name")
|
|
257
|
+
names = self._process_field_for_functions(
|
|
258
|
+
name_field,
|
|
259
|
+
num_functions,
|
|
260
|
+
"name",
|
|
261
|
+
)
|
|
262
|
+
if names is None:
|
|
263
|
+
continue # Validation error, skip this tool
|
|
264
|
+
|
|
265
|
+
# Process description field (can be str or List[str])
|
|
266
|
+
desc_field = tool_config.get("description")
|
|
267
|
+
descriptions = self._process_field_for_functions(
|
|
268
|
+
desc_field,
|
|
269
|
+
num_functions,
|
|
270
|
+
"description",
|
|
271
|
+
)
|
|
272
|
+
if descriptions is None:
|
|
273
|
+
continue # Validation error, skip this tool
|
|
274
|
+
|
|
275
|
+
# Process preset_args field (can be dict or List[dict])
|
|
276
|
+
preset_field = tool_config.get("preset_args")
|
|
277
|
+
preset_args_list = self._process_field_for_functions(
|
|
278
|
+
preset_field,
|
|
279
|
+
num_functions,
|
|
280
|
+
"preset_args",
|
|
281
|
+
)
|
|
282
|
+
if preset_args_list is None:
|
|
283
|
+
continue # Validation error, skip this tool
|
|
284
|
+
|
|
285
|
+
# Register each function with its corresponding values
|
|
286
|
+
for i, func in enumerate(functions):
|
|
287
|
+
# Load the function first if custom name is needed
|
|
288
|
+
if names[i] and names[i] != func:
|
|
289
|
+
# Need to load function and apply custom name
|
|
290
|
+
if path:
|
|
291
|
+
loaded_func = self.custom_tool_manager._load_function_from_path(path, func)
|
|
292
|
+
else:
|
|
293
|
+
loaded_func = self.custom_tool_manager._load_builtin_function(func)
|
|
294
|
+
|
|
295
|
+
if loaded_func is None:
|
|
296
|
+
logger.error(f"Could not load function '{func}' from path: {path}")
|
|
297
|
+
continue
|
|
298
|
+
|
|
299
|
+
# Apply custom name by modifying __name__ attribute
|
|
300
|
+
loaded_func.__name__ = names[i]
|
|
301
|
+
|
|
302
|
+
# Register with loaded function (no path needed)
|
|
303
|
+
self.custom_tool_manager.add_tool_function(
|
|
304
|
+
path=None,
|
|
305
|
+
func=loaded_func,
|
|
306
|
+
category=category,
|
|
307
|
+
preset_args=preset_args_list[i],
|
|
308
|
+
description=descriptions[i],
|
|
309
|
+
)
|
|
310
|
+
else:
|
|
311
|
+
# No custom name or same as function name, use normal registration
|
|
312
|
+
self.custom_tool_manager.add_tool_function(
|
|
313
|
+
path=path,
|
|
314
|
+
func=func,
|
|
315
|
+
category=category,
|
|
316
|
+
preset_args=preset_args_list[i],
|
|
317
|
+
description=descriptions[i],
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
# Use custom name for logging and tracking if provided
|
|
321
|
+
registered_name = names[i] if names[i] else func
|
|
322
|
+
|
|
323
|
+
# Track tool name for categorization
|
|
324
|
+
if registered_name.startswith("custom_tool__"):
|
|
325
|
+
self._custom_tool_names.add(registered_name)
|
|
326
|
+
else:
|
|
327
|
+
self._custom_tool_names.add(f"custom_tool__{registered_name}")
|
|
328
|
+
|
|
329
|
+
logger.info(
|
|
330
|
+
f"Registered custom tool: {registered_name} from {path} " f"(category: {category}, " f"desc: '{descriptions[i][:50] if descriptions[i] else 'None'}...')",
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
except Exception as e:
|
|
334
|
+
func_name = tool_config.get("function", "unknown")
|
|
335
|
+
logger.error(
|
|
336
|
+
f"Failed to register custom tool {func_name}: {e}",
|
|
337
|
+
exc_info=True,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
def _process_field_for_functions(
|
|
341
|
+
self,
|
|
342
|
+
field_value: Any,
|
|
343
|
+
num_functions: int,
|
|
344
|
+
field_name: str,
|
|
345
|
+
) -> Optional[List[Any]]:
|
|
346
|
+
"""Process a config field that can be a single value or list.
|
|
347
|
+
|
|
348
|
+
Conversion rules:
|
|
349
|
+
- None ā [None, None, ...] (repeated num_functions times)
|
|
350
|
+
- Single value (not list) ā [value, value, ...] (shared)
|
|
351
|
+
- List with matching length ā use as-is (1-to-1 mapping)
|
|
352
|
+
- List with wrong length ā ERROR (return None)
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
field_value: The field value from config
|
|
356
|
+
num_functions: Number of functions being registered
|
|
357
|
+
field_name: Name of the field (for error messages)
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
List of values (one per function), or None if validation fails
|
|
361
|
+
|
|
362
|
+
Examples:
|
|
363
|
+
_process_field_for_functions(None, 3, "desc")
|
|
364
|
+
ā [None, None, None]
|
|
365
|
+
|
|
366
|
+
_process_field_for_functions("shared", 3, "desc")
|
|
367
|
+
ā ["shared", "shared", "shared"]
|
|
368
|
+
|
|
369
|
+
_process_field_for_functions(["a", "b", "c"], 3, "desc")
|
|
370
|
+
ā ["a", "b", "c"]
|
|
371
|
+
|
|
372
|
+
_process_field_for_functions(["a", "b"], 3, "desc")
|
|
373
|
+
ā None (error logged)
|
|
374
|
+
"""
|
|
375
|
+
# Case 1: None or missing field ā use None for all functions
|
|
376
|
+
if field_value is None:
|
|
377
|
+
return [None] * num_functions
|
|
378
|
+
|
|
379
|
+
# Case 2: Single value (not a list) ā share across all functions
|
|
380
|
+
if not isinstance(field_value, list):
|
|
381
|
+
return [field_value] * num_functions
|
|
382
|
+
|
|
383
|
+
# Case 3: List value ā must match function count exactly
|
|
384
|
+
if len(field_value) == num_functions:
|
|
385
|
+
return field_value
|
|
386
|
+
else:
|
|
387
|
+
# Length mismatch ā validation error
|
|
388
|
+
logger.error(
|
|
389
|
+
f"Configuration error: {field_name} is a list with "
|
|
390
|
+
f"{len(field_value)} items, but there are {num_functions} functions. "
|
|
391
|
+
f"Either use a single value (shared) or a list with exactly "
|
|
392
|
+
f"{num_functions} items (1-to-1 mapping).",
|
|
393
|
+
)
|
|
394
|
+
return None
|
|
395
|
+
|
|
396
|
+
async def _execute_custom_tool(self, call: Dict[str, Any]) -> str:
|
|
397
|
+
"""Execute a custom tool and return the result.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
call: Function call dictionary with name and arguments
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
The execution result as a string
|
|
404
|
+
"""
|
|
405
|
+
import json
|
|
406
|
+
|
|
407
|
+
tool_request = {
|
|
408
|
+
"name": call["name"],
|
|
409
|
+
"input": json.loads(call["arguments"]) if isinstance(call["arguments"], str) else call["arguments"],
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
result_text = ""
|
|
413
|
+
try:
|
|
414
|
+
async for result in self.custom_tool_manager.execute_tool(tool_request):
|
|
415
|
+
# Accumulate results
|
|
416
|
+
if hasattr(result, "output_blocks"):
|
|
417
|
+
for block in result.output_blocks:
|
|
418
|
+
if hasattr(block, "data"):
|
|
419
|
+
result_text += str(block.data)
|
|
420
|
+
elif hasattr(block, "content"):
|
|
421
|
+
result_text += str(block.content)
|
|
422
|
+
elif hasattr(result, "content"):
|
|
423
|
+
result_text += str(result.content)
|
|
424
|
+
else:
|
|
425
|
+
result_text += str(result)
|
|
426
|
+
except Exception as e:
|
|
427
|
+
logger.error(f"Error in custom tool execution: {e}")
|
|
428
|
+
result_text = f"Error: {str(e)}"
|
|
429
|
+
|
|
430
|
+
return result_text or "Tool executed successfully"
|
|
431
|
+
|
|
432
|
+
def _get_custom_tools_schemas(self) -> List[Dict[str, Any]]:
|
|
433
|
+
"""Get OpenAI-formatted schemas for all registered custom tools."""
|
|
434
|
+
return self.custom_tool_manager.fetch_tool_schemas()
|
|
435
|
+
|
|
436
|
+
# MCP support methods
|
|
172
437
|
async def _setup_mcp_tools(self) -> None:
|
|
173
438
|
"""Initialize MCP client for mcp_tools-based servers (stdio + streamable-http)."""
|
|
174
439
|
if not self.mcp_servers or self._mcp_initialized:
|
|
@@ -268,10 +533,10 @@ class MCPBackend(LLMBackend):
|
|
|
268
533
|
max_retries: int = 3,
|
|
269
534
|
) -> Tuple[str, Any]:
|
|
270
535
|
"""Execute MCP function with exponential backoff retry logic."""
|
|
271
|
-
# Check if
|
|
272
|
-
if self.
|
|
273
|
-
logger.info(f"[MCP] Planning mode enabled - blocking MCP tool
|
|
274
|
-
error_str = "š« [MCP]
|
|
536
|
+
# Check if this specific MCP tool is blocked by planning mode
|
|
537
|
+
if self.is_mcp_tool_blocked(function_name):
|
|
538
|
+
logger.info(f"[MCP] Planning mode enabled - blocking MCP tool: {function_name}")
|
|
539
|
+
error_str = f"š« [MCP] Tool '{function_name}' blocked during coordination (planning mode active)"
|
|
275
540
|
return error_str, {"error": error_str, "blocked_by": "planning_mode", "function_name": function_name}
|
|
276
541
|
|
|
277
542
|
# Convert JSON string to dict for shared utility
|
|
@@ -783,14 +1048,16 @@ class MCPBackend(LLMBackend):
|
|
|
783
1048
|
async for chunk in self.yield_mcp_status_chunks(use_mcp):
|
|
784
1049
|
yield chunk
|
|
785
1050
|
|
|
786
|
-
|
|
1051
|
+
use_custom_tools = bool(self._custom_tool_names)
|
|
1052
|
+
|
|
1053
|
+
if use_mcp or use_custom_tools:
|
|
787
1054
|
# MCP MODE: Recursive function call detection and execution
|
|
788
1055
|
logger.info("Using recursive MCP execution mode")
|
|
789
1056
|
|
|
790
1057
|
current_messages = self._trim_message_history(messages.copy())
|
|
791
1058
|
|
|
792
1059
|
# Start recursive MCP streaming
|
|
793
|
-
async for chunk in self.
|
|
1060
|
+
async for chunk in self._stream_with_custom_and_mcp_tools(current_messages, tools, client, **kwargs):
|
|
794
1061
|
yield chunk
|
|
795
1062
|
|
|
796
1063
|
else:
|
|
@@ -798,7 +1065,7 @@ class MCPBackend(LLMBackend):
|
|
|
798
1065
|
logger.info("Using no-MCP mode")
|
|
799
1066
|
|
|
800
1067
|
# Start non-MCP streaming
|
|
801
|
-
async for chunk in self.
|
|
1068
|
+
async for chunk in self._stream_without_custom_and_mcp_tools(messages, tools, client, **kwargs):
|
|
802
1069
|
yield chunk
|
|
803
1070
|
|
|
804
1071
|
except Exception as e:
|
|
@@ -808,7 +1075,7 @@ class MCPBackend(LLMBackend):
|
|
|
808
1075
|
await self._record_mcp_circuit_breaker_failure(e, agent_id)
|
|
809
1076
|
|
|
810
1077
|
# Handle MCP exceptions with fallback
|
|
811
|
-
async for chunk in self.
|
|
1078
|
+
async for chunk in self._stream_handle_custom_and_mcp_exceptions(e, messages, tools, client, **kwargs):
|
|
812
1079
|
yield chunk
|
|
813
1080
|
else:
|
|
814
1081
|
logger.error(f"Streaming error: {e}")
|
|
@@ -824,7 +1091,7 @@ class MCPBackend(LLMBackend):
|
|
|
824
1091
|
|
|
825
1092
|
if isinstance(e, (MCPConnectionError, MCPTimeoutError, MCPServerError, MCPError)):
|
|
826
1093
|
# Handle MCP exceptions with fallback
|
|
827
|
-
async for chunk in self.
|
|
1094
|
+
async for chunk in self._stream_handle_custom_and_mcp_exceptions(e, messages, tools, client, **kwargs):
|
|
828
1095
|
yield chunk
|
|
829
1096
|
else:
|
|
830
1097
|
# Generic setup error: still notify if MCP was configured
|
|
@@ -837,7 +1104,7 @@ class MCPBackend(LLMBackend):
|
|
|
837
1104
|
)
|
|
838
1105
|
|
|
839
1106
|
# Proceed with non-MCP streaming
|
|
840
|
-
async for chunk in self.
|
|
1107
|
+
async for chunk in self._stream_without_custom_and_mcp_tools(messages, tools, client, **kwargs):
|
|
841
1108
|
yield chunk
|
|
842
1109
|
except Exception as inner_e:
|
|
843
1110
|
logger.error(f"Streaming error during MCP setup fallback: {inner_e}")
|
|
@@ -845,7 +1112,7 @@ class MCPBackend(LLMBackend):
|
|
|
845
1112
|
finally:
|
|
846
1113
|
await self._cleanup_client(client)
|
|
847
1114
|
|
|
848
|
-
async def
|
|
1115
|
+
async def _stream_without_custom_and_mcp_tools(
|
|
849
1116
|
self,
|
|
850
1117
|
messages: List[Dict[str, Any]],
|
|
851
1118
|
tools: List[Dict[str, Any]],
|
|
@@ -885,7 +1152,7 @@ class MCPBackend(LLMBackend):
|
|
|
885
1152
|
async for chunk in self._process_stream(stream, all_params, agent_id):
|
|
886
1153
|
yield chunk
|
|
887
1154
|
|
|
888
|
-
async def
|
|
1155
|
+
async def _stream_handle_custom_and_mcp_exceptions(
|
|
889
1156
|
self,
|
|
890
1157
|
error: Exception,
|
|
891
1158
|
messages: List[Dict[str, Any]],
|
|
@@ -921,7 +1188,7 @@ class MCPBackend(LLMBackend):
|
|
|
921
1188
|
content=f"\nā ļø {user_message} ({error}); continuing without MCP tools\n",
|
|
922
1189
|
)
|
|
923
1190
|
|
|
924
|
-
async for chunk in self.
|
|
1191
|
+
async for chunk in self._stream_without_custom_and_mcp_tools(messages, tools, client, **kwargs):
|
|
925
1192
|
yield chunk
|
|
926
1193
|
|
|
927
1194
|
def _track_mcp_function_names(self, tools: List[Dict[str, Any]]) -> None:
|
|
@@ -1012,7 +1279,7 @@ class MCPBackend(LLMBackend):
|
|
|
1012
1279
|
self._mcp_functions.clear()
|
|
1013
1280
|
self._mcp_function_names.clear()
|
|
1014
1281
|
|
|
1015
|
-
async def __aenter__(self) -> "
|
|
1282
|
+
async def __aenter__(self) -> "CustomToolAndMCPBackend":
|
|
1016
1283
|
"""Async context manager entry."""
|
|
1017
1284
|
# Initialize MCP tools if configured
|
|
1018
1285
|
if MCPResourceManager:
|
|
@@ -1087,6 +1354,10 @@ class MCPBackend(LLMBackend):
|
|
|
1087
1354
|
"""Check if a tool call is an MCP function."""
|
|
1088
1355
|
return tool_name in self._mcp_functions
|
|
1089
1356
|
|
|
1357
|
+
def is_custom_tool_call(self, tool_name: str) -> bool:
|
|
1358
|
+
"""Check if a tool call is a custom tool function."""
|
|
1359
|
+
return tool_name in self._custom_tool_names
|
|
1360
|
+
|
|
1090
1361
|
def get_mcp_tools_formatted(self) -> List[Dict[str, Any]]:
|
|
1091
1362
|
"""Get MCP tools formatted for specific API format."""
|
|
1092
1363
|
if not self._mcp_functions:
|
massgen/backend/capabilities.py
CHANGED
|
@@ -137,13 +137,14 @@ BACKEND_CAPABILITIES: Dict[str, BackendCapabilities] = {
|
|
|
137
137
|
builtin_tools=["web_search", "code_execution"],
|
|
138
138
|
filesystem_support="mcp",
|
|
139
139
|
models=[
|
|
140
|
+
"claude-haiku-4-5-20251001",
|
|
140
141
|
"claude-sonnet-4-5-20250929",
|
|
142
|
+
"claude-opus-4-1-20250805",
|
|
141
143
|
"claude-sonnet-4-20250514",
|
|
142
|
-
"claude-opus-4-20250514",
|
|
143
144
|
"claude-3-5-sonnet-latest",
|
|
144
145
|
"claude-3-5-haiku-latest",
|
|
145
146
|
],
|
|
146
|
-
default_model="claude-sonnet-4-
|
|
147
|
+
default_model="claude-sonnet-4-5-20250929",
|
|
147
148
|
env_var="ANTHROPIC_API_KEY",
|
|
148
149
|
notes="Web search and code execution are built-in tools. Audio/video understanding support (v0.0.30+).",
|
|
149
150
|
),
|
|
@@ -175,8 +176,8 @@ BACKEND_CAPABILITIES: Dict[str, BackendCapabilities] = {
|
|
|
175
176
|
filesystem_support="native",
|
|
176
177
|
models=[
|
|
177
178
|
"claude-sonnet-4-5-20250929",
|
|
179
|
+
"claude-opus-4-1-20250805",
|
|
178
180
|
"claude-sonnet-4-20250514",
|
|
179
|
-
"claude-opus-4-20250514",
|
|
180
181
|
],
|
|
181
182
|
default_model="claude-sonnet-4-5-20250929",
|
|
182
183
|
env_var="ANTHROPIC_API_KEY",
|
|
@@ -218,12 +219,11 @@ BACKEND_CAPABILITIES: Dict[str, BackendCapabilities] = {
|
|
|
218
219
|
filesystem_support="mcp",
|
|
219
220
|
models=[
|
|
220
221
|
"grok-4",
|
|
222
|
+
"grok-4-fast",
|
|
221
223
|
"grok-3",
|
|
222
224
|
"grok-3-mini",
|
|
223
|
-
"grok-beta",
|
|
224
|
-
"grok-vision-beta",
|
|
225
225
|
],
|
|
226
|
-
default_model="grok-
|
|
226
|
+
default_model="grok-4",
|
|
227
227
|
env_var="XAI_API_KEY",
|
|
228
228
|
notes="Web search includes real-time data access.",
|
|
229
229
|
),
|