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.
- massgen/__init__.py +94 -0
- massgen/agent_config.py +507 -0
- massgen/backend/CLAUDE_API_RESEARCH.md +266 -0
- massgen/backend/Function calling openai responses.md +1161 -0
- massgen/backend/GEMINI_API_DOCUMENTATION.md +410 -0
- massgen/backend/OPENAI_RESPONSES_API_FORMAT.md +65 -0
- massgen/backend/__init__.py +25 -0
- massgen/backend/base.py +180 -0
- massgen/backend/chat_completions.py +228 -0
- massgen/backend/claude.py +661 -0
- massgen/backend/gemini.py +652 -0
- massgen/backend/grok.py +187 -0
- massgen/backend/response.py +397 -0
- massgen/chat_agent.py +440 -0
- massgen/cli.py +686 -0
- massgen/configs/README.md +293 -0
- massgen/configs/creative_team.yaml +53 -0
- massgen/configs/gemini_4o_claude.yaml +31 -0
- massgen/configs/news_analysis.yaml +51 -0
- massgen/configs/research_team.yaml +51 -0
- massgen/configs/single_agent.yaml +18 -0
- massgen/configs/single_flash2.5.yaml +44 -0
- massgen/configs/technical_analysis.yaml +51 -0
- massgen/configs/three_agents_default.yaml +31 -0
- massgen/configs/travel_planning.yaml +51 -0
- massgen/configs/two_agents.yaml +39 -0
- massgen/frontend/__init__.py +20 -0
- massgen/frontend/coordination_ui.py +945 -0
- massgen/frontend/displays/__init__.py +24 -0
- massgen/frontend/displays/base_display.py +83 -0
- massgen/frontend/displays/rich_terminal_display.py +3497 -0
- massgen/frontend/displays/simple_display.py +93 -0
- massgen/frontend/displays/terminal_display.py +381 -0
- massgen/frontend/logging/__init__.py +9 -0
- massgen/frontend/logging/realtime_logger.py +197 -0
- massgen/message_templates.py +431 -0
- massgen/orchestrator.py +1222 -0
- massgen/tests/__init__.py +10 -0
- massgen/tests/multi_turn_conversation_design.md +214 -0
- massgen/tests/multiturn_llm_input_analysis.md +189 -0
- massgen/tests/test_case_studies.md +113 -0
- massgen/tests/test_claude_backend.py +310 -0
- massgen/tests/test_grok_backend.py +160 -0
- massgen/tests/test_message_context_building.py +293 -0
- massgen/tests/test_rich_terminal_display.py +378 -0
- massgen/tests/test_v3_3agents.py +117 -0
- massgen/tests/test_v3_simple.py +216 -0
- massgen/tests/test_v3_three_agents.py +272 -0
- massgen/tests/test_v3_two_agents.py +176 -0
- massgen/utils.py +79 -0
- massgen/v1/README.md +330 -0
- massgen/v1/__init__.py +91 -0
- massgen/v1/agent.py +605 -0
- massgen/v1/agents.py +330 -0
- massgen/v1/backends/gemini.py +584 -0
- massgen/v1/backends/grok.py +410 -0
- massgen/v1/backends/oai.py +571 -0
- massgen/v1/cli.py +351 -0
- massgen/v1/config.py +169 -0
- massgen/v1/examples/fast-4o-mini-config.yaml +44 -0
- massgen/v1/examples/fast_config.yaml +44 -0
- massgen/v1/examples/production.yaml +70 -0
- massgen/v1/examples/single_agent.yaml +39 -0
- massgen/v1/logging.py +974 -0
- massgen/v1/main.py +368 -0
- massgen/v1/orchestrator.py +1138 -0
- massgen/v1/streaming_display.py +1190 -0
- massgen/v1/tools.py +160 -0
- massgen/v1/types.py +245 -0
- massgen/v1/utils.py +199 -0
- massgen-0.0.3.dist-info/METADATA +568 -0
- massgen-0.0.3.dist-info/RECORD +76 -0
- massgen-0.0.3.dist-info/WHEEL +5 -0
- massgen-0.0.3.dist-info/entry_points.txt +2 -0
- massgen-0.0.3.dist-info/licenses/LICENSE +204 -0
- massgen-0.0.3.dist-info/top_level.txt +1 -0
massgen/backend/grok.py
ADDED
|
@@ -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
|