massgen 0.0.3__py3-none-any.whl → 0.1.0__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 +142 -8
- massgen/adapters/__init__.py +29 -0
- massgen/adapters/ag2_adapter.py +483 -0
- massgen/adapters/base.py +183 -0
- massgen/adapters/tests/__init__.py +0 -0
- massgen/adapters/tests/test_ag2_adapter.py +439 -0
- massgen/adapters/tests/test_agent_adapter.py +128 -0
- massgen/adapters/utils/__init__.py +2 -0
- massgen/adapters/utils/ag2_utils.py +236 -0
- massgen/adapters/utils/tests/__init__.py +0 -0
- massgen/adapters/utils/tests/test_ag2_utils.py +138 -0
- massgen/agent_config.py +329 -55
- massgen/api_params_handler/__init__.py +10 -0
- massgen/api_params_handler/_api_params_handler_base.py +99 -0
- massgen/api_params_handler/_chat_completions_api_params_handler.py +176 -0
- massgen/api_params_handler/_claude_api_params_handler.py +113 -0
- massgen/api_params_handler/_response_api_params_handler.py +130 -0
- massgen/backend/__init__.py +39 -4
- massgen/backend/azure_openai.py +385 -0
- massgen/backend/base.py +341 -69
- massgen/backend/base_with_mcp.py +1102 -0
- massgen/backend/capabilities.py +386 -0
- massgen/backend/chat_completions.py +577 -130
- massgen/backend/claude.py +1033 -537
- massgen/backend/claude_code.py +1203 -0
- massgen/backend/cli_base.py +209 -0
- massgen/backend/docs/BACKEND_ARCHITECTURE.md +126 -0
- massgen/backend/{CLAUDE_API_RESEARCH.md → docs/CLAUDE_API_RESEARCH.md} +18 -18
- massgen/backend/{GEMINI_API_DOCUMENTATION.md → docs/GEMINI_API_DOCUMENTATION.md} +9 -9
- massgen/backend/docs/Gemini MCP Integration Analysis.md +1050 -0
- massgen/backend/docs/MCP_IMPLEMENTATION_CLAUDE_BACKEND.md +177 -0
- massgen/backend/docs/MCP_INTEGRATION_RESPONSE_BACKEND.md +352 -0
- massgen/backend/docs/OPENAI_GPT5_MODELS.md +211 -0
- massgen/backend/{OPENAI_RESPONSES_API_FORMAT.md → docs/OPENAI_RESPONSE_API_TOOL_CALLS.md} +3 -3
- massgen/backend/docs/OPENAI_response_streaming.md +20654 -0
- massgen/backend/docs/inference_backend.md +257 -0
- massgen/backend/docs/permissions_and_context_files.md +1085 -0
- massgen/backend/external.py +126 -0
- massgen/backend/gemini.py +1850 -241
- massgen/backend/grok.py +40 -156
- massgen/backend/inference.py +156 -0
- massgen/backend/lmstudio.py +171 -0
- massgen/backend/response.py +1095 -322
- massgen/chat_agent.py +131 -113
- massgen/cli.py +1560 -275
- massgen/config_builder.py +2396 -0
- massgen/configs/BACKEND_CONFIGURATION.md +458 -0
- massgen/configs/README.md +559 -216
- massgen/configs/ag2/ag2_case_study.yaml +27 -0
- massgen/configs/ag2/ag2_coder.yaml +34 -0
- massgen/configs/ag2/ag2_coder_case_study.yaml +36 -0
- massgen/configs/ag2/ag2_gemini.yaml +27 -0
- massgen/configs/ag2/ag2_groupchat.yaml +108 -0
- massgen/configs/ag2/ag2_groupchat_gpt.yaml +118 -0
- massgen/configs/ag2/ag2_single_agent.yaml +21 -0
- massgen/configs/basic/multi/fast_timeout_example.yaml +37 -0
- massgen/configs/basic/multi/gemini_4o_claude.yaml +31 -0
- massgen/configs/basic/multi/gemini_gpt5nano_claude.yaml +36 -0
- massgen/configs/{gemini_4o_claude.yaml → basic/multi/geminicode_4o_claude.yaml} +3 -3
- massgen/configs/basic/multi/geminicode_gpt5nano_claude.yaml +36 -0
- massgen/configs/basic/multi/glm_gemini_claude.yaml +25 -0
- massgen/configs/basic/multi/gpt4o_audio_generation.yaml +30 -0
- massgen/configs/basic/multi/gpt4o_image_generation.yaml +31 -0
- massgen/configs/basic/multi/gpt5nano_glm_qwen.yaml +26 -0
- massgen/configs/basic/multi/gpt5nano_image_understanding.yaml +26 -0
- massgen/configs/{three_agents_default.yaml → basic/multi/three_agents_default.yaml} +8 -4
- massgen/configs/basic/multi/three_agents_opensource.yaml +27 -0
- massgen/configs/basic/multi/three_agents_vllm.yaml +20 -0
- massgen/configs/basic/multi/two_agents_gemini.yaml +19 -0
- massgen/configs/{two_agents.yaml → basic/multi/two_agents_gpt5.yaml} +14 -6
- massgen/configs/basic/multi/two_agents_opensource_lmstudio.yaml +31 -0
- massgen/configs/basic/multi/two_qwen_vllm_sglang.yaml +28 -0
- massgen/configs/{single_agent.yaml → basic/single/single_agent.yaml} +1 -1
- massgen/configs/{single_flash2.5.yaml → basic/single/single_flash2.5.yaml} +1 -2
- massgen/configs/basic/single/single_gemini2.5pro.yaml +16 -0
- massgen/configs/basic/single/single_gpt4o_audio_generation.yaml +22 -0
- massgen/configs/basic/single/single_gpt4o_image_generation.yaml +22 -0
- massgen/configs/basic/single/single_gpt4o_video_generation.yaml +24 -0
- massgen/configs/basic/single/single_gpt5nano.yaml +20 -0
- massgen/configs/basic/single/single_gpt5nano_file_search.yaml +18 -0
- massgen/configs/basic/single/single_gpt5nano_image_understanding.yaml +17 -0
- massgen/configs/basic/single/single_gptoss120b.yaml +15 -0
- massgen/configs/basic/single/single_openrouter_audio_understanding.yaml +15 -0
- massgen/configs/basic/single/single_qwen_video_understanding.yaml +15 -0
- massgen/configs/debug/code_execution/command_filtering_blacklist.yaml +29 -0
- massgen/configs/debug/code_execution/command_filtering_whitelist.yaml +28 -0
- massgen/configs/debug/code_execution/docker_verification.yaml +29 -0
- massgen/configs/debug/skip_coordination_test.yaml +27 -0
- massgen/configs/debug/test_sdk_migration.yaml +17 -0
- massgen/configs/docs/DISCORD_MCP_SETUP.md +208 -0
- massgen/configs/docs/TWITTER_MCP_ENESCINAR_SETUP.md +82 -0
- massgen/configs/providers/azure/azure_openai_multi.yaml +21 -0
- massgen/configs/providers/azure/azure_openai_single.yaml +19 -0
- massgen/configs/providers/claude/claude.yaml +14 -0
- massgen/configs/providers/gemini/gemini_gpt5nano.yaml +28 -0
- massgen/configs/providers/local/lmstudio.yaml +11 -0
- massgen/configs/providers/openai/gpt5.yaml +46 -0
- massgen/configs/providers/openai/gpt5_nano.yaml +46 -0
- massgen/configs/providers/others/grok_single_agent.yaml +19 -0
- massgen/configs/providers/others/zai_coding_team.yaml +108 -0
- massgen/configs/providers/others/zai_glm45.yaml +12 -0
- massgen/configs/{creative_team.yaml → teams/creative/creative_team.yaml} +16 -6
- massgen/configs/{travel_planning.yaml → teams/creative/travel_planning.yaml} +16 -6
- massgen/configs/{news_analysis.yaml → teams/research/news_analysis.yaml} +16 -6
- massgen/configs/{research_team.yaml → teams/research/research_team.yaml} +15 -7
- massgen/configs/{technical_analysis.yaml → teams/research/technical_analysis.yaml} +16 -6
- massgen/configs/tools/code-execution/basic_command_execution.yaml +25 -0
- massgen/configs/tools/code-execution/code_execution_use_case_simple.yaml +41 -0
- massgen/configs/tools/code-execution/docker_claude_code.yaml +32 -0
- massgen/configs/tools/code-execution/docker_multi_agent.yaml +32 -0
- massgen/configs/tools/code-execution/docker_simple.yaml +29 -0
- massgen/configs/tools/code-execution/docker_with_resource_limits.yaml +32 -0
- massgen/configs/tools/code-execution/multi_agent_playwright_automation.yaml +57 -0
- massgen/configs/tools/filesystem/cc_gpt5_gemini_filesystem.yaml +34 -0
- massgen/configs/tools/filesystem/claude_code_context_sharing.yaml +68 -0
- massgen/configs/tools/filesystem/claude_code_flash2.5.yaml +43 -0
- massgen/configs/tools/filesystem/claude_code_flash2.5_gptoss.yaml +49 -0
- massgen/configs/tools/filesystem/claude_code_gpt5nano.yaml +31 -0
- massgen/configs/tools/filesystem/claude_code_single.yaml +40 -0
- massgen/configs/tools/filesystem/fs_permissions_test.yaml +87 -0
- massgen/configs/tools/filesystem/gemini_gemini_workspace_cleanup.yaml +54 -0
- massgen/configs/tools/filesystem/gemini_gpt5_filesystem_casestudy.yaml +30 -0
- massgen/configs/tools/filesystem/gemini_gpt5nano_file_context_path.yaml +43 -0
- massgen/configs/tools/filesystem/gemini_gpt5nano_protected_paths.yaml +45 -0
- massgen/configs/tools/filesystem/gpt5mini_cc_fs_context_path.yaml +31 -0
- massgen/configs/tools/filesystem/grok4_gpt5_gemini_filesystem.yaml +32 -0
- massgen/configs/tools/filesystem/multiturn/grok4_gpt5_claude_code_filesystem_multiturn.yaml +58 -0
- massgen/configs/tools/filesystem/multiturn/grok4_gpt5_gemini_filesystem_multiturn.yaml +58 -0
- massgen/configs/tools/filesystem/multiturn/two_claude_code_filesystem_multiturn.yaml +47 -0
- massgen/configs/tools/filesystem/multiturn/two_gemini_flash_filesystem_multiturn.yaml +48 -0
- massgen/configs/tools/mcp/claude_code_discord_mcp_example.yaml +27 -0
- massgen/configs/tools/mcp/claude_code_simple_mcp.yaml +35 -0
- massgen/configs/tools/mcp/claude_code_twitter_mcp_example.yaml +32 -0
- massgen/configs/tools/mcp/claude_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/claude_mcp_test.yaml +27 -0
- massgen/configs/tools/mcp/five_agents_travel_mcp_test.yaml +157 -0
- massgen/configs/tools/mcp/five_agents_weather_mcp_test.yaml +103 -0
- massgen/configs/tools/mcp/gemini_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/gemini_mcp_filesystem_test.yaml +23 -0
- massgen/configs/tools/mcp/gemini_mcp_filesystem_test_sharing.yaml +23 -0
- massgen/configs/tools/mcp/gemini_mcp_filesystem_test_single_agent.yaml +17 -0
- massgen/configs/tools/mcp/gemini_mcp_filesystem_test_with_claude_code.yaml +24 -0
- massgen/configs/tools/mcp/gemini_mcp_test.yaml +27 -0
- massgen/configs/tools/mcp/gemini_notion_mcp.yaml +52 -0
- massgen/configs/tools/mcp/gpt5_nano_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/gpt5_nano_mcp_test.yaml +27 -0
- massgen/configs/tools/mcp/gpt5mini_claude_code_discord_mcp_example.yaml +38 -0
- massgen/configs/tools/mcp/gpt_oss_mcp_example.yaml +25 -0
- massgen/configs/tools/mcp/gpt_oss_mcp_test.yaml +28 -0
- massgen/configs/tools/mcp/grok3_mini_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/grok3_mini_mcp_test.yaml +27 -0
- massgen/configs/tools/mcp/multimcp_gemini.yaml +111 -0
- massgen/configs/tools/mcp/qwen_api_mcp_example.yaml +25 -0
- massgen/configs/tools/mcp/qwen_api_mcp_test.yaml +28 -0
- massgen/configs/tools/mcp/qwen_local_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/qwen_local_mcp_test.yaml +27 -0
- massgen/configs/tools/planning/five_agents_discord_mcp_planning_mode.yaml +140 -0
- massgen/configs/tools/planning/five_agents_filesystem_mcp_planning_mode.yaml +151 -0
- massgen/configs/tools/planning/five_agents_notion_mcp_planning_mode.yaml +151 -0
- massgen/configs/tools/planning/five_agents_twitter_mcp_planning_mode.yaml +155 -0
- massgen/configs/tools/planning/gpt5_mini_case_study_mcp_planning_mode.yaml +73 -0
- massgen/configs/tools/web-search/claude_streamable_http_test.yaml +43 -0
- massgen/configs/tools/web-search/gemini_streamable_http_test.yaml +43 -0
- massgen/configs/tools/web-search/gpt5_mini_streamable_http_test.yaml +43 -0
- massgen/configs/tools/web-search/gpt_oss_streamable_http_test.yaml +44 -0
- massgen/configs/tools/web-search/grok3_mini_streamable_http_test.yaml +43 -0
- massgen/configs/tools/web-search/qwen_api_streamable_http_test.yaml +44 -0
- massgen/configs/tools/web-search/qwen_local_streamable_http_test.yaml +43 -0
- massgen/coordination_tracker.py +708 -0
- massgen/docker/README.md +462 -0
- massgen/filesystem_manager/__init__.py +21 -0
- massgen/filesystem_manager/_base.py +9 -0
- massgen/filesystem_manager/_code_execution_server.py +545 -0
- massgen/filesystem_manager/_docker_manager.py +477 -0
- massgen/filesystem_manager/_file_operation_tracker.py +248 -0
- massgen/filesystem_manager/_filesystem_manager.py +813 -0
- massgen/filesystem_manager/_path_permission_manager.py +1261 -0
- massgen/filesystem_manager/_workspace_tools_server.py +1815 -0
- massgen/formatter/__init__.py +10 -0
- massgen/formatter/_chat_completions_formatter.py +284 -0
- massgen/formatter/_claude_formatter.py +235 -0
- massgen/formatter/_formatter_base.py +156 -0
- massgen/formatter/_response_formatter.py +263 -0
- massgen/frontend/__init__.py +1 -2
- massgen/frontend/coordination_ui.py +471 -286
- massgen/frontend/displays/base_display.py +56 -11
- massgen/frontend/displays/create_coordination_table.py +1956 -0
- massgen/frontend/displays/rich_terminal_display.py +1259 -619
- massgen/frontend/displays/simple_display.py +9 -4
- massgen/frontend/displays/terminal_display.py +27 -68
- massgen/logger_config.py +681 -0
- massgen/mcp_tools/README.md +232 -0
- massgen/mcp_tools/__init__.py +105 -0
- massgen/mcp_tools/backend_utils.py +1035 -0
- massgen/mcp_tools/circuit_breaker.py +195 -0
- massgen/mcp_tools/client.py +894 -0
- massgen/mcp_tools/config_validator.py +138 -0
- massgen/mcp_tools/docs/circuit_breaker.md +646 -0
- massgen/mcp_tools/docs/client.md +950 -0
- massgen/mcp_tools/docs/config_validator.md +478 -0
- massgen/mcp_tools/docs/exceptions.md +1165 -0
- massgen/mcp_tools/docs/security.md +854 -0
- massgen/mcp_tools/exceptions.py +338 -0
- massgen/mcp_tools/hooks.py +212 -0
- massgen/mcp_tools/security.py +780 -0
- massgen/message_templates.py +342 -64
- massgen/orchestrator.py +1515 -241
- massgen/stream_chunk/__init__.py +35 -0
- massgen/stream_chunk/base.py +92 -0
- massgen/stream_chunk/multimodal.py +237 -0
- massgen/stream_chunk/text.py +162 -0
- massgen/tests/mcp_test_server.py +150 -0
- massgen/tests/multi_turn_conversation_design.md +0 -8
- massgen/tests/test_azure_openai_backend.py +156 -0
- massgen/tests/test_backend_capabilities.py +262 -0
- massgen/tests/test_backend_event_loop_all.py +179 -0
- massgen/tests/test_chat_completions_refactor.py +142 -0
- massgen/tests/test_claude_backend.py +15 -28
- massgen/tests/test_claude_code.py +268 -0
- massgen/tests/test_claude_code_context_sharing.py +233 -0
- massgen/tests/test_claude_code_orchestrator.py +175 -0
- massgen/tests/test_cli_backends.py +180 -0
- massgen/tests/test_code_execution.py +679 -0
- massgen/tests/test_external_agent_backend.py +134 -0
- massgen/tests/test_final_presentation_fallback.py +237 -0
- massgen/tests/test_gemini_planning_mode.py +351 -0
- massgen/tests/test_grok_backend.py +7 -10
- massgen/tests/test_http_mcp_server.py +42 -0
- massgen/tests/test_integration_simple.py +198 -0
- massgen/tests/test_mcp_blocking.py +125 -0
- massgen/tests/test_message_context_building.py +29 -47
- massgen/tests/test_orchestrator_final_presentation.py +48 -0
- massgen/tests/test_path_permission_manager.py +2087 -0
- massgen/tests/test_rich_terminal_display.py +14 -13
- massgen/tests/test_timeout.py +133 -0
- massgen/tests/test_v3_3agents.py +11 -12
- massgen/tests/test_v3_simple.py +8 -13
- massgen/tests/test_v3_three_agents.py +11 -18
- massgen/tests/test_v3_two_agents.py +8 -13
- massgen/token_manager/__init__.py +7 -0
- massgen/token_manager/token_manager.py +400 -0
- massgen/utils.py +52 -16
- massgen/v1/agent.py +45 -91
- massgen/v1/agents.py +18 -53
- massgen/v1/backends/gemini.py +50 -153
- massgen/v1/backends/grok.py +21 -54
- massgen/v1/backends/oai.py +39 -111
- massgen/v1/cli.py +36 -93
- massgen/v1/config.py +8 -12
- massgen/v1/logging.py +43 -127
- massgen/v1/main.py +18 -32
- massgen/v1/orchestrator.py +68 -209
- massgen/v1/streaming_display.py +62 -163
- massgen/v1/tools.py +8 -12
- massgen/v1/types.py +9 -23
- massgen/v1/utils.py +5 -23
- massgen-0.1.0.dist-info/METADATA +1245 -0
- massgen-0.1.0.dist-info/RECORD +273 -0
- massgen-0.1.0.dist-info/entry_points.txt +2 -0
- massgen/frontend/logging/__init__.py +0 -9
- massgen/frontend/logging/realtime_logger.py +0 -197
- massgen-0.0.3.dist-info/METADATA +0 -568
- massgen-0.0.3.dist-info/RECORD +0 -76
- massgen-0.0.3.dist-info/entry_points.txt +0 -2
- /massgen/backend/{Function calling openai responses.md → docs/Function calling openai responses.md} +0 -0
- {massgen-0.0.3.dist-info → massgen-0.1.0.dist-info}/WHEEL +0 -0
- {massgen-0.0.3.dist-info → massgen-0.1.0.dist-info}/licenses/LICENSE +0 -0
- {massgen-0.0.3.dist-info → massgen-0.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Message formatting utilities.
|
|
4
|
+
Provides utility classes for message formatting and conversion.
|
|
5
|
+
"""
|
|
6
|
+
from ._chat_completions_formatter import ChatCompletionsFormatter
|
|
7
|
+
from ._claude_formatter import ClaudeFormatter
|
|
8
|
+
from ._response_formatter import ResponseFormatter
|
|
9
|
+
|
|
10
|
+
__all__ = ["ChatCompletionsFormatter", "ResponseFormatter", "ClaudeFormatter"]
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Chat Completions formatter implementation.
|
|
4
|
+
Handles formatting for OpenAI Chat Completions API format.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from typing import Any, Dict, List
|
|
11
|
+
|
|
12
|
+
from ._formatter_base import FormatterBase
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ChatCompletionsFormatter(FormatterBase):
|
|
16
|
+
"""Formatter for Chat Completions API format."""
|
|
17
|
+
|
|
18
|
+
def format_messages(self, messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
19
|
+
"""
|
|
20
|
+
Convert messages for Chat Completions API compatibility.
|
|
21
|
+
|
|
22
|
+
Chat Completions API expects tool call arguments as JSON strings in conversation history,
|
|
23
|
+
but they may be passed as objects from other parts of the system.
|
|
24
|
+
"""
|
|
25
|
+
converted_messages = []
|
|
26
|
+
|
|
27
|
+
for message in messages:
|
|
28
|
+
# Create a copy to avoid modifying the original
|
|
29
|
+
converted_msg = dict(message)
|
|
30
|
+
|
|
31
|
+
# Normalize multimodal content (text/image/audio/video)
|
|
32
|
+
converted_msg = self._convert_multimodal_content(converted_msg)
|
|
33
|
+
|
|
34
|
+
# Convert tool_calls arguments from objects to JSON strings
|
|
35
|
+
if message.get("role") == "assistant" and "tool_calls" in message:
|
|
36
|
+
converted_tool_calls = []
|
|
37
|
+
for tool_call in message["tool_calls"]:
|
|
38
|
+
converted_call = dict(tool_call)
|
|
39
|
+
if "function" in converted_call:
|
|
40
|
+
converted_function = dict(converted_call["function"])
|
|
41
|
+
arguments = converted_function.get("arguments")
|
|
42
|
+
|
|
43
|
+
# Convert arguments to JSON string if it's an object
|
|
44
|
+
if isinstance(arguments, dict):
|
|
45
|
+
converted_function["arguments"] = json.dumps(arguments)
|
|
46
|
+
elif arguments is None:
|
|
47
|
+
converted_function["arguments"] = "{}"
|
|
48
|
+
elif not isinstance(arguments, str):
|
|
49
|
+
# Handle other non-string types
|
|
50
|
+
converted_function["arguments"] = self._serialize_tool_arguments(arguments)
|
|
51
|
+
# If it's already a string, keep it as-is
|
|
52
|
+
|
|
53
|
+
converted_call["function"] = converted_function
|
|
54
|
+
converted_tool_calls.append(converted_call)
|
|
55
|
+
converted_msg["tool_calls"] = converted_tool_calls
|
|
56
|
+
|
|
57
|
+
converted_messages.append(converted_msg)
|
|
58
|
+
|
|
59
|
+
return converted_messages
|
|
60
|
+
|
|
61
|
+
def _convert_multimodal_content(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
62
|
+
"""
|
|
63
|
+
Convert multimodal content to Chat Completions API format.
|
|
64
|
+
"""
|
|
65
|
+
content = message.get("content")
|
|
66
|
+
|
|
67
|
+
# If content is not a list, no conversion needed
|
|
68
|
+
if not isinstance(content, list):
|
|
69
|
+
return message
|
|
70
|
+
|
|
71
|
+
converted_content = []
|
|
72
|
+
for item in content:
|
|
73
|
+
if not isinstance(item, dict):
|
|
74
|
+
# If item is a string, treat as text
|
|
75
|
+
converted_content.append({"type": "text", "text": str(item)})
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
item_type = item.get("type")
|
|
79
|
+
|
|
80
|
+
if item_type == "text":
|
|
81
|
+
# Text items pass through as-is
|
|
82
|
+
converted_content.append(item)
|
|
83
|
+
|
|
84
|
+
elif item_type == "image":
|
|
85
|
+
# Convert image item to image_url format
|
|
86
|
+
converted_item = self._convert_image_content(item)
|
|
87
|
+
if converted_item:
|
|
88
|
+
converted_content.append(converted_item)
|
|
89
|
+
|
|
90
|
+
elif item_type == "audio":
|
|
91
|
+
# Convert audio item to input_audio format (base64)
|
|
92
|
+
converted_item = self._convert_audio_content(item)
|
|
93
|
+
if converted_item:
|
|
94
|
+
converted_content.append(converted_item)
|
|
95
|
+
|
|
96
|
+
elif item_type == "video":
|
|
97
|
+
# Convert video item to video_url format (base64 data URL)
|
|
98
|
+
converted_item = self._convert_video_content(item)
|
|
99
|
+
if converted_item:
|
|
100
|
+
converted_content.append(converted_item)
|
|
101
|
+
|
|
102
|
+
elif item_type == "video_url":
|
|
103
|
+
# Convert video URL to video_url format
|
|
104
|
+
converted_item = self._convert_video_url_content(item)
|
|
105
|
+
if converted_item:
|
|
106
|
+
converted_content.append(converted_item)
|
|
107
|
+
|
|
108
|
+
elif item_type == "file_pending_upload":
|
|
109
|
+
continue
|
|
110
|
+
elif item_type in ["image_url", "input_audio", "video_url"]:
|
|
111
|
+
# Already in Chat Completions API format
|
|
112
|
+
converted_content.append(item)
|
|
113
|
+
|
|
114
|
+
else:
|
|
115
|
+
# Unknown type, pass through
|
|
116
|
+
converted_content.append(item)
|
|
117
|
+
|
|
118
|
+
message["content"] = converted_content
|
|
119
|
+
return message
|
|
120
|
+
|
|
121
|
+
def _convert_image_content(self, image_item: Dict[str, Any]) -> Dict[str, Any]:
|
|
122
|
+
"""
|
|
123
|
+
Convert image content item to Chat Completions API format.
|
|
124
|
+
|
|
125
|
+
Supports:
|
|
126
|
+
- URL format: {"type": "image", "url": "https://..."}
|
|
127
|
+
- Base64 format: {"type": "image", "base64": "...", "mime_type": "image/jpeg"}
|
|
128
|
+
|
|
129
|
+
Returns Chat Completions format: {"type": "image_url", "image_url": {"url": "..."}}
|
|
130
|
+
"""
|
|
131
|
+
# Handle URL format
|
|
132
|
+
if "url" in image_item:
|
|
133
|
+
return {
|
|
134
|
+
"type": "image_url",
|
|
135
|
+
"image_url": {"url": image_item["url"]},
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# Handle base64 format
|
|
139
|
+
if "base64" in image_item:
|
|
140
|
+
mime_type = image_item.get("mime_type", "image/jpeg")
|
|
141
|
+
base64_data = image_item["base64"]
|
|
142
|
+
|
|
143
|
+
# Create data URL
|
|
144
|
+
data_url = f"data:{mime_type};base64,{base64_data}"
|
|
145
|
+
return {
|
|
146
|
+
"type": "image_url",
|
|
147
|
+
"image_url": {"url": data_url},
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# No valid image data found
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
def _convert_audio_content(self, audio_item: Dict[str, Any]) -> Dict[str, Any]:
|
|
154
|
+
"""
|
|
155
|
+
Convert audio content item to Chat Completions API format.
|
|
156
|
+
|
|
157
|
+
Supports base64 format: {"type": "audio", "base64": "...", "mime_type": "audio/wav"}
|
|
158
|
+
|
|
159
|
+
Returns Chat Completions format: {"type": "input_audio", "input_audio": {"data": "...", "format": "wav"}}
|
|
160
|
+
"""
|
|
161
|
+
if "base64" not in audio_item:
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
base64_data = audio_item["base64"]
|
|
165
|
+
mime_type = audio_item.get("mime_type", "audio/wav")
|
|
166
|
+
|
|
167
|
+
# Extract format from MIME type (e.g., "audio/wav" → "wav")
|
|
168
|
+
audio_format = mime_type.split("/")[-1] if "/" in mime_type else "wav"
|
|
169
|
+
|
|
170
|
+
# Map common MIME types to OpenAI audio formats
|
|
171
|
+
format_mapping = {
|
|
172
|
+
"mpeg": "mp3",
|
|
173
|
+
"x-wav": "wav",
|
|
174
|
+
"wave": "wav",
|
|
175
|
+
}
|
|
176
|
+
audio_format = format_mapping.get(audio_format, audio_format)
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
"type": "input_audio",
|
|
180
|
+
"input_audio": {
|
|
181
|
+
"data": base64_data,
|
|
182
|
+
"format": audio_format,
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
def _convert_video_content(self, video_item: Dict[str, Any]) -> Dict[str, Any]:
|
|
187
|
+
"""
|
|
188
|
+
Convert video content item to Chat Completions API format.
|
|
189
|
+
|
|
190
|
+
Supports base64 format: {"type": "video", "base64": "...", "mime_type": "video/mp4"}
|
|
191
|
+
|
|
192
|
+
Returns format: {"type": "video_url", "video_url": {"url": "data:video/...;base64,..."}}
|
|
193
|
+
"""
|
|
194
|
+
if "base64" not in video_item:
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
base64_data = video_item["base64"]
|
|
198
|
+
mime_type = video_item.get("mime_type", "video/mp4")
|
|
199
|
+
|
|
200
|
+
# Create data URL
|
|
201
|
+
data_url = f"data:{mime_type};base64,{base64_data}"
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
"type": "video_url",
|
|
205
|
+
"video_url": {"url": data_url},
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
def _convert_video_url_content(self, video_item: Dict[str, Any]) -> Dict[str, Any]:
|
|
209
|
+
"""
|
|
210
|
+
Convert video URL content item to Chat Completions API format.
|
|
211
|
+
|
|
212
|
+
Supports URL format: {"type": "video_url", "url": "https://..."}
|
|
213
|
+
|
|
214
|
+
Returns format: {"type": "video_url", "video_url": {"url": "..."}}
|
|
215
|
+
"""
|
|
216
|
+
if "url" not in video_item:
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
"type": "video_url",
|
|
221
|
+
"video_url": {"url": video_item["url"]},
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
def format_tools(self, tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
225
|
+
"""
|
|
226
|
+
Convert tools to Chat Completions format if needed.
|
|
227
|
+
|
|
228
|
+
Response API format: {"type": "function", "name": ..., "description": ..., "parameters": ...}
|
|
229
|
+
Chat Completions format: {"type": "function", "function": {"name": ..., "description": ..., "parameters": ...}}
|
|
230
|
+
"""
|
|
231
|
+
if not tools:
|
|
232
|
+
return tools
|
|
233
|
+
|
|
234
|
+
converted_tools = []
|
|
235
|
+
for tool in tools:
|
|
236
|
+
if tool.get("type") == "function":
|
|
237
|
+
if "function" in tool:
|
|
238
|
+
# Already in Chat Completions format
|
|
239
|
+
converted_tools.append(tool)
|
|
240
|
+
elif "name" in tool and "description" in tool:
|
|
241
|
+
# Response API format - convert to Chat Completions format
|
|
242
|
+
converted_tools.append(
|
|
243
|
+
{
|
|
244
|
+
"type": "function",
|
|
245
|
+
"function": {
|
|
246
|
+
"name": tool["name"],
|
|
247
|
+
"description": tool["description"],
|
|
248
|
+
"parameters": tool.get("parameters", {}),
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
)
|
|
252
|
+
else:
|
|
253
|
+
# Unknown format - keep as-is
|
|
254
|
+
converted_tools.append(tool)
|
|
255
|
+
else:
|
|
256
|
+
# Non-function tool - keep as-is
|
|
257
|
+
converted_tools.append(tool)
|
|
258
|
+
|
|
259
|
+
return converted_tools
|
|
260
|
+
|
|
261
|
+
def format_mcp_tools(self, mcp_functions: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
262
|
+
"""Convert MCP tools to Chat Completions format."""
|
|
263
|
+
if not mcp_functions:
|
|
264
|
+
return []
|
|
265
|
+
|
|
266
|
+
converted_tools = []
|
|
267
|
+
for mcp_function in mcp_functions.values():
|
|
268
|
+
if hasattr(mcp_function, "to_chat_completions_format"):
|
|
269
|
+
tool = mcp_function.to_chat_completions_format()
|
|
270
|
+
elif hasattr(mcp_function, "to_openai_format"):
|
|
271
|
+
tool = mcp_function.to_openai_format()
|
|
272
|
+
else:
|
|
273
|
+
# Fallback format
|
|
274
|
+
tool = {
|
|
275
|
+
"type": "function",
|
|
276
|
+
"function": {
|
|
277
|
+
"name": getattr(mcp_function, "name", "unknown"),
|
|
278
|
+
"description": getattr(mcp_function, "description", ""),
|
|
279
|
+
"parameters": getattr(mcp_function, "input_schema", {}),
|
|
280
|
+
},
|
|
281
|
+
}
|
|
282
|
+
converted_tools.append(tool)
|
|
283
|
+
|
|
284
|
+
return converted_tools
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Claude formatter implementation.
|
|
4
|
+
Handles formatting for Anthropic Claude Messages API format.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Dict, List, Tuple
|
|
10
|
+
|
|
11
|
+
from ._formatter_base import FormatterBase
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ClaudeFormatter(FormatterBase):
|
|
15
|
+
"""Formatter for Claude API format."""
|
|
16
|
+
|
|
17
|
+
def format_messages(self, messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
18
|
+
formatted, _ = self.format_messages_and_system(messages)
|
|
19
|
+
return formatted
|
|
20
|
+
|
|
21
|
+
def format_messages_and_system(
|
|
22
|
+
self,
|
|
23
|
+
messages: List[Dict[str, Any]],
|
|
24
|
+
) -> Tuple[List[Dict[str, Any]], str]:
|
|
25
|
+
"""
|
|
26
|
+
Convert messages to Claude's expected format.
|
|
27
|
+
|
|
28
|
+
Handle different tool message formats and extract system message:
|
|
29
|
+
- Chat Completions tool message: {"role": "tool", "tool_call_id": "...", "content": "..."}
|
|
30
|
+
- Response API tool message: {"type": "function_call_output", "call_id": "...", "output": "..."}
|
|
31
|
+
- System messages: Extract and return separately for top-level system parameter
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
tuple: (converted_messages, system_message)
|
|
35
|
+
"""
|
|
36
|
+
converted_messages = []
|
|
37
|
+
system_message = ""
|
|
38
|
+
|
|
39
|
+
for message in messages:
|
|
40
|
+
if message.get("role") == "system":
|
|
41
|
+
# Extract system message for top-level parameter
|
|
42
|
+
system_message = message.get("content", "")
|
|
43
|
+
elif message.get("role") == "tool":
|
|
44
|
+
# Chat Completions tool message -> Claude tool result
|
|
45
|
+
converted_messages.append(
|
|
46
|
+
{
|
|
47
|
+
"role": "user",
|
|
48
|
+
"content": [
|
|
49
|
+
{
|
|
50
|
+
"type": "tool_result",
|
|
51
|
+
"tool_use_id": message.get("tool_call_id"),
|
|
52
|
+
"content": message.get("content", ""),
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
)
|
|
57
|
+
elif message.get("type") == "function_call_output":
|
|
58
|
+
# Response API tool message -> Claude tool result
|
|
59
|
+
converted_messages.append(
|
|
60
|
+
{
|
|
61
|
+
"role": "user",
|
|
62
|
+
"content": [
|
|
63
|
+
{
|
|
64
|
+
"type": "tool_result",
|
|
65
|
+
"tool_use_id": message.get("call_id"),
|
|
66
|
+
"content": message.get("output", ""),
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
elif message.get("role") == "assistant" and "tool_calls" in message:
|
|
72
|
+
# Assistant message with tool calls - convert to Claude format
|
|
73
|
+
content = []
|
|
74
|
+
|
|
75
|
+
# Add text content if present
|
|
76
|
+
if message.get("content"):
|
|
77
|
+
content.append({"type": "text", "text": message["content"]})
|
|
78
|
+
|
|
79
|
+
# Convert tool calls to Claude tool use format
|
|
80
|
+
for tool_call in message["tool_calls"]:
|
|
81
|
+
tool_name = self.extract_tool_name(tool_call)
|
|
82
|
+
tool_args = self.extract_tool_arguments(tool_call)
|
|
83
|
+
tool_id = self.extract_tool_call_id(tool_call)
|
|
84
|
+
|
|
85
|
+
content.append(
|
|
86
|
+
{
|
|
87
|
+
"type": "tool_use",
|
|
88
|
+
"id": tool_id,
|
|
89
|
+
"name": tool_name,
|
|
90
|
+
"input": tool_args,
|
|
91
|
+
},
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
converted_messages.append({"role": "assistant", "content": content})
|
|
95
|
+
elif message.get("role") in ["user", "assistant"]:
|
|
96
|
+
# Keep user and assistant messages, skip system
|
|
97
|
+
converted_message = dict(message)
|
|
98
|
+
if isinstance(converted_message.get("content"), str):
|
|
99
|
+
# Claude expects content to be text for simple messages
|
|
100
|
+
pass
|
|
101
|
+
elif isinstance(converted_message.get("content"), list):
|
|
102
|
+
converted_message = self._convert_multimodal_content(converted_message)
|
|
103
|
+
converted_messages.append(converted_message)
|
|
104
|
+
|
|
105
|
+
return converted_messages, system_message
|
|
106
|
+
|
|
107
|
+
def _convert_multimodal_content(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
108
|
+
"""Normalize multimodal content blocks to Claude's nested source structure."""
|
|
109
|
+
|
|
110
|
+
content = message.get("content")
|
|
111
|
+
if not isinstance(content, list):
|
|
112
|
+
return message
|
|
113
|
+
|
|
114
|
+
# Formatter handles generic multimodal content; upload_files-sourced items already preprocessed in backend.
|
|
115
|
+
converted_items: List[Dict[str, Any]] = []
|
|
116
|
+
|
|
117
|
+
for item in content:
|
|
118
|
+
if not isinstance(item, dict):
|
|
119
|
+
converted_items.append(item)
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
item_type = item.get("type")
|
|
123
|
+
|
|
124
|
+
if item_type in {"tool_result", "tool_use", "text", "file_pending_upload"}:
|
|
125
|
+
converted_items.append(item)
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
if item_type not in {"image", "document"}:
|
|
129
|
+
converted_items.append(item)
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
if isinstance(item.get("source"), dict):
|
|
133
|
+
converted_items.append(item)
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
if "file_id" in item:
|
|
137
|
+
normalized = {key: value for key, value in item.items() if key != "file_id"}
|
|
138
|
+
normalized["source"] = {
|
|
139
|
+
"type": "file",
|
|
140
|
+
"file_id": item["file_id"],
|
|
141
|
+
}
|
|
142
|
+
converted_items.append(normalized)
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
if "base64" in item:
|
|
146
|
+
media_type = item.get("mime_type") or item.get("media_type")
|
|
147
|
+
normalized = {key: value for key, value in item.items() if key not in {"base64", "mime_type", "media_type"}}
|
|
148
|
+
normalized["source"] = {
|
|
149
|
+
"type": "base64",
|
|
150
|
+
"media_type": media_type,
|
|
151
|
+
"data": item["base64"],
|
|
152
|
+
}
|
|
153
|
+
converted_items.append(normalized)
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
if "url" in item:
|
|
157
|
+
normalized = {key: value for key, value in item.items() if key != "url"}
|
|
158
|
+
normalized["source"] = {
|
|
159
|
+
"type": "url",
|
|
160
|
+
"url": item["url"],
|
|
161
|
+
}
|
|
162
|
+
converted_items.append(normalized)
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
converted_items.append(item)
|
|
166
|
+
|
|
167
|
+
message["content"] = converted_items
|
|
168
|
+
return message
|
|
169
|
+
|
|
170
|
+
def format_tools(self, tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
171
|
+
"""
|
|
172
|
+
Convert tools to Claude's expected format.
|
|
173
|
+
|
|
174
|
+
Input formats supported:
|
|
175
|
+
- Response API format: {"type": "function", "name": ..., "description": ..., "parameters": ...}
|
|
176
|
+
- Chat Completions format: {"type": "function", "function": {"name": ..., "description": ..., "parameters": ...}}
|
|
177
|
+
|
|
178
|
+
Claude format: {"type": "custom", "name": ..., "description": ..., "input_schema": ...}
|
|
179
|
+
"""
|
|
180
|
+
if not tools:
|
|
181
|
+
return tools
|
|
182
|
+
|
|
183
|
+
converted_tools = []
|
|
184
|
+
for tool in tools:
|
|
185
|
+
if tool.get("type") == "function":
|
|
186
|
+
if "function" in tool:
|
|
187
|
+
# Chat Completions format -> Claude custom tool
|
|
188
|
+
func = tool["function"]
|
|
189
|
+
converted_tools.append(
|
|
190
|
+
{
|
|
191
|
+
"type": "custom",
|
|
192
|
+
"name": func["name"],
|
|
193
|
+
"description": func["description"],
|
|
194
|
+
"input_schema": func.get("parameters", {}),
|
|
195
|
+
},
|
|
196
|
+
)
|
|
197
|
+
elif "name" in tool and "description" in tool:
|
|
198
|
+
# Response API format -> Claude custom tool
|
|
199
|
+
converted_tools.append(
|
|
200
|
+
{
|
|
201
|
+
"type": "custom",
|
|
202
|
+
"name": tool["name"],
|
|
203
|
+
"description": tool["description"],
|
|
204
|
+
"input_schema": tool.get("parameters", {}),
|
|
205
|
+
},
|
|
206
|
+
)
|
|
207
|
+
else:
|
|
208
|
+
# Unknown format - keep as-is
|
|
209
|
+
converted_tools.append(tool)
|
|
210
|
+
else:
|
|
211
|
+
# Non-function tool (builtin tools) - keep as-is
|
|
212
|
+
converted_tools.append(tool)
|
|
213
|
+
|
|
214
|
+
return converted_tools
|
|
215
|
+
|
|
216
|
+
def format_mcp_tools(self, mcp_functions: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
217
|
+
"""Convert MCP tools to Claude's custom tool format."""
|
|
218
|
+
if not mcp_functions:
|
|
219
|
+
return []
|
|
220
|
+
|
|
221
|
+
converted_tools = []
|
|
222
|
+
for mcp_function in mcp_functions.values():
|
|
223
|
+
if hasattr(mcp_function, "to_claude_format"):
|
|
224
|
+
tool = mcp_function.to_claude_format()
|
|
225
|
+
else:
|
|
226
|
+
# Fallback format for Claude
|
|
227
|
+
tool = {
|
|
228
|
+
"type": "custom",
|
|
229
|
+
"name": getattr(mcp_function, "name", "unknown"),
|
|
230
|
+
"description": getattr(mcp_function, "description", ""),
|
|
231
|
+
"input_schema": getattr(mcp_function, "input_schema", {}),
|
|
232
|
+
}
|
|
233
|
+
converted_tools.append(tool)
|
|
234
|
+
|
|
235
|
+
return converted_tools
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Base class for API parameters handlers.
|
|
4
|
+
Provides common functionality for building API parameters across different backends.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from typing import Any, Dict, List
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FormatterBase(ABC):
|
|
14
|
+
"""Abstract base class for API parameter handlers."""
|
|
15
|
+
|
|
16
|
+
def __init__(self) -> None:
|
|
17
|
+
"""Initialize the API params handler.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
backend_instance: The backend instance containing necessary formatters and config
|
|
21
|
+
"""
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def format_messages(
|
|
26
|
+
self,
|
|
27
|
+
messages: List[Dict[str, Any]],
|
|
28
|
+
) -> List[Dict[str, Any]]:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def format_tools(
|
|
33
|
+
self,
|
|
34
|
+
tools: List[Dict[str, Any]],
|
|
35
|
+
) -> List[Dict[str, Any]]:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def format_mcp_tools(
|
|
40
|
+
self,
|
|
41
|
+
mcp_functions: Dict[str, Any],
|
|
42
|
+
) -> List[Dict[str, Any]]:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def extract_tool_name(tool_call: Dict[str, Any]) -> str:
|
|
47
|
+
"""
|
|
48
|
+
Extract tool name from a tool call (handles multiple formats).
|
|
49
|
+
|
|
50
|
+
Supports:
|
|
51
|
+
- Chat Completions format: {"function": {"name": "...", ...}}
|
|
52
|
+
- Response API format: {"name": "..."}
|
|
53
|
+
- Claude native format: {"name": "..."}
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
tool_call: Tool call data structure from any backend
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Tool name string
|
|
60
|
+
"""
|
|
61
|
+
# Chat Completions format
|
|
62
|
+
if "function" in tool_call:
|
|
63
|
+
return tool_call.get("function", {}).get("name", "unknown")
|
|
64
|
+
# Response API / Claude native format
|
|
65
|
+
elif "name" in tool_call:
|
|
66
|
+
return tool_call.get("name", "unknown")
|
|
67
|
+
# Fallback
|
|
68
|
+
return "unknown"
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def extract_tool_arguments(tool_call: Dict[str, Any]) -> Dict[str, Any]:
|
|
72
|
+
"""
|
|
73
|
+
Extract tool arguments from a tool call (handles multiple formats).
|
|
74
|
+
|
|
75
|
+
Supports:
|
|
76
|
+
- Chat Completions format: {"function": {"arguments": ...}}
|
|
77
|
+
- Response API format: {"arguments": ...}
|
|
78
|
+
- Claude native format: {"input": ...}
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
tool_call: Tool call data structure from any backend
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Tool arguments dictionary (parsed from JSON string if needed)
|
|
85
|
+
"""
|
|
86
|
+
import json
|
|
87
|
+
|
|
88
|
+
# Chat Completions format
|
|
89
|
+
if "function" in tool_call:
|
|
90
|
+
args = tool_call.get("function", {}).get("arguments", {})
|
|
91
|
+
# Claude native format
|
|
92
|
+
elif "input" in tool_call:
|
|
93
|
+
args = tool_call.get("input", {})
|
|
94
|
+
# Response API format
|
|
95
|
+
elif "arguments" in tool_call:
|
|
96
|
+
args = tool_call.get("arguments", {})
|
|
97
|
+
else:
|
|
98
|
+
args = {}
|
|
99
|
+
|
|
100
|
+
# Parse JSON string if needed
|
|
101
|
+
if isinstance(args, str):
|
|
102
|
+
try:
|
|
103
|
+
return json.loads(args) if args.strip() else {}
|
|
104
|
+
except (json.JSONDecodeError, ValueError):
|
|
105
|
+
return {}
|
|
106
|
+
return args if isinstance(args, dict) else {}
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def extract_tool_call_id(tool_call: Dict[str, Any]) -> str:
|
|
110
|
+
"""
|
|
111
|
+
Extract tool call ID from a tool call (handles multiple formats).
|
|
112
|
+
|
|
113
|
+
Supports:
|
|
114
|
+
- Chat Completions format: {"id": "..."}
|
|
115
|
+
- Response API format: {"call_id": "..."}
|
|
116
|
+
- Claude native format: {"id": "..."}
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
tool_call: Tool call data structure from any backend
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Tool call ID string
|
|
123
|
+
"""
|
|
124
|
+
# Try multiple possible ID fields
|
|
125
|
+
return tool_call.get("id") or tool_call.get("call_id") or ""
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def _serialize_tool_arguments(arguments) -> str:
|
|
129
|
+
"""Safely serialize tool call arguments to JSON string.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
arguments: Tool arguments (can be string, dict, or other types)
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
JSON string representation of arguments
|
|
136
|
+
"""
|
|
137
|
+
import json
|
|
138
|
+
|
|
139
|
+
if isinstance(arguments, str):
|
|
140
|
+
# If already a string, validate it's valid JSON
|
|
141
|
+
try:
|
|
142
|
+
json.loads(arguments) # Validate JSON
|
|
143
|
+
return arguments
|
|
144
|
+
except (json.JSONDecodeError, ValueError):
|
|
145
|
+
# If not valid JSON, treat as plain string and wrap in quotes
|
|
146
|
+
return json.dumps(arguments)
|
|
147
|
+
elif arguments is None:
|
|
148
|
+
return "{}"
|
|
149
|
+
else:
|
|
150
|
+
# Convert to JSON string
|
|
151
|
+
try:
|
|
152
|
+
return json.dumps(arguments)
|
|
153
|
+
except (TypeError, ValueError) as e:
|
|
154
|
+
# Logger not imported at module level, use print for warning
|
|
155
|
+
print(f"Warning: Failed to serialize tool arguments: {e}, arguments: {arguments}")
|
|
156
|
+
return "{}"
|