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
|
@@ -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
|
|
@@ -241,6 +241,94 @@ class ResponseFormatter(FormatterBase):
|
|
|
241
241
|
|
|
242
242
|
return converted_tools
|
|
243
243
|
|
|
244
|
+
def format_custom_tools(self, custom_tools: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
245
|
+
"""
|
|
246
|
+
Convert custom tools from RegisteredToolEntry format to Response API format.
|
|
247
|
+
|
|
248
|
+
Custom tools are provided as a dictionary where:
|
|
249
|
+
- Keys are tool names (str)
|
|
250
|
+
- Values are RegisteredToolEntry objects with:
|
|
251
|
+
- tool_name: str
|
|
252
|
+
- schema_def: dict with structure {"type": "function", "function": {...}}
|
|
253
|
+
- get_extended_schema: property that returns the schema with extensions
|
|
254
|
+
|
|
255
|
+
Response API expects: {"type": "function", "name": ..., "description": ..., "parameters": ...}
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
custom_tools: Dictionary of tool_name -> RegisteredToolEntry objects
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
List of tools in Response API format
|
|
262
|
+
"""
|
|
263
|
+
if not custom_tools:
|
|
264
|
+
return []
|
|
265
|
+
|
|
266
|
+
converted_tools = []
|
|
267
|
+
|
|
268
|
+
# Handle dictionary format: {tool_name: RegisteredToolEntry, ...}
|
|
269
|
+
if isinstance(custom_tools, dict):
|
|
270
|
+
for tool_name, tool_entry in custom_tools.items():
|
|
271
|
+
# Check if it's a RegisteredToolEntry object with schema_def
|
|
272
|
+
if hasattr(tool_entry, "schema_def"):
|
|
273
|
+
tool_schema = tool_entry.schema_def
|
|
274
|
+
|
|
275
|
+
# Extract function details from Chat Completions format
|
|
276
|
+
if tool_schema.get("type") == "function" and "function" in tool_schema:
|
|
277
|
+
func = tool_schema["function"]
|
|
278
|
+
converted_tools.append(
|
|
279
|
+
{
|
|
280
|
+
"type": "function",
|
|
281
|
+
"name": func.get("name", tool_entry.tool_name if hasattr(tool_entry, "tool_name") else tool_name),
|
|
282
|
+
"description": func.get("description", ""),
|
|
283
|
+
"parameters": func.get("parameters", {}),
|
|
284
|
+
},
|
|
285
|
+
)
|
|
286
|
+
# Check if it has get_extended_schema property
|
|
287
|
+
elif hasattr(tool_entry, "get_extended_schema"):
|
|
288
|
+
tool_schema = tool_entry.get_extended_schema
|
|
289
|
+
|
|
290
|
+
if tool_schema.get("type") == "function" and "function" in tool_schema:
|
|
291
|
+
func = tool_schema["function"]
|
|
292
|
+
converted_tools.append(
|
|
293
|
+
{
|
|
294
|
+
"type": "function",
|
|
295
|
+
"name": func.get("name", tool_entry.tool_name if hasattr(tool_entry, "tool_name") else tool_name),
|
|
296
|
+
"description": func.get("description", ""),
|
|
297
|
+
"parameters": func.get("parameters", {}),
|
|
298
|
+
},
|
|
299
|
+
)
|
|
300
|
+
# Handle list format for backward compatibility
|
|
301
|
+
elif isinstance(custom_tools, list):
|
|
302
|
+
for tool in custom_tools:
|
|
303
|
+
if hasattr(tool, "schema_def"):
|
|
304
|
+
tool_schema = tool.schema_def
|
|
305
|
+
|
|
306
|
+
if tool_schema.get("type") == "function" and "function" in tool_schema:
|
|
307
|
+
func = tool_schema["function"]
|
|
308
|
+
converted_tools.append(
|
|
309
|
+
{
|
|
310
|
+
"type": "function",
|
|
311
|
+
"name": func.get("name", tool.tool_name),
|
|
312
|
+
"description": func.get("description", ""),
|
|
313
|
+
"parameters": func.get("parameters", {}),
|
|
314
|
+
},
|
|
315
|
+
)
|
|
316
|
+
elif hasattr(tool, "get_extended_schema"):
|
|
317
|
+
tool_schema = tool.get_extended_schema
|
|
318
|
+
|
|
319
|
+
if tool_schema.get("type") == "function" and "function" in tool_schema:
|
|
320
|
+
func = tool_schema["function"]
|
|
321
|
+
converted_tools.append(
|
|
322
|
+
{
|
|
323
|
+
"type": "function",
|
|
324
|
+
"name": func.get("name", tool.tool_name),
|
|
325
|
+
"description": func.get("description", ""),
|
|
326
|
+
"parameters": func.get("parameters", {}),
|
|
327
|
+
},
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
return converted_tools
|
|
331
|
+
|
|
244
332
|
def format_mcp_tools(self, mcp_functions: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
245
333
|
"""Convert MCP tools to Response API format (OpenAI function declarations)."""
|
|
246
334
|
if not mcp_functions:
|
|
@@ -315,7 +315,8 @@ class CoordinationUI:
|
|
|
315
315
|
# time.sleep(1.0)
|
|
316
316
|
|
|
317
317
|
# Get final presentation from winning agent
|
|
318
|
-
if
|
|
318
|
+
# Run final presentation if enabled and there's a selected agent (regardless of votes)
|
|
319
|
+
if self.enable_final_presentation and selected_agent:
|
|
319
320
|
# Don't print - let the display handle it
|
|
320
321
|
# print(f"\nš¤ Final Presentation from {selected_agent}:")
|
|
321
322
|
# print("=" * 60)
|
|
@@ -691,7 +692,8 @@ class CoordinationUI:
|
|
|
691
692
|
# time.sleep(1.0)
|
|
692
693
|
|
|
693
694
|
# Get final presentation from winning agent
|
|
694
|
-
if
|
|
695
|
+
# Run final presentation if enabled and there's a selected agent (regardless of votes)
|
|
696
|
+
if self.enable_final_presentation and selected_agent:
|
|
695
697
|
# Don't print - let the display handle it
|
|
696
698
|
# print(f"\nš¤ Final Presentation from {selected_agent}:")
|
|
697
699
|
# print("=" * 60)
|
massgen/logger_config.py
CHANGED
|
@@ -16,13 +16,21 @@ Color Scheme for Debug Logging:
|
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
18
|
import inspect
|
|
19
|
+
import subprocess
|
|
19
20
|
import sys
|
|
20
21
|
from datetime import datetime
|
|
21
22
|
from pathlib import Path
|
|
22
23
|
from typing import Any, Optional
|
|
23
24
|
|
|
25
|
+
import yaml
|
|
24
26
|
from loguru import logger
|
|
25
27
|
|
|
28
|
+
# Try to import massgen for version info (optional)
|
|
29
|
+
try:
|
|
30
|
+
import massgen
|
|
31
|
+
except ImportError:
|
|
32
|
+
massgen = None
|
|
33
|
+
|
|
26
34
|
# Remove default logger to have full control
|
|
27
35
|
logger.remove()
|
|
28
36
|
|
|
@@ -93,7 +101,12 @@ def get_log_session_dir(turn: Optional[int] = None) -> Path:
|
|
|
93
101
|
return _LOG_SESSION_DIR
|
|
94
102
|
|
|
95
103
|
|
|
96
|
-
def save_execution_metadata(
|
|
104
|
+
def save_execution_metadata(
|
|
105
|
+
query: str,
|
|
106
|
+
config_path: Optional[str] = None,
|
|
107
|
+
config_content: Optional[dict] = None,
|
|
108
|
+
cli_args: Optional[dict] = None,
|
|
109
|
+
):
|
|
97
110
|
"""Save the query and config metadata to the log directory.
|
|
98
111
|
|
|
99
112
|
This allows reconstructing what was executed in this session.
|
|
@@ -102,9 +115,8 @@ def save_execution_metadata(query: str, config_path: Optional[str] = None, confi
|
|
|
102
115
|
query: The user's query/prompt
|
|
103
116
|
config_path: Path to the config file that was used (optional)
|
|
104
117
|
config_content: The actual config dictionary (optional)
|
|
118
|
+
cli_args: Command line arguments as dict (optional)
|
|
105
119
|
"""
|
|
106
|
-
import yaml
|
|
107
|
-
|
|
108
120
|
log_dir = get_log_session_dir()
|
|
109
121
|
|
|
110
122
|
# Create a single metadata file with all execution info
|
|
@@ -119,6 +131,26 @@ def save_execution_metadata(query: str, config_path: Optional[str] = None, confi
|
|
|
119
131
|
if config_content:
|
|
120
132
|
metadata["config"] = config_content
|
|
121
133
|
|
|
134
|
+
if cli_args:
|
|
135
|
+
metadata["cli_args"] = cli_args
|
|
136
|
+
|
|
137
|
+
# Try to get git information if in a git repository
|
|
138
|
+
try:
|
|
139
|
+
git_commit = subprocess.check_output(["git", "rev-parse", "HEAD"], stderr=subprocess.DEVNULL, text=True).strip()
|
|
140
|
+
git_branch = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"], stderr=subprocess.DEVNULL, text=True).strip()
|
|
141
|
+
metadata["git"] = {"commit": git_commit, "branch": git_branch}
|
|
142
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
143
|
+
# Not in a git repo or git not available
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
# Add Python version and package version
|
|
147
|
+
metadata["python_version"] = sys.version
|
|
148
|
+
if massgen is not None:
|
|
149
|
+
metadata["massgen_version"] = getattr(massgen, "__version__", "unknown")
|
|
150
|
+
|
|
151
|
+
# Add working directory
|
|
152
|
+
metadata["working_directory"] = str(Path.cwd())
|
|
153
|
+
|
|
122
154
|
metadata_file = log_dir / "execution_metadata.yaml"
|
|
123
155
|
try:
|
|
124
156
|
with open(metadata_file, "w", encoding="utf-8") as f:
|