massgen 0.1.0a3__py3-none-any.whl → 0.1.1__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 +8 -1
- 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 +31 -0
- massgen/backend/{base_with_mcp.py → base_with_custom_tool_and_mcp.py} +282 -11
- massgen/backend/chat_completions.py +182 -92
- 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 +1275 -1607
- massgen/backend/gemini_mcp_manager.py +545 -0
- massgen/backend/gemini_trackers.py +344 -0
- massgen/backend/gemini_utils.py +43 -0
- massgen/backend/response.py +129 -70
- massgen/cli.py +577 -110
- massgen/config_builder.py +376 -27
- massgen/configs/README.md +111 -80
- massgen/configs/basic/multi/three_agents_default.yaml +1 -1
- 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/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 +179 -10
- 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_tools.py +127 -0
- 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.1.dist-info}/METADATA +89 -131
- {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/RECORD +111 -36
- {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/WHEEL +0 -0
- {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/entry_points.txt +0 -0
- {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/licenses/LICENSE +0 -0
- {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/top_level.txt +0 -0
|
@@ -258,6 +258,110 @@ class ChatCompletionsFormatter(FormatterBase):
|
|
|
258
258
|
|
|
259
259
|
return converted_tools
|
|
260
260
|
|
|
261
|
+
def format_custom_tools(self, custom_tools: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
262
|
+
"""
|
|
263
|
+
Convert custom tools from RegisteredToolEntry format to Chat Completions API format.
|
|
264
|
+
|
|
265
|
+
Custom tools are provided as a dictionary where:
|
|
266
|
+
- Keys are tool names (str)
|
|
267
|
+
- Values are RegisteredToolEntry objects with:
|
|
268
|
+
- tool_name: str
|
|
269
|
+
- schema_def: dict with structure {"type": "function", "function": {...}}
|
|
270
|
+
- get_extended_schema: property that returns the schema with extensions
|
|
271
|
+
|
|
272
|
+
Chat Completions API expects: {"type": "function", "function": {"name": ..., "description": ..., "parameters": ...}}
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
custom_tools: Dictionary of tool_name -> RegisteredToolEntry objects
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
List of tools in Chat Completions API format
|
|
279
|
+
"""
|
|
280
|
+
if not custom_tools:
|
|
281
|
+
return []
|
|
282
|
+
|
|
283
|
+
converted_tools = []
|
|
284
|
+
|
|
285
|
+
# Handle dictionary format: {tool_name: RegisteredToolEntry, ...}
|
|
286
|
+
if isinstance(custom_tools, dict):
|
|
287
|
+
for tool_name, tool_entry in custom_tools.items():
|
|
288
|
+
# Check if it's a RegisteredToolEntry object with schema_def
|
|
289
|
+
if hasattr(tool_entry, "schema_def"):
|
|
290
|
+
tool_schema = tool_entry.schema_def
|
|
291
|
+
|
|
292
|
+
# Schema may already be in Chat Completions format
|
|
293
|
+
if tool_schema.get("type") == "function" and "function" in tool_schema:
|
|
294
|
+
# Already in correct format, just append
|
|
295
|
+
converted_tools.append(tool_schema)
|
|
296
|
+
elif tool_schema.get("type") == "function":
|
|
297
|
+
# Response API format, need to wrap in function object
|
|
298
|
+
converted_tools.append(
|
|
299
|
+
{
|
|
300
|
+
"type": "function",
|
|
301
|
+
"function": {
|
|
302
|
+
"name": tool_schema.get("name", tool_entry.tool_name if hasattr(tool_entry, "tool_name") else tool_name),
|
|
303
|
+
"description": tool_schema.get("description", ""),
|
|
304
|
+
"parameters": tool_schema.get("parameters", {}),
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
)
|
|
308
|
+
# Check if it has get_extended_schema property
|
|
309
|
+
elif hasattr(tool_entry, "get_extended_schema"):
|
|
310
|
+
tool_schema = tool_entry.get_extended_schema
|
|
311
|
+
|
|
312
|
+
if tool_schema.get("type") == "function" and "function" in tool_schema:
|
|
313
|
+
# Already in correct format
|
|
314
|
+
converted_tools.append(tool_schema)
|
|
315
|
+
elif tool_schema.get("type") == "function":
|
|
316
|
+
# Response API format, need to wrap
|
|
317
|
+
converted_tools.append(
|
|
318
|
+
{
|
|
319
|
+
"type": "function",
|
|
320
|
+
"function": {
|
|
321
|
+
"name": tool_schema.get("name", tool_entry.tool_name if hasattr(tool_entry, "tool_name") else tool_name),
|
|
322
|
+
"description": tool_schema.get("description", ""),
|
|
323
|
+
"parameters": tool_schema.get("parameters", {}),
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
)
|
|
327
|
+
# Handle list format for backward compatibility
|
|
328
|
+
elif isinstance(custom_tools, list):
|
|
329
|
+
for tool in custom_tools:
|
|
330
|
+
if hasattr(tool, "schema_def"):
|
|
331
|
+
tool_schema = tool.schema_def
|
|
332
|
+
|
|
333
|
+
if tool_schema.get("type") == "function" and "function" in tool_schema:
|
|
334
|
+
converted_tools.append(tool_schema)
|
|
335
|
+
elif tool_schema.get("type") == "function":
|
|
336
|
+
converted_tools.append(
|
|
337
|
+
{
|
|
338
|
+
"type": "function",
|
|
339
|
+
"function": {
|
|
340
|
+
"name": tool_schema.get("name", tool.tool_name),
|
|
341
|
+
"description": tool_schema.get("description", ""),
|
|
342
|
+
"parameters": tool_schema.get("parameters", {}),
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
)
|
|
346
|
+
elif hasattr(tool, "get_extended_schema"):
|
|
347
|
+
tool_schema = tool.get_extended_schema
|
|
348
|
+
|
|
349
|
+
if tool_schema.get("type") == "function" and "function" in tool_schema:
|
|
350
|
+
converted_tools.append(tool_schema)
|
|
351
|
+
elif tool_schema.get("type") == "function":
|
|
352
|
+
converted_tools.append(
|
|
353
|
+
{
|
|
354
|
+
"type": "function",
|
|
355
|
+
"function": {
|
|
356
|
+
"name": tool_schema.get("name", tool.tool_name),
|
|
357
|
+
"description": tool_schema.get("description", ""),
|
|
358
|
+
"parameters": tool_schema.get("parameters", {}),
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
return converted_tools
|
|
364
|
+
|
|
261
365
|
def format_mcp_tools(self, mcp_functions: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
262
366
|
"""Convert MCP tools to Chat Completions format."""
|
|
263
367
|
if not mcp_functions:
|
|
@@ -233,3 +233,123 @@ class ClaudeFormatter(FormatterBase):
|
|
|
233
233
|
converted_tools.append(tool)
|
|
234
234
|
|
|
235
235
|
return converted_tools
|
|
236
|
+
|
|
237
|
+
def format_custom_tools(self, custom_tools: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
238
|
+
"""
|
|
239
|
+
Convert custom tools from RegisteredToolEntry format to Claude's custom tool format.
|
|
240
|
+
|
|
241
|
+
Custom tools are provided as a dictionary where:
|
|
242
|
+
- Keys are tool names (str)
|
|
243
|
+
- Values are RegisteredToolEntry objects with:
|
|
244
|
+
- tool_name: str
|
|
245
|
+
- schema_def: dict with structure {"type": "function", "function": {...}}
|
|
246
|
+
- get_extended_schema: property that returns the schema with extensions
|
|
247
|
+
|
|
248
|
+
Claude expects: {"type": "custom", "name": ..., "description": ..., "input_schema": ...}
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
custom_tools: Dictionary of tool_name -> RegisteredToolEntry objects
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
List of tools in Claude's custom tool format
|
|
255
|
+
"""
|
|
256
|
+
if not custom_tools:
|
|
257
|
+
return []
|
|
258
|
+
|
|
259
|
+
converted_tools = []
|
|
260
|
+
|
|
261
|
+
# Handle dictionary format: {tool_name: RegisteredToolEntry, ...}
|
|
262
|
+
if isinstance(custom_tools, dict):
|
|
263
|
+
for tool_name, tool_entry in custom_tools.items():
|
|
264
|
+
# Check if it's a RegisteredToolEntry object with schema_def
|
|
265
|
+
if hasattr(tool_entry, "schema_def"):
|
|
266
|
+
tool_schema = tool_entry.schema_def
|
|
267
|
+
|
|
268
|
+
# Extract function details from Chat Completions format
|
|
269
|
+
if tool_schema.get("type") == "function" and "function" in tool_schema:
|
|
270
|
+
func = tool_schema["function"]
|
|
271
|
+
converted_tools.append(
|
|
272
|
+
{
|
|
273
|
+
"type": "custom",
|
|
274
|
+
"name": func.get("name", tool_entry.tool_name if hasattr(tool_entry, "tool_name") else tool_name),
|
|
275
|
+
"description": func.get("description", ""),
|
|
276
|
+
"input_schema": func.get("parameters", {}),
|
|
277
|
+
},
|
|
278
|
+
)
|
|
279
|
+
elif tool_schema.get("type") == "function":
|
|
280
|
+
# Response API format - already has name, description, parameters at top level
|
|
281
|
+
converted_tools.append(
|
|
282
|
+
{
|
|
283
|
+
"type": "custom",
|
|
284
|
+
"name": tool_schema.get("name", tool_entry.tool_name if hasattr(tool_entry, "tool_name") else tool_name),
|
|
285
|
+
"description": tool_schema.get("description", ""),
|
|
286
|
+
"input_schema": tool_schema.get("parameters", {}),
|
|
287
|
+
},
|
|
288
|
+
)
|
|
289
|
+
else:
|
|
290
|
+
# Unknown format, try to extract what we can
|
|
291
|
+
converted_tools.append(
|
|
292
|
+
{
|
|
293
|
+
"type": "custom",
|
|
294
|
+
"name": tool_entry.tool_name if hasattr(tool_entry, "tool_name") else tool_name,
|
|
295
|
+
"description": tool_schema.get("description", ""),
|
|
296
|
+
"input_schema": tool_schema.get("parameters", {}),
|
|
297
|
+
},
|
|
298
|
+
)
|
|
299
|
+
# Handle direct schema format (for backward compatibility)
|
|
300
|
+
elif isinstance(tool_entry, dict):
|
|
301
|
+
if tool_entry.get("type") == "function" and "function" in tool_entry:
|
|
302
|
+
# Chat Completions format
|
|
303
|
+
func = tool_entry["function"]
|
|
304
|
+
converted_tools.append(
|
|
305
|
+
{
|
|
306
|
+
"type": "custom",
|
|
307
|
+
"name": func.get("name", tool_name),
|
|
308
|
+
"description": func.get("description", ""),
|
|
309
|
+
"input_schema": func.get("parameters", {}),
|
|
310
|
+
},
|
|
311
|
+
)
|
|
312
|
+
elif tool_entry.get("type") == "function":
|
|
313
|
+
# Response API format
|
|
314
|
+
converted_tools.append(
|
|
315
|
+
{
|
|
316
|
+
"type": "custom",
|
|
317
|
+
"name": tool_entry.get("name", tool_name),
|
|
318
|
+
"description": tool_entry.get("description", ""),
|
|
319
|
+
"input_schema": tool_entry.get("parameters", {}),
|
|
320
|
+
},
|
|
321
|
+
)
|
|
322
|
+
else:
|
|
323
|
+
# Already in Claude format or unknown
|
|
324
|
+
converted_tools.append(tool_entry)
|
|
325
|
+
|
|
326
|
+
# Handle list format (if custom_tools is already a list)
|
|
327
|
+
elif isinstance(custom_tools, list):
|
|
328
|
+
for tool in custom_tools:
|
|
329
|
+
if isinstance(tool, dict):
|
|
330
|
+
if tool.get("type") == "function" and "function" in tool:
|
|
331
|
+
# Chat Completions format
|
|
332
|
+
func = tool["function"]
|
|
333
|
+
converted_tools.append(
|
|
334
|
+
{
|
|
335
|
+
"type": "custom",
|
|
336
|
+
"name": func.get("name", ""),
|
|
337
|
+
"description": func.get("description", ""),
|
|
338
|
+
"input_schema": func.get("parameters", {}),
|
|
339
|
+
},
|
|
340
|
+
)
|
|
341
|
+
elif tool.get("type") == "function":
|
|
342
|
+
# Response API format
|
|
343
|
+
converted_tools.append(
|
|
344
|
+
{
|
|
345
|
+
"type": "custom",
|
|
346
|
+
"name": tool.get("name", ""),
|
|
347
|
+
"description": tool.get("description", ""),
|
|
348
|
+
"input_schema": tool.get("parameters", {}),
|
|
349
|
+
},
|
|
350
|
+
)
|
|
351
|
+
else:
|
|
352
|
+
# Already in Claude format or unknown
|
|
353
|
+
converted_tools.append(tool)
|
|
354
|
+
|
|
355
|
+
return converted_tools
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Gemini formatter for message formatting, coordination prompts, and structured output parsing.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import re
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
from ._formatter_base import FormatterBase
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GeminiFormatter(FormatterBase):
|
|
17
|
+
def format_messages(self, messages: List[Dict[str, Any]]) -> str:
|
|
18
|
+
"""
|
|
19
|
+
Build conversation content string from message history.
|
|
20
|
+
|
|
21
|
+
Behavior mirrors the formatting used previously in the Gemini backend:
|
|
22
|
+
- System messages buffered separately then prepended.
|
|
23
|
+
- User => "User: {content}"
|
|
24
|
+
- Assistant => "Assistant: {content}"
|
|
25
|
+
- Tool => "Tool Result: {content}"
|
|
26
|
+
"""
|
|
27
|
+
conversation_content = ""
|
|
28
|
+
system_message = ""
|
|
29
|
+
|
|
30
|
+
for msg in messages:
|
|
31
|
+
role = msg.get("role")
|
|
32
|
+
if role == "system":
|
|
33
|
+
system_message = msg.get("content", "")
|
|
34
|
+
elif role == "user":
|
|
35
|
+
conversation_content += f"User: {msg.get('content', '')}\n"
|
|
36
|
+
elif role == "assistant":
|
|
37
|
+
conversation_content += f"Assistant: {msg.get('content', '')}\n"
|
|
38
|
+
elif role == "tool":
|
|
39
|
+
tool_output = msg.get("content", "")
|
|
40
|
+
conversation_content += f"Tool Result: {tool_output}\n"
|
|
41
|
+
|
|
42
|
+
# Combine system message and conversation
|
|
43
|
+
full_content = ""
|
|
44
|
+
if system_message:
|
|
45
|
+
full_content += f"{system_message}\n\n"
|
|
46
|
+
full_content += conversation_content
|
|
47
|
+
return full_content
|
|
48
|
+
|
|
49
|
+
def format_tools(self, tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
50
|
+
"""
|
|
51
|
+
Gemini uses SDK-native tool format, not reformatting.
|
|
52
|
+
"""
|
|
53
|
+
return tools or []
|
|
54
|
+
|
|
55
|
+
def format_mcp_tools(self, mcp_functions: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
56
|
+
"""
|
|
57
|
+
MCP tools are passed via SDK sessions in stream_with_tools; not function declarations.
|
|
58
|
+
"""
|
|
59
|
+
return []
|
|
60
|
+
|
|
61
|
+
# Coordination helpers
|
|
62
|
+
|
|
63
|
+
def has_coordination_tools(self, tools: List[Dict[str, Any]]) -> bool:
|
|
64
|
+
"""Detect if tools contain vote/new_answer coordination tools."""
|
|
65
|
+
if not tools:
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
tool_names = set()
|
|
69
|
+
for tool in tools:
|
|
70
|
+
if tool.get("type") == "function":
|
|
71
|
+
if "function" in tool:
|
|
72
|
+
tool_names.add(tool["function"].get("name", ""))
|
|
73
|
+
elif "name" in tool:
|
|
74
|
+
tool_names.add(tool.get("name", ""))
|
|
75
|
+
|
|
76
|
+
return "vote" in tool_names and "new_answer" in tool_names
|
|
77
|
+
|
|
78
|
+
def build_structured_output_prompt(self, base_content: str, valid_agent_ids: Optional[List[str]] = None) -> str:
|
|
79
|
+
"""Build prompt that encourages structured output for coordination."""
|
|
80
|
+
agent_list = ""
|
|
81
|
+
if valid_agent_ids:
|
|
82
|
+
agent_list = f"Valid agents: {', '.join(valid_agent_ids)}"
|
|
83
|
+
|
|
84
|
+
return f"""{base_content}
|
|
85
|
+
|
|
86
|
+
IMPORTANT: You must respond with a structured JSON decision at the end of your response.
|
|
87
|
+
|
|
88
|
+
If you want to VOTE for an existing agent's answer:
|
|
89
|
+
{{
|
|
90
|
+
"action_type": "vote",
|
|
91
|
+
"vote_data": {{
|
|
92
|
+
"action": "vote",
|
|
93
|
+
"agent_id": "agent1", // Choose from: {agent_list or "agent1, agent2, agent3, etc."}
|
|
94
|
+
"reason": "Brief reason for your vote"
|
|
95
|
+
}}
|
|
96
|
+
}}
|
|
97
|
+
|
|
98
|
+
If you want to provide a NEW ANSWER:
|
|
99
|
+
{{
|
|
100
|
+
"action_type": "new_answer",
|
|
101
|
+
"answer_data": {{
|
|
102
|
+
"action": "new_answer",
|
|
103
|
+
"content": "Your complete improved answer here"
|
|
104
|
+
}}
|
|
105
|
+
}}
|
|
106
|
+
|
|
107
|
+
Make your decision and include the JSON at the very end of your response."""
|
|
108
|
+
|
|
109
|
+
def extract_structured_response(self, response_text: str) -> Optional[Dict[str, Any]]:
|
|
110
|
+
"""Extract structured JSON response from model output."""
|
|
111
|
+
try:
|
|
112
|
+
# Strategy 0: Look for JSON inside markdown code blocks first
|
|
113
|
+
markdown_json_pattern = r"```json\s*(\{.*?\})\s*```"
|
|
114
|
+
markdown_matches = re.findall(markdown_json_pattern, response_text, re.DOTALL)
|
|
115
|
+
|
|
116
|
+
for match in reversed(markdown_matches):
|
|
117
|
+
try:
|
|
118
|
+
parsed = json.loads(match.strip())
|
|
119
|
+
if isinstance(parsed, dict) and "action_type" in parsed:
|
|
120
|
+
return parsed
|
|
121
|
+
except json.JSONDecodeError:
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
# Strategy 1: Look for complete JSON blocks with proper braces
|
|
125
|
+
json_pattern = r"\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}"
|
|
126
|
+
json_matches = re.findall(json_pattern, response_text, re.DOTALL)
|
|
127
|
+
|
|
128
|
+
# Try parsing each match (in reverse order - last one first)
|
|
129
|
+
for match in reversed(json_matches):
|
|
130
|
+
try:
|
|
131
|
+
cleaned_match = match.strip()
|
|
132
|
+
parsed = json.loads(cleaned_match)
|
|
133
|
+
if isinstance(parsed, dict) and "action_type" in parsed:
|
|
134
|
+
return parsed
|
|
135
|
+
except json.JSONDecodeError:
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
# Strategy 2: Look for JSON blocks with nested braces (more complex)
|
|
139
|
+
brace_count = 0
|
|
140
|
+
json_start = -1
|
|
141
|
+
|
|
142
|
+
for i, char in enumerate(response_text):
|
|
143
|
+
if char == "{":
|
|
144
|
+
if brace_count == 0:
|
|
145
|
+
json_start = i
|
|
146
|
+
brace_count += 1
|
|
147
|
+
elif char == "}":
|
|
148
|
+
brace_count -= 1
|
|
149
|
+
if brace_count == 0 and json_start >= 0:
|
|
150
|
+
# Found a complete JSON block
|
|
151
|
+
json_block = response_text[json_start : i + 1]
|
|
152
|
+
try:
|
|
153
|
+
parsed = json.loads(json_block)
|
|
154
|
+
if isinstance(parsed, dict) and "action_type" in parsed:
|
|
155
|
+
return parsed
|
|
156
|
+
except json.JSONDecodeError:
|
|
157
|
+
pass
|
|
158
|
+
json_start = -1
|
|
159
|
+
|
|
160
|
+
# Strategy 3: Line-by-line approach (fallback)
|
|
161
|
+
lines = response_text.strip().split("\n")
|
|
162
|
+
json_candidates = []
|
|
163
|
+
|
|
164
|
+
for i, line in enumerate(lines):
|
|
165
|
+
stripped = line.strip()
|
|
166
|
+
if stripped.startswith("{") and stripped.endswith("}"):
|
|
167
|
+
json_candidates.append(stripped)
|
|
168
|
+
elif stripped.startswith("{"):
|
|
169
|
+
# Multi-line JSON - collect until closing brace
|
|
170
|
+
json_text = stripped
|
|
171
|
+
for j in range(i + 1, len(lines)):
|
|
172
|
+
json_text += "\n" + lines[j].strip()
|
|
173
|
+
if lines[j].strip().endswith("}"):
|
|
174
|
+
json_candidates.append(json_text)
|
|
175
|
+
break
|
|
176
|
+
|
|
177
|
+
# Try to parse each candidate
|
|
178
|
+
for candidate in reversed(json_candidates):
|
|
179
|
+
try:
|
|
180
|
+
parsed = json.loads(candidate)
|
|
181
|
+
if isinstance(parsed, dict) and "action_type" in parsed:
|
|
182
|
+
return parsed
|
|
183
|
+
except json.JSONDecodeError:
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
except Exception:
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
def convert_structured_to_tool_calls(self, structured_response: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
192
|
+
"""Convert structured response to tool call format."""
|
|
193
|
+
action_type = structured_response.get("action_type")
|
|
194
|
+
|
|
195
|
+
if action_type == "vote":
|
|
196
|
+
vote_data = structured_response.get("vote_data", {})
|
|
197
|
+
return [
|
|
198
|
+
{
|
|
199
|
+
"id": f"vote_{abs(hash(str(vote_data))) % 10000 + 1}",
|
|
200
|
+
"type": "function",
|
|
201
|
+
"function": {
|
|
202
|
+
"name": "vote",
|
|
203
|
+
"arguments": {
|
|
204
|
+
"agent_id": vote_data.get("agent_id", ""),
|
|
205
|
+
"reason": vote_data.get("reason", ""),
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
]
|
|
210
|
+
|
|
211
|
+
elif action_type == "new_answer":
|
|
212
|
+
answer_data = structured_response.get("answer_data", {})
|
|
213
|
+
return [
|
|
214
|
+
{
|
|
215
|
+
"id": f"new_answer_{abs(hash(str(answer_data))) % 10000 + 1}",
|
|
216
|
+
"type": "function",
|
|
217
|
+
"function": {
|
|
218
|
+
"name": "new_answer",
|
|
219
|
+
"arguments": {"content": answer_data.get("content", "")},
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
]
|
|
223
|
+
|
|
224
|
+
return []
|
|
225
|
+
|
|
226
|
+
# Custom tools formatting for Gemini
|
|
227
|
+
|
|
228
|
+
def format_custom_tools(
|
|
229
|
+
self,
|
|
230
|
+
custom_tools: List[Dict[str, Any]],
|
|
231
|
+
return_sdk_objects: bool = True,
|
|
232
|
+
) -> List[Any]:
|
|
233
|
+
"""
|
|
234
|
+
Convert custom tools from OpenAI Chat Completions format to Gemini format.
|
|
235
|
+
|
|
236
|
+
Can return either SDK FunctionDeclaration objects (default) or Gemini-format dictionaries.
|
|
237
|
+
|
|
238
|
+
OpenAI format:
|
|
239
|
+
[{"type": "function", "function": {"name": ..., "description": ..., "parameters": {...}}}]
|
|
240
|
+
|
|
241
|
+
Gemini dictionary format:
|
|
242
|
+
[{"name": ..., "description": ..., "parameters": {...}}]
|
|
243
|
+
|
|
244
|
+
Gemini SDK format:
|
|
245
|
+
[FunctionDeclaration(name=..., description=..., parameters=Schema(...))]
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
custom_tools: List of tools in OpenAI Chat Completions format
|
|
249
|
+
return_sdk_objects: If True, return FunctionDeclaration objects;
|
|
250
|
+
if False, return Gemini-format dictionaries
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
List of tools in Gemini SDK format (default) or dictionary format
|
|
254
|
+
"""
|
|
255
|
+
if not custom_tools:
|
|
256
|
+
return []
|
|
257
|
+
|
|
258
|
+
# Step 1: Convert to Gemini dictionary format
|
|
259
|
+
gemini_dicts = self._convert_to_gemini_dict_format(custom_tools)
|
|
260
|
+
|
|
261
|
+
if not return_sdk_objects:
|
|
262
|
+
return gemini_dicts
|
|
263
|
+
|
|
264
|
+
# Step 2: Convert dictionaries to SDK FunctionDeclaration objects
|
|
265
|
+
return self._convert_to_function_declarations(gemini_dicts)
|
|
266
|
+
|
|
267
|
+
def _convert_to_gemini_dict_format(self, custom_tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
268
|
+
"""
|
|
269
|
+
Convert OpenAI format to Gemini dictionary format (intermediate step).
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
custom_tools: List of tools in OpenAI Chat Completions format
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
List of tools in Gemini-compatible dictionary format
|
|
276
|
+
"""
|
|
277
|
+
if not custom_tools:
|
|
278
|
+
return []
|
|
279
|
+
|
|
280
|
+
converted_tools = []
|
|
281
|
+
|
|
282
|
+
for tool in custom_tools:
|
|
283
|
+
# Handle OpenAI Chat Completions format with type="function" wrapper
|
|
284
|
+
if isinstance(tool, dict) and tool.get("type") == "function" and "function" in tool:
|
|
285
|
+
func_def = tool["function"]
|
|
286
|
+
converted_tool = {
|
|
287
|
+
"name": func_def.get("name", ""),
|
|
288
|
+
"description": func_def.get("description", ""),
|
|
289
|
+
"parameters": func_def.get("parameters", {}),
|
|
290
|
+
}
|
|
291
|
+
converted_tools.append(converted_tool)
|
|
292
|
+
# Handle already-converted Gemini format (idempotent)
|
|
293
|
+
elif isinstance(tool, dict) and "name" in tool and "parameters" in tool:
|
|
294
|
+
# Already in Gemini format, pass through
|
|
295
|
+
converted_tools.append(tool)
|
|
296
|
+
else:
|
|
297
|
+
# Skip unrecognized formats
|
|
298
|
+
logger.warning(f"[GeminiFormatter] Skipping unrecognized tool format: {tool}")
|
|
299
|
+
|
|
300
|
+
return converted_tools
|
|
301
|
+
|
|
302
|
+
def _convert_to_function_declarations(self, tools_dicts: List[Dict[str, Any]]) -> List[Any]:
|
|
303
|
+
"""
|
|
304
|
+
Convert Gemini-format tool dictionaries to FunctionDeclaration objects.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
tools_dicts: List of tool dictionaries in Gemini format
|
|
308
|
+
[{"name": ..., "description": ..., "parameters": {...}}]
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
List of google.genai.types.FunctionDeclaration objects
|
|
312
|
+
"""
|
|
313
|
+
if not tools_dicts:
|
|
314
|
+
return []
|
|
315
|
+
|
|
316
|
+
try:
|
|
317
|
+
from google.genai import types
|
|
318
|
+
except ImportError:
|
|
319
|
+
logger.error("[GeminiFormatter] Cannot import google.genai.types for FunctionDeclaration")
|
|
320
|
+
logger.error("[GeminiFormatter] Falling back to dictionary format")
|
|
321
|
+
return tools_dicts # Fallback to dict format
|
|
322
|
+
|
|
323
|
+
function_declarations = []
|
|
324
|
+
|
|
325
|
+
for tool_dict in tools_dicts:
|
|
326
|
+
try:
|
|
327
|
+
# Create Schema object for parameters
|
|
328
|
+
params = tool_dict.get("parameters", {})
|
|
329
|
+
|
|
330
|
+
# Convert parameters to Schema object (recursive)
|
|
331
|
+
schema = self._build_schema_recursive(params)
|
|
332
|
+
|
|
333
|
+
# Create FunctionDeclaration object
|
|
334
|
+
func_decl = types.FunctionDeclaration(
|
|
335
|
+
name=tool_dict.get("name", ""),
|
|
336
|
+
description=tool_dict.get("description", ""),
|
|
337
|
+
parameters=schema,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
function_declarations.append(func_decl)
|
|
341
|
+
|
|
342
|
+
logger.debug(
|
|
343
|
+
f"[GeminiFormatter] Converted tool '{tool_dict.get('name')}' " f"to FunctionDeclaration",
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
except Exception as e:
|
|
347
|
+
logger.error(
|
|
348
|
+
f"[GeminiFormatter] Failed to convert tool to FunctionDeclaration: {e}",
|
|
349
|
+
)
|
|
350
|
+
logger.error(f"[GeminiFormatter] Tool dict: {tool_dict}")
|
|
351
|
+
# Continue processing other tools instead of failing completely
|
|
352
|
+
continue
|
|
353
|
+
|
|
354
|
+
return function_declarations
|
|
355
|
+
|
|
356
|
+
def _build_schema_recursive(self, param_schema: Dict[str, Any]) -> Any:
|
|
357
|
+
"""
|
|
358
|
+
Recursively build a Gemini Schema object from JSON Schema format.
|
|
359
|
+
|
|
360
|
+
Handles nested objects, arrays, and all standard JSON Schema types.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
param_schema: JSON Schema dictionary (may be nested)
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
google.genai.types.Schema object
|
|
367
|
+
"""
|
|
368
|
+
try:
|
|
369
|
+
from google.genai import types
|
|
370
|
+
except ImportError:
|
|
371
|
+
logger.error("[GeminiFormatter] Cannot import google.genai.types")
|
|
372
|
+
return None
|
|
373
|
+
|
|
374
|
+
# Get the type (default to "object" for top-level parameters)
|
|
375
|
+
param_type = param_schema.get("type", "object")
|
|
376
|
+
gemini_type = self._convert_json_type_to_gemini_type(param_type)
|
|
377
|
+
|
|
378
|
+
# Build base schema kwargs
|
|
379
|
+
schema_kwargs = {
|
|
380
|
+
"type": gemini_type,
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
# Add description if present
|
|
384
|
+
if "description" in param_schema:
|
|
385
|
+
schema_kwargs["description"] = param_schema["description"]
|
|
386
|
+
|
|
387
|
+
# Handle object type with nested properties
|
|
388
|
+
if param_type == "object" and "properties" in param_schema:
|
|
389
|
+
schema_kwargs["properties"] = {prop_name: self._build_schema_recursive(prop_schema) for prop_name, prop_schema in param_schema["properties"].items()}
|
|
390
|
+
|
|
391
|
+
# Add required fields if present
|
|
392
|
+
if "required" in param_schema:
|
|
393
|
+
schema_kwargs["required"] = param_schema["required"]
|
|
394
|
+
|
|
395
|
+
# Handle array type with items
|
|
396
|
+
elif param_type == "array" and "items" in param_schema:
|
|
397
|
+
schema_kwargs["items"] = self._build_schema_recursive(param_schema["items"])
|
|
398
|
+
|
|
399
|
+
# Handle enum if present (for string/number types)
|
|
400
|
+
if "enum" in param_schema:
|
|
401
|
+
schema_kwargs["enum"] = param_schema["enum"]
|
|
402
|
+
|
|
403
|
+
# Handle format if present (e.g., "date-time", "email", etc.)
|
|
404
|
+
if "format" in param_schema:
|
|
405
|
+
schema_kwargs["format"] = param_schema["format"]
|
|
406
|
+
|
|
407
|
+
# Handle nullable if present
|
|
408
|
+
if "nullable" in param_schema:
|
|
409
|
+
schema_kwargs["nullable"] = param_schema["nullable"]
|
|
410
|
+
|
|
411
|
+
return types.Schema(**schema_kwargs)
|
|
412
|
+
|
|
413
|
+
def _convert_json_type_to_gemini_type(self, json_type: str) -> Any:
|
|
414
|
+
"""
|
|
415
|
+
Convert JSON Schema type string to Gemini Type enum.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
json_type: JSON Schema type like "string", "number", "integer", etc.
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
Corresponding google.genai.types.Type enum value
|
|
422
|
+
"""
|
|
423
|
+
try:
|
|
424
|
+
from google.genai import types
|
|
425
|
+
except ImportError:
|
|
426
|
+
# If we can't import, return string as fallback
|
|
427
|
+
# This shouldn't happen in practice since _build_schema_recursive checks first
|
|
428
|
+
return "STRING"
|
|
429
|
+
|
|
430
|
+
# Map JSON Schema types to Gemini Type enum
|
|
431
|
+
type_mapping = {
|
|
432
|
+
"string": types.Type.STRING,
|
|
433
|
+
"number": types.Type.NUMBER,
|
|
434
|
+
"integer": types.Type.INTEGER,
|
|
435
|
+
"boolean": types.Type.BOOLEAN,
|
|
436
|
+
"array": types.Type.ARRAY,
|
|
437
|
+
"object": types.Type.OBJECT,
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
# Return mapped type or default to STRING
|
|
441
|
+
gemini_type = type_mapping.get(json_type.lower(), types.Type.STRING)
|
|
442
|
+
|
|
443
|
+
if json_type.lower() not in type_mapping:
|
|
444
|
+
logger.warning(
|
|
445
|
+
f"[GeminiFormatter] Unknown JSON type '{json_type}', defaulting to STRING",
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
return gemini_type
|