massgen 0.0.3__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 (76) hide show
  1. massgen/__init__.py +94 -0
  2. massgen/agent_config.py +507 -0
  3. massgen/backend/CLAUDE_API_RESEARCH.md +266 -0
  4. massgen/backend/Function calling openai responses.md +1161 -0
  5. massgen/backend/GEMINI_API_DOCUMENTATION.md +410 -0
  6. massgen/backend/OPENAI_RESPONSES_API_FORMAT.md +65 -0
  7. massgen/backend/__init__.py +25 -0
  8. massgen/backend/base.py +180 -0
  9. massgen/backend/chat_completions.py +228 -0
  10. massgen/backend/claude.py +661 -0
  11. massgen/backend/gemini.py +652 -0
  12. massgen/backend/grok.py +187 -0
  13. massgen/backend/response.py +397 -0
  14. massgen/chat_agent.py +440 -0
  15. massgen/cli.py +686 -0
  16. massgen/configs/README.md +293 -0
  17. massgen/configs/creative_team.yaml +53 -0
  18. massgen/configs/gemini_4o_claude.yaml +31 -0
  19. massgen/configs/news_analysis.yaml +51 -0
  20. massgen/configs/research_team.yaml +51 -0
  21. massgen/configs/single_agent.yaml +18 -0
  22. massgen/configs/single_flash2.5.yaml +44 -0
  23. massgen/configs/technical_analysis.yaml +51 -0
  24. massgen/configs/three_agents_default.yaml +31 -0
  25. massgen/configs/travel_planning.yaml +51 -0
  26. massgen/configs/two_agents.yaml +39 -0
  27. massgen/frontend/__init__.py +20 -0
  28. massgen/frontend/coordination_ui.py +945 -0
  29. massgen/frontend/displays/__init__.py +24 -0
  30. massgen/frontend/displays/base_display.py +83 -0
  31. massgen/frontend/displays/rich_terminal_display.py +3497 -0
  32. massgen/frontend/displays/simple_display.py +93 -0
  33. massgen/frontend/displays/terminal_display.py +381 -0
  34. massgen/frontend/logging/__init__.py +9 -0
  35. massgen/frontend/logging/realtime_logger.py +197 -0
  36. massgen/message_templates.py +431 -0
  37. massgen/orchestrator.py +1222 -0
  38. massgen/tests/__init__.py +10 -0
  39. massgen/tests/multi_turn_conversation_design.md +214 -0
  40. massgen/tests/multiturn_llm_input_analysis.md +189 -0
  41. massgen/tests/test_case_studies.md +113 -0
  42. massgen/tests/test_claude_backend.py +310 -0
  43. massgen/tests/test_grok_backend.py +160 -0
  44. massgen/tests/test_message_context_building.py +293 -0
  45. massgen/tests/test_rich_terminal_display.py +378 -0
  46. massgen/tests/test_v3_3agents.py +117 -0
  47. massgen/tests/test_v3_simple.py +216 -0
  48. massgen/tests/test_v3_three_agents.py +272 -0
  49. massgen/tests/test_v3_two_agents.py +176 -0
  50. massgen/utils.py +79 -0
  51. massgen/v1/README.md +330 -0
  52. massgen/v1/__init__.py +91 -0
  53. massgen/v1/agent.py +605 -0
  54. massgen/v1/agents.py +330 -0
  55. massgen/v1/backends/gemini.py +584 -0
  56. massgen/v1/backends/grok.py +410 -0
  57. massgen/v1/backends/oai.py +571 -0
  58. massgen/v1/cli.py +351 -0
  59. massgen/v1/config.py +169 -0
  60. massgen/v1/examples/fast-4o-mini-config.yaml +44 -0
  61. massgen/v1/examples/fast_config.yaml +44 -0
  62. massgen/v1/examples/production.yaml +70 -0
  63. massgen/v1/examples/single_agent.yaml +39 -0
  64. massgen/v1/logging.py +974 -0
  65. massgen/v1/main.py +368 -0
  66. massgen/v1/orchestrator.py +1138 -0
  67. massgen/v1/streaming_display.py +1190 -0
  68. massgen/v1/tools.py +160 -0
  69. massgen/v1/types.py +245 -0
  70. massgen/v1/utils.py +199 -0
  71. massgen-0.0.3.dist-info/METADATA +568 -0
  72. massgen-0.0.3.dist-info/RECORD +76 -0
  73. massgen-0.0.3.dist-info/WHEEL +5 -0
  74. massgen-0.0.3.dist-info/entry_points.txt +2 -0
  75. massgen-0.0.3.dist-info/licenses/LICENSE +204 -0
  76. massgen-0.0.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,187 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ Grok/xAI backend implementation using OpenAI-compatible API.
5
+ Clean implementation with only Grok-specific features.
6
+
7
+ ✅ TESTED: Backend works correctly with architecture
8
+ - ✅ Grok API integration working
9
+ - ✅ Tool message conversion compatible with Chat Completions format
10
+ - ✅ Streaming functionality working correctly
11
+ - ✅ SingleAgent integration working
12
+ - ✅ Error handling and pricing calculations implemented
13
+
14
+ TODO for future releases:
15
+ - Test multi-agent orchestrator integration
16
+ - Test web search capabilities with tools
17
+ - Validate advanced Grok-specific features
18
+ """
19
+
20
+ import os
21
+ from typing import Dict, List, Any, AsyncGenerator, Optional
22
+ from .chat_completions import ChatCompletionsBackend
23
+ from .base import StreamChunk
24
+
25
+
26
+ class GrokBackend(ChatCompletionsBackend):
27
+ """Grok backend using xAI's OpenAI-compatible API."""
28
+
29
+ def __init__(self, api_key: Optional[str] = None, **kwargs):
30
+ super().__init__(api_key, **kwargs)
31
+ self.api_key = api_key or os.getenv("XAI_API_KEY")
32
+ self.base_url = "https://api.x.ai/v1"
33
+
34
+ async def stream_with_tools(
35
+ self, messages: List[Dict[str, Any]], tools: List[Dict[str, Any]], **kwargs
36
+ ) -> AsyncGenerator[StreamChunk, None]:
37
+ """Stream response using xAI's OpenAI-compatible API."""
38
+
39
+ # Convert messages for Grok API compatibility
40
+ grok_messages = self._convert_messages_for_grok(messages)
41
+
42
+ try:
43
+ import openai
44
+
45
+ # Use OpenAI client with xAI base URL
46
+ client = openai.AsyncOpenAI(api_key=self.api_key, base_url=self.base_url)
47
+
48
+ # Extract parameters
49
+ model = kwargs.get("model", "grok-3-mini")
50
+ max_tokens = kwargs.get("max_tokens", None)
51
+ temperature = kwargs.get("temperature", None)
52
+ enable_web_search = kwargs.get("enable_web_search", False)
53
+
54
+ # Convert tools to Chat Completions format
55
+ converted_tools = (
56
+ self.convert_tools_to_chat_completions_format(tools) if tools else None
57
+ )
58
+
59
+ # Chat Completions API parameters
60
+ api_params = {
61
+ "model": model,
62
+ "messages": grok_messages,
63
+ "tools": converted_tools,
64
+ "max_tokens": max_tokens,
65
+ "temperature": temperature,
66
+ "stream": True,
67
+ }
68
+
69
+ # Add Live Search parameters if enabled (Grok-specific)
70
+ if enable_web_search:
71
+ search_params_kwargs = {"mode": "auto", "return_citations": True}
72
+
73
+ # Allow override of search parameters from backend params
74
+ max_results = kwargs.get("max_search_results")
75
+ if max_results is not None:
76
+ search_params_kwargs["max_search_results"] = max_results
77
+
78
+ search_mode = kwargs.get("search_mode")
79
+ if search_mode is not None:
80
+ search_params_kwargs["mode"] = search_mode
81
+
82
+ return_citations = kwargs.get("return_citations")
83
+ if return_citations is not None:
84
+ search_params_kwargs["return_citations"] = return_citations
85
+
86
+ # Use extra_body to pass search_parameters to xAI API
87
+ api_params["extra_body"] = {"search_parameters": search_params_kwargs}
88
+
89
+ # Create stream
90
+ stream = await client.chat.completions.create(**api_params)
91
+
92
+ # Use base class streaming handler
93
+ async for chunk in self.handle_chat_completions_stream(
94
+ stream, enable_web_search
95
+ ):
96
+ yield chunk
97
+
98
+ except Exception as e:
99
+ yield StreamChunk(type="error", error=f"Grok API error: {e}")
100
+
101
+ def get_provider_name(self) -> str:
102
+ """Get the name of this provider."""
103
+ return "Grok"
104
+
105
+ def get_supported_builtin_tools(self) -> List[str]:
106
+ """Get list of builtin tools supported by Grok."""
107
+ return ["web_search"]
108
+
109
+ def estimate_tokens(self, text: str) -> int:
110
+ """Estimate token count for text (rough approximation)."""
111
+ return int(len(text.split()) * 1.3)
112
+
113
+ def calculate_cost(
114
+ self, input_tokens: int, output_tokens: int, model: str
115
+ ) -> float:
116
+ """Calculate cost for token usage."""
117
+ model_lower = model.lower()
118
+
119
+ # Handle -mini models with lower costs
120
+ if "grok-2" in model_lower:
121
+ if "mini" in model_lower:
122
+ input_cost = (input_tokens / 1_000_000) * 1.0 # Lower cost for mini
123
+ output_cost = (output_tokens / 1_000_000) * 5.0
124
+ else:
125
+ input_cost = (input_tokens / 1_000_000) * 2.0
126
+ output_cost = (output_tokens / 1_000_000) * 10.0
127
+ elif "grok-3" in model_lower:
128
+ if "mini" in model_lower:
129
+ input_cost = (input_tokens / 1_000_000) * 2.5 # Lower cost for mini
130
+ output_cost = (output_tokens / 1_000_000) * 7.5
131
+ else:
132
+ input_cost = (input_tokens / 1_000_000) * 5.0
133
+ output_cost = (output_tokens / 1_000_000) * 15.0
134
+ elif "grok-4" in model_lower:
135
+ if "mini" in model_lower:
136
+ input_cost = (input_tokens / 1_000_000) * 4.0 # Lower cost for mini
137
+ output_cost = (output_tokens / 1_000_000) * 10.0
138
+ else:
139
+ input_cost = (input_tokens / 1_000_000) * 8.0
140
+ output_cost = (output_tokens / 1_000_000) * 20.0
141
+ else:
142
+ # Default fallback (assume grok-3 pricing)
143
+ input_cost = (input_tokens / 1_000_000) * 5.0
144
+ output_cost = (output_tokens / 1_000_000) * 15.0
145
+
146
+ return input_cost + output_cost
147
+
148
+ def _convert_messages_for_grok(
149
+ self, messages: List[Dict[str, Any]]
150
+ ) -> List[Dict[str, Any]]:
151
+ """
152
+ Convert messages for Grok API compatibility.
153
+
154
+ Grok expects tool call arguments as JSON strings in conversation history,
155
+ but returns them as objects in responses.
156
+ """
157
+ import json
158
+
159
+ converted_messages = []
160
+
161
+ for message in messages:
162
+ # Create a copy to avoid modifying the original
163
+ converted_msg = dict(message)
164
+
165
+ # Convert tool_calls arguments from objects to JSON strings
166
+ if message.get("role") == "assistant" and "tool_calls" in message:
167
+ converted_tool_calls = []
168
+ for tool_call in message["tool_calls"]:
169
+ converted_call = dict(tool_call)
170
+ if "function" in converted_call:
171
+ converted_function = dict(converted_call["function"])
172
+ arguments = converted_function.get("arguments")
173
+
174
+ # Convert arguments to JSON string if it's an object
175
+ if isinstance(arguments, dict):
176
+ converted_function["arguments"] = json.dumps(arguments)
177
+ elif arguments is None:
178
+ converted_function["arguments"] = "{}"
179
+ # If it's already a string, keep it as-is
180
+
181
+ converted_call["function"] = converted_function
182
+ converted_tool_calls.append(converted_call)
183
+ converted_msg["tool_calls"] = converted_tool_calls
184
+
185
+ converted_messages.append(converted_msg)
186
+
187
+ return converted_messages
@@ -0,0 +1,397 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ Response API backend implementation.
5
+ Standalone implementation optimized for the standard Response API format (originated by OpenAI).
6
+ """
7
+
8
+ import os
9
+ from typing import Dict, List, Any, AsyncGenerator, Optional
10
+ from .base import LLMBackend, StreamChunk
11
+
12
+
13
+ class ResponseBackend(LLMBackend):
14
+ """Backend using the standard Response API format."""
15
+
16
+ def __init__(self, api_key: Optional[str] = None, **kwargs):
17
+ super().__init__(api_key, **kwargs)
18
+ self.api_key = api_key or os.getenv("OPENAI_API_KEY")
19
+
20
+ def convert_tools_to_response_api_format(
21
+ self, tools: List[Dict[str, Any]]
22
+ ) -> List[Dict[str, Any]]:
23
+ """Convert tools from Chat Completions format to Response API format if needed.
24
+
25
+ Chat Completions format: {"type": "function", "function": {"name": ..., "description": ..., "parameters": ...}}
26
+ Response API format: {"type": "function", "name": ..., "description": ..., "parameters": ...}
27
+ """
28
+ if not tools:
29
+ return tools
30
+
31
+ converted_tools = []
32
+ for tool in tools:
33
+ if tool.get("type") == "function" and "function" in tool:
34
+ # Chat Completions format - convert to Response API format
35
+ func = tool["function"]
36
+ converted_tools.append(
37
+ {
38
+ "type": "function",
39
+ "name": func["name"],
40
+ "description": func["description"],
41
+ "parameters": func.get("parameters", {}),
42
+ }
43
+ )
44
+ else:
45
+ # Already in Response API format or non-function tool
46
+ converted_tools.append(tool)
47
+
48
+ return converted_tools
49
+
50
+ def convert_messages_to_response_api_format(
51
+ self, messages: List[Dict[str, Any]]
52
+ ) -> List[Dict[str, Any]]:
53
+ """Convert messages from Chat Completions format to Response API format.
54
+
55
+ Chat Completions tool message: {"role": "tool", "tool_call_id": "...", "content": "..."}
56
+ Response API tool message: {"type": "function_call_output", "call_id": "...", "output": "..."}
57
+
58
+ Note: Assistant messages with tool_calls should not be in input - they're generated by the backend.
59
+ """
60
+ converted_messages = []
61
+
62
+ for message in messages:
63
+ if message.get("role") == "tool":
64
+ # Convert Chat Completions tool message to Response API format
65
+ converted_messages.append(
66
+ {
67
+ "type": "function_call_output",
68
+ "call_id": message.get("tool_call_id"),
69
+ "output": message.get("content", ""),
70
+ }
71
+ )
72
+ elif message.get("type") == "function_call_output":
73
+ # Already in Response API format - keep as-is
74
+ converted_messages.append(message)
75
+ elif message.get("role") == "assistant" and "tool_calls" in message:
76
+ # Assistant message with tool_calls in native Responses API format
77
+ # Remove tool_calls when sending as input - only results should be sent back
78
+ cleaned_message = {
79
+ k: v for k, v in message.items() if k != "tool_calls"
80
+ }
81
+ converted_messages.append(cleaned_message)
82
+ else:
83
+ # Keep other message types as-is
84
+ converted_messages.append(message)
85
+
86
+ return converted_messages
87
+
88
+ async def stream_with_tools(
89
+ self, messages: List[Dict[str, Any]], tools: List[Dict[str, Any]], **kwargs
90
+ ) -> AsyncGenerator[StreamChunk, None]:
91
+ """Stream response using OpenAI Response API."""
92
+ try:
93
+ import openai
94
+
95
+ client = openai.AsyncOpenAI(api_key=self.api_key)
96
+
97
+ # Extract model and provider tool settings
98
+ model = kwargs.get("model", "gpt-4o-mini")
99
+ enable_web_search = kwargs.get("enable_web_search", False)
100
+ enable_code_interpreter = kwargs.get("enable_code_interpreter", False)
101
+
102
+ # Convert messages to Response API format (handles tool messages)
103
+ converted_messages = self.convert_messages_to_response_api_format(messages)
104
+
105
+ # Response API parameters (uses 'input', not 'messages')
106
+ api_params = {"model": model, "input": converted_messages, "stream": True}
107
+
108
+ # Add max_output_tokens if specified (o-series models don't support this)
109
+ max_tokens = kwargs.get("max_tokens")
110
+ if max_tokens and not model.startswith("o"):
111
+ api_params["max_output_tokens"] = max_tokens
112
+
113
+ # Add framework tools (convert to Response API format)
114
+ if tools:
115
+ converted_tools = self.convert_tools_to_response_api_format(tools)
116
+ api_params["tools"] = converted_tools
117
+
118
+ # Add provider tools (web search, code interpreter) if enabled
119
+ provider_tools = []
120
+ if enable_web_search:
121
+ provider_tools.append({"type": "web_search"})
122
+
123
+ if enable_code_interpreter:
124
+ provider_tools.append(
125
+ {"type": "code_interpreter", "container": {"type": "auto"}}
126
+ )
127
+
128
+ if provider_tools:
129
+ if "tools" not in api_params:
130
+ api_params["tools"] = []
131
+ api_params["tools"].extend(provider_tools)
132
+
133
+ stream = await client.responses.create(**api_params)
134
+
135
+ content = ""
136
+
137
+ async for chunk in stream:
138
+ # Handle Responses API streaming format
139
+ if hasattr(chunk, "type"):
140
+ if chunk.type == "response.output_text.delta" and hasattr(
141
+ chunk, "delta"
142
+ ):
143
+ content += chunk.delta
144
+ yield StreamChunk(type="content", content=chunk.delta)
145
+ elif chunk.type == "response.web_search_call.in_progress":
146
+ yield StreamChunk(
147
+ type="content",
148
+ content=f"\n🔍 [Provider Tool: Web Search] Starting search...",
149
+ )
150
+ elif chunk.type == "response.web_search_call.searching":
151
+ yield StreamChunk(
152
+ type="content",
153
+ content=f"🔍 [Provider Tool: Web Search] Searching...",
154
+ )
155
+ elif chunk.type == "response.web_search_call.completed":
156
+ yield StreamChunk(
157
+ type="content",
158
+ content=f"✅ [Provider Tool: Web Search] Search completed",
159
+ )
160
+ elif chunk.type == "response.code_interpreter_call.in_progress":
161
+ yield StreamChunk(
162
+ type="content",
163
+ content=f"\n💻 [Provider Tool: Code Interpreter] Starting execution...",
164
+ )
165
+ elif chunk.type == "response.code_interpreter_call.executing":
166
+ yield StreamChunk(
167
+ type="content",
168
+ content=f"💻 [Provider Tool: Code Interpreter] Executing...",
169
+ )
170
+ elif chunk.type == "response.code_interpreter_call.completed":
171
+ yield StreamChunk(
172
+ type="content",
173
+ content=f"✅ [Provider Tool: Code Interpreter] Execution completed",
174
+ )
175
+ elif chunk.type == "response.output_item.done":
176
+ # Get search query or executed code details - show them right after completion
177
+ if hasattr(chunk, "item") and chunk.item:
178
+ if (
179
+ hasattr(chunk.item, "type")
180
+ and chunk.item.type == "web_search_call"
181
+ ):
182
+ if hasattr(chunk.item, "action") and hasattr(
183
+ chunk.item.action, "query"
184
+ ):
185
+ search_query = chunk.item.action.query
186
+ if search_query:
187
+ yield StreamChunk(
188
+ type="content",
189
+ content=f"🔍 [Search Query] '{search_query}'",
190
+ )
191
+ elif (
192
+ hasattr(chunk.item, "type")
193
+ and chunk.item.type == "code_interpreter_call"
194
+ ):
195
+ if hasattr(chunk.item, "code") and chunk.item.code:
196
+ # Format code as a proper code block - don't assume language
197
+ yield StreamChunk(
198
+ type="content",
199
+ content=f"💻 [Code Executed]\n```\n{chunk.item.code}\n```\n",
200
+ )
201
+
202
+ # Also show the execution output if available
203
+ if (
204
+ hasattr(chunk.item, "outputs")
205
+ and chunk.item.outputs
206
+ ):
207
+ for output in chunk.item.outputs:
208
+ output_text = None
209
+ if hasattr(output, "text") and output.text:
210
+ output_text = output.text
211
+ elif (
212
+ hasattr(output, "content")
213
+ and output.content
214
+ ):
215
+ output_text = output.content
216
+ elif hasattr(output, "data") and output.data:
217
+ output_text = str(output.data)
218
+ elif isinstance(output, str):
219
+ output_text = output
220
+ elif isinstance(output, dict):
221
+ # Handle dict format outputs
222
+ if "text" in output:
223
+ output_text = output["text"]
224
+ elif "content" in output:
225
+ output_text = output["content"]
226
+ elif "data" in output:
227
+ output_text = str(output["data"])
228
+
229
+ if output_text and output_text.strip():
230
+ yield StreamChunk(
231
+ type="content",
232
+ content=f"📊 [Result] {output_text.strip()}\n",
233
+ )
234
+ elif chunk.type == "response.completed":
235
+ # Extract and yield tool calls from the complete response
236
+ if hasattr(chunk, "response"):
237
+ response_dict = self._convert_to_dict(chunk.response)
238
+
239
+ # Extract builtin tool results from output array
240
+ builtin_tool_results = []
241
+ if (
242
+ isinstance(response_dict, dict)
243
+ and "output" in response_dict
244
+ ):
245
+ for item in response_dict["output"]:
246
+ if item.get("type") == "code_interpreter_call":
247
+ # Code execution result
248
+ builtin_tool_results.append(
249
+ {
250
+ "id": item.get("id", ""),
251
+ "tool_type": "code_interpreter",
252
+ "status": item.get("status"),
253
+ "code": item.get("code", ""),
254
+ "outputs": item.get("outputs"),
255
+ "container_id": item.get(
256
+ "container_id"
257
+ ),
258
+ }
259
+ )
260
+ elif item.get("type") == "web_search_call":
261
+ # Web search result
262
+ builtin_tool_results.append(
263
+ {
264
+ "id": item.get("id", ""),
265
+ "tool_type": "web_search",
266
+ "status": item.get("status"),
267
+ "query": item.get("query", ""),
268
+ "results": item.get("results"),
269
+ }
270
+ )
271
+
272
+ # Yield builtin tool results if any were found
273
+ if builtin_tool_results:
274
+ yield StreamChunk(
275
+ type="builtin_tool_results",
276
+ builtin_tool_results=builtin_tool_results,
277
+ )
278
+
279
+ # Yield the complete response for internal use
280
+ yield StreamChunk(
281
+ type="complete_response", response=response_dict
282
+ )
283
+ else:
284
+ # Fallback if no response object
285
+ complete_message = {
286
+ "role": "assistant",
287
+ "content": content.strip(),
288
+ }
289
+ yield StreamChunk(
290
+ type="complete_message",
291
+ complete_message=complete_message,
292
+ )
293
+
294
+ # Signal completion
295
+ yield StreamChunk(type="done")
296
+
297
+ except Exception as e:
298
+ yield StreamChunk(type="error", error=str(e))
299
+
300
+ def get_provider_name(self) -> str:
301
+ """Get the provider name."""
302
+ return "OpenAI"
303
+
304
+ def get_supported_builtin_tools(self) -> List[str]:
305
+ """Get list of builtin tools supported by OpenAI."""
306
+ return ["web_search", "code_interpreter"]
307
+
308
+ def extract_tool_name(self, tool_call: Dict[str, Any]) -> str:
309
+ """Extract tool name from OpenAI format (handles both Chat Completions and Responses API)."""
310
+ # Check if it's Chat Completions format
311
+ if "function" in tool_call:
312
+ return tool_call.get("function", {}).get("name", "unknown")
313
+ # Otherwise assume Responses API format
314
+ return tool_call.get("name", "unknown")
315
+
316
+ def extract_tool_arguments(self, tool_call: Dict[str, Any]) -> Dict[str, Any]:
317
+ """Extract tool arguments from OpenAI format (handles both Chat Completions and Responses API)."""
318
+ # Check if it's Chat Completions format
319
+ if "function" in tool_call:
320
+ return tool_call.get("function", {}).get("arguments", {})
321
+ # Otherwise assume Responses API format
322
+ arguments = tool_call.get("arguments", {})
323
+ if isinstance(arguments, str):
324
+ try:
325
+ import json
326
+
327
+ return json.loads(arguments)
328
+ except:
329
+ return {}
330
+ return arguments
331
+
332
+ def extract_tool_call_id(self, tool_call: Dict[str, Any]) -> str:
333
+ """Extract tool call ID from OpenAI format (handles both Chat Completions and Responses API)."""
334
+ # For Responses API, use call_id (for tool results), for Chat Completions use id
335
+ return tool_call.get("call_id") or tool_call.get("id") or ""
336
+
337
+ def create_tool_result_message(
338
+ self, tool_call: Dict[str, Any], result_content: str
339
+ ) -> Dict[str, Any]:
340
+ """Create tool result message for OpenAI Responses API format."""
341
+ tool_call_id = self.extract_tool_call_id(tool_call)
342
+ # Use Responses API format directly - no conversion needed
343
+ return {
344
+ "type": "function_call_output",
345
+ "call_id": tool_call_id,
346
+ "output": result_content,
347
+ }
348
+
349
+ def extract_tool_result_content(self, tool_result_message: Dict[str, Any]) -> str:
350
+ """Extract content from OpenAI Responses API tool result message."""
351
+ return tool_result_message.get("output", "")
352
+
353
+ def _convert_to_dict(self, obj) -> Dict[str, Any]:
354
+ """Convert any object to dictionary with multiple fallback methods."""
355
+ try:
356
+ if hasattr(obj, "model_dump"):
357
+ return obj.model_dump()
358
+ elif hasattr(obj, "dict"):
359
+ return obj.dict()
360
+ else:
361
+ return dict(obj)
362
+ except:
363
+ # Final fallback: extract key attributes manually
364
+ return {
365
+ key: getattr(obj, key, None)
366
+ for key in dir(obj)
367
+ if not key.startswith("_") and not callable(getattr(obj, key, None))
368
+ }
369
+
370
+ def estimate_tokens(self, text: str) -> int:
371
+ """Estimate token count for text (rough approximation)."""
372
+ return len(text) // 4
373
+
374
+ def calculate_cost(
375
+ self, input_tokens: int, output_tokens: int, model: str
376
+ ) -> float:
377
+ """Calculate cost for OpenAI token usage (2024-2025 pricing)."""
378
+ model_lower = model.lower()
379
+
380
+ if "gpt-4" in model_lower:
381
+ if "4o-mini" in model_lower:
382
+ input_cost = input_tokens * 0.00015 / 1000
383
+ output_cost = output_tokens * 0.0006 / 1000
384
+ elif "4o" in model_lower:
385
+ input_cost = input_tokens * 0.005 / 1000
386
+ output_cost = output_tokens * 0.020 / 1000
387
+ else:
388
+ input_cost = input_tokens * 0.03 / 1000
389
+ output_cost = output_tokens * 0.06 / 1000
390
+ elif "gpt-3.5" in model_lower:
391
+ input_cost = input_tokens * 0.0005 / 1000
392
+ output_cost = output_tokens * 0.0015 / 1000
393
+ else:
394
+ input_cost = input_tokens * 0.0005 / 1000
395
+ output_cost = output_tokens * 0.0015 / 1000
396
+
397
+ return input_cost + output_cost