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.

Files changed (120) hide show
  1. massgen/__init__.py +1 -1
  2. massgen/agent_config.py +17 -0
  3. massgen/api_params_handler/_api_params_handler_base.py +1 -0
  4. massgen/api_params_handler/_chat_completions_api_params_handler.py +15 -2
  5. massgen/api_params_handler/_claude_api_params_handler.py +8 -1
  6. massgen/api_params_handler/_gemini_api_params_handler.py +73 -0
  7. massgen/api_params_handler/_response_api_params_handler.py +8 -1
  8. massgen/backend/base.py +83 -0
  9. massgen/backend/{base_with_mcp.py → base_with_custom_tool_and_mcp.py} +286 -15
  10. massgen/backend/capabilities.py +6 -6
  11. massgen/backend/chat_completions.py +200 -103
  12. massgen/backend/claude.py +115 -18
  13. massgen/backend/claude_code.py +378 -14
  14. massgen/backend/docs/CLAUDE_API_RESEARCH.md +3 -3
  15. massgen/backend/gemini.py +1333 -1629
  16. massgen/backend/gemini_mcp_manager.py +545 -0
  17. massgen/backend/gemini_trackers.py +344 -0
  18. massgen/backend/gemini_utils.py +43 -0
  19. massgen/backend/grok.py +39 -6
  20. massgen/backend/response.py +147 -81
  21. massgen/cli.py +605 -110
  22. massgen/config_builder.py +376 -27
  23. massgen/configs/README.md +123 -80
  24. massgen/configs/basic/multi/three_agents_default.yaml +3 -3
  25. massgen/configs/basic/single/single_agent.yaml +1 -1
  26. massgen/configs/providers/openai/gpt5_nano.yaml +3 -3
  27. massgen/configs/tools/custom_tools/claude_code_custom_tool_example.yaml +32 -0
  28. massgen/configs/tools/custom_tools/claude_code_custom_tool_example_no_path.yaml +28 -0
  29. massgen/configs/tools/custom_tools/claude_code_custom_tool_with_mcp_example.yaml +40 -0
  30. massgen/configs/tools/custom_tools/claude_code_custom_tool_with_wrong_mcp_example.yaml +38 -0
  31. massgen/configs/tools/custom_tools/claude_code_wrong_custom_tool_with_mcp_example.yaml +38 -0
  32. massgen/configs/tools/custom_tools/claude_custom_tool_example.yaml +24 -0
  33. massgen/configs/tools/custom_tools/claude_custom_tool_example_no_path.yaml +22 -0
  34. massgen/configs/tools/custom_tools/claude_custom_tool_with_mcp_example.yaml +35 -0
  35. massgen/configs/tools/custom_tools/claude_custom_tool_with_wrong_mcp_example.yaml +33 -0
  36. massgen/configs/tools/custom_tools/claude_wrong_custom_tool_with_mcp_example.yaml +33 -0
  37. massgen/configs/tools/custom_tools/gemini_custom_tool_example.yaml +24 -0
  38. massgen/configs/tools/custom_tools/gemini_custom_tool_example_no_path.yaml +22 -0
  39. massgen/configs/tools/custom_tools/gemini_custom_tool_with_mcp_example.yaml +35 -0
  40. massgen/configs/tools/custom_tools/gemini_custom_tool_with_wrong_mcp_example.yaml +33 -0
  41. massgen/configs/tools/custom_tools/gemini_wrong_custom_tool_with_mcp_example.yaml +33 -0
  42. massgen/configs/tools/custom_tools/github_issue_market_analysis.yaml +94 -0
  43. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_example.yaml +24 -0
  44. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_example_no_path.yaml +22 -0
  45. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_with_mcp_example.yaml +35 -0
  46. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_with_wrong_mcp_example.yaml +33 -0
  47. massgen/configs/tools/custom_tools/gpt5_nano_wrong_custom_tool_with_mcp_example.yaml +33 -0
  48. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_example.yaml +25 -0
  49. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_example_no_path.yaml +23 -0
  50. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_with_mcp_example.yaml +34 -0
  51. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_with_wrong_mcp_example.yaml +34 -0
  52. massgen/configs/tools/custom_tools/gpt_oss_wrong_custom_tool_with_mcp_example.yaml +34 -0
  53. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_example.yaml +24 -0
  54. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_example_no_path.yaml +22 -0
  55. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_with_mcp_example.yaml +35 -0
  56. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_with_wrong_mcp_example.yaml +33 -0
  57. massgen/configs/tools/custom_tools/grok3_mini_wrong_custom_tool_with_mcp_example.yaml +33 -0
  58. massgen/configs/tools/custom_tools/qwen_api_custom_tool_example.yaml +25 -0
  59. massgen/configs/tools/custom_tools/qwen_api_custom_tool_example_no_path.yaml +23 -0
  60. massgen/configs/tools/custom_tools/qwen_api_custom_tool_with_mcp_example.yaml +36 -0
  61. massgen/configs/tools/custom_tools/qwen_api_custom_tool_with_wrong_mcp_example.yaml +34 -0
  62. massgen/configs/tools/custom_tools/qwen_api_wrong_custom_tool_with_mcp_example.yaml +34 -0
  63. massgen/configs/tools/custom_tools/qwen_local_custom_tool_example.yaml +24 -0
  64. massgen/configs/tools/custom_tools/qwen_local_custom_tool_example_no_path.yaml +22 -0
  65. massgen/configs/tools/custom_tools/qwen_local_custom_tool_with_mcp_example.yaml +35 -0
  66. massgen/configs/tools/custom_tools/qwen_local_custom_tool_with_wrong_mcp_example.yaml +33 -0
  67. massgen/configs/tools/custom_tools/qwen_local_wrong_custom_tool_with_mcp_example.yaml +33 -0
  68. massgen/configs/tools/filesystem/claude_code_context_sharing.yaml +1 -1
  69. massgen/configs/tools/planning/five_agents_discord_mcp_planning_mode.yaml +7 -29
  70. massgen/configs/tools/planning/five_agents_filesystem_mcp_planning_mode.yaml +5 -6
  71. massgen/configs/tools/planning/five_agents_notion_mcp_planning_mode.yaml +4 -4
  72. massgen/configs/tools/planning/five_agents_twitter_mcp_planning_mode.yaml +4 -4
  73. massgen/configs/tools/planning/gpt5_mini_case_study_mcp_planning_mode.yaml +2 -2
  74. massgen/configs/voting/gemini_gpt_voting_sensitivity.yaml +67 -0
  75. massgen/formatter/_chat_completions_formatter.py +104 -0
  76. massgen/formatter/_claude_formatter.py +120 -0
  77. massgen/formatter/_gemini_formatter.py +448 -0
  78. massgen/formatter/_response_formatter.py +88 -0
  79. massgen/frontend/coordination_ui.py +4 -2
  80. massgen/logger_config.py +35 -3
  81. massgen/message_templates.py +56 -6
  82. massgen/orchestrator.py +512 -16
  83. massgen/stream_chunk/base.py +3 -0
  84. massgen/tests/custom_tools_example.py +392 -0
  85. massgen/tests/mcp_test_server.py +17 -7
  86. massgen/tests/test_config_builder.py +423 -0
  87. massgen/tests/test_custom_tools.py +401 -0
  88. massgen/tests/test_intelligent_planning_mode.py +643 -0
  89. massgen/tests/test_tools.py +127 -0
  90. massgen/token_manager/token_manager.py +13 -4
  91. massgen/tool/README.md +935 -0
  92. massgen/tool/__init__.py +39 -0
  93. massgen/tool/_async_helpers.py +70 -0
  94. massgen/tool/_basic/__init__.py +8 -0
  95. massgen/tool/_basic/_two_num_tool.py +24 -0
  96. massgen/tool/_code_executors/__init__.py +10 -0
  97. massgen/tool/_code_executors/_python_executor.py +74 -0
  98. massgen/tool/_code_executors/_shell_executor.py +61 -0
  99. massgen/tool/_exceptions.py +39 -0
  100. massgen/tool/_file_handlers/__init__.py +10 -0
  101. massgen/tool/_file_handlers/_file_operations.py +218 -0
  102. massgen/tool/_manager.py +634 -0
  103. massgen/tool/_registered_tool.py +88 -0
  104. massgen/tool/_result.py +66 -0
  105. massgen/tool/_self_evolution/_github_issue_analyzer.py +369 -0
  106. massgen/tool/docs/builtin_tools.md +681 -0
  107. massgen/tool/docs/exceptions.md +794 -0
  108. massgen/tool/docs/execution_results.md +691 -0
  109. massgen/tool/docs/manager.md +887 -0
  110. massgen/tool/docs/workflow_toolkits.md +529 -0
  111. massgen/tool/workflow_toolkits/__init__.py +57 -0
  112. massgen/tool/workflow_toolkits/base.py +55 -0
  113. massgen/tool/workflow_toolkits/new_answer.py +126 -0
  114. massgen/tool/workflow_toolkits/vote.py +167 -0
  115. {massgen-0.1.0a3.dist-info → massgen-0.1.2.dist-info}/METADATA +87 -129
  116. {massgen-0.1.0a3.dist-info → massgen-0.1.2.dist-info}/RECORD +120 -44
  117. {massgen-0.1.0a3.dist-info → massgen-0.1.2.dist-info}/WHEEL +0 -0
  118. {massgen-0.1.0a3.dist-info → massgen-0.1.2.dist-info}/entry_points.txt +0 -0
  119. {massgen-0.1.0a3.dist-info → massgen-0.1.2.dist-info}/licenses/LICENSE +0 -0
  120. {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 self.enable_final_presentation and selected_agent and vote_results.get("vote_counts"):
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 self.enable_final_presentation and selected_agent and vote_results.get("vote_counts"):
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(query: str, config_path: Optional[str] = None, config_content: Optional[dict] = None):
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: