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
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Claude backend implementation using Anthropic's Messages API.
|
|
5
|
+
Production-ready implementation with full multi-tool support.
|
|
6
|
+
|
|
7
|
+
✅ FEATURES IMPLEMENTED:
|
|
8
|
+
- ✅ Messages API integration with streaming support
|
|
9
|
+
- ✅ Multi-tool support (server-side + user-defined tools combined)
|
|
10
|
+
- ✅ Web search tool integration with pricing tracking
|
|
11
|
+
- ✅ Code execution tool integration with session management
|
|
12
|
+
- ✅ Tool message format conversion for MassGen compatibility
|
|
13
|
+
- ✅ Advanced streaming with tool parameter streaming
|
|
14
|
+
- ✅ Error handling and token usage tracking
|
|
15
|
+
- ✅ Production-ready pricing calculations (2025 rates)
|
|
16
|
+
|
|
17
|
+
Multi-Tool Capabilities:
|
|
18
|
+
- Can combine web search + code execution + user functions in single request
|
|
19
|
+
- No API limitations unlike other providers
|
|
20
|
+
- Parallel and sequential tool execution supported
|
|
21
|
+
- Perfect integration with MassGen StreamChunk pattern
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
import json
|
|
26
|
+
from typing import Dict, List, Any, AsyncGenerator, Optional
|
|
27
|
+
from .base import LLMBackend, StreamChunk
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ClaudeBackend(LLMBackend):
|
|
31
|
+
"""Claude backend using Anthropic's Messages API with full multi-tool support."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, api_key: Optional[str] = None, **kwargs):
|
|
34
|
+
super().__init__(api_key, **kwargs)
|
|
35
|
+
self.api_key = api_key or os.getenv("ANTHROPIC_API_KEY")
|
|
36
|
+
self.search_count = 0 # Track web search usage for pricing
|
|
37
|
+
self.code_session_hours = 0.0 # Track code execution usage
|
|
38
|
+
|
|
39
|
+
def convert_tools_to_claude_format(
|
|
40
|
+
self, tools: List[Dict[str, Any]]
|
|
41
|
+
) -> List[Dict[str, Any]]:
|
|
42
|
+
"""Convert tools to Claude's expected format.
|
|
43
|
+
|
|
44
|
+
Input formats supported:
|
|
45
|
+
- Response API format: {"type": "function", "name": ..., "description": ..., "parameters": ...}
|
|
46
|
+
- Chat Completions format: {"type": "function", "function": {"name": ..., "description": ..., "parameters": ...}}
|
|
47
|
+
|
|
48
|
+
Claude format: {"type": "function", "name": ..., "description": ..., "input_schema": ...}
|
|
49
|
+
"""
|
|
50
|
+
if not tools:
|
|
51
|
+
return tools
|
|
52
|
+
|
|
53
|
+
converted_tools = []
|
|
54
|
+
for tool in tools:
|
|
55
|
+
if tool.get("type") == "function":
|
|
56
|
+
if "function" in tool:
|
|
57
|
+
# Chat Completions format -> Claude custom tool
|
|
58
|
+
func = tool["function"]
|
|
59
|
+
converted_tools.append(
|
|
60
|
+
{
|
|
61
|
+
"type": "custom",
|
|
62
|
+
"name": func["name"],
|
|
63
|
+
"description": func["description"],
|
|
64
|
+
"input_schema": func.get("parameters", {}),
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
elif "name" in tool and "description" in tool:
|
|
68
|
+
# Response API format -> Claude custom tool
|
|
69
|
+
converted_tools.append(
|
|
70
|
+
{
|
|
71
|
+
"type": "custom",
|
|
72
|
+
"name": tool["name"],
|
|
73
|
+
"description": tool["description"],
|
|
74
|
+
"input_schema": tool.get("parameters", {}),
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
else:
|
|
78
|
+
# Unknown format - keep as-is
|
|
79
|
+
converted_tools.append(tool)
|
|
80
|
+
else:
|
|
81
|
+
# Non-function tool (builtin tools) - keep as-is
|
|
82
|
+
converted_tools.append(tool)
|
|
83
|
+
|
|
84
|
+
return converted_tools
|
|
85
|
+
|
|
86
|
+
def convert_messages_to_claude_format(
|
|
87
|
+
self, messages: List[Dict[str, Any]]
|
|
88
|
+
) -> tuple:
|
|
89
|
+
"""Convert messages to Claude's expected format.
|
|
90
|
+
|
|
91
|
+
Handle different tool message formats and extract system message:
|
|
92
|
+
- Chat Completions tool message: {"role": "tool", "tool_call_id": "...", "content": "..."}
|
|
93
|
+
- Response API tool message: {"type": "function_call_output", "call_id": "...", "output": "..."}
|
|
94
|
+
- System messages: Extract and return separately for top-level system parameter
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
tuple: (converted_messages, system_message)
|
|
98
|
+
"""
|
|
99
|
+
converted_messages = []
|
|
100
|
+
system_message = ""
|
|
101
|
+
|
|
102
|
+
for message in messages:
|
|
103
|
+
if message.get("role") == "system":
|
|
104
|
+
# Extract system message for top-level parameter
|
|
105
|
+
system_message = message.get("content", "")
|
|
106
|
+
elif message.get("role") == "tool":
|
|
107
|
+
# Chat Completions tool message -> Claude tool result
|
|
108
|
+
converted_messages.append(
|
|
109
|
+
{
|
|
110
|
+
"role": "user",
|
|
111
|
+
"content": [
|
|
112
|
+
{
|
|
113
|
+
"type": "tool_result",
|
|
114
|
+
"tool_use_id": message.get("tool_call_id"),
|
|
115
|
+
"content": message.get("content", ""),
|
|
116
|
+
}
|
|
117
|
+
],
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
elif message.get("type") == "function_call_output":
|
|
121
|
+
# Response API tool message -> Claude tool result
|
|
122
|
+
converted_messages.append(
|
|
123
|
+
{
|
|
124
|
+
"role": "user",
|
|
125
|
+
"content": [
|
|
126
|
+
{
|
|
127
|
+
"type": "tool_result",
|
|
128
|
+
"tool_use_id": message.get("call_id"),
|
|
129
|
+
"content": message.get("output", ""),
|
|
130
|
+
}
|
|
131
|
+
],
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
elif message.get("role") == "assistant" and "tool_calls" in message:
|
|
135
|
+
# Assistant message with tool calls - convert to Claude format
|
|
136
|
+
content = []
|
|
137
|
+
|
|
138
|
+
# Add text content if present
|
|
139
|
+
if message.get("content"):
|
|
140
|
+
content.append({"type": "text", "text": message["content"]})
|
|
141
|
+
|
|
142
|
+
# Convert tool calls to Claude tool use format
|
|
143
|
+
for tool_call in message["tool_calls"]:
|
|
144
|
+
tool_name = self.extract_tool_name(tool_call)
|
|
145
|
+
tool_args = self.extract_tool_arguments(tool_call)
|
|
146
|
+
tool_id = self.extract_tool_call_id(tool_call)
|
|
147
|
+
|
|
148
|
+
content.append(
|
|
149
|
+
{
|
|
150
|
+
"type": "tool_use",
|
|
151
|
+
"id": tool_id,
|
|
152
|
+
"name": tool_name,
|
|
153
|
+
"input": tool_args,
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
converted_messages.append({"role": "assistant", "content": content})
|
|
158
|
+
elif message.get("role") in ["user", "assistant"]:
|
|
159
|
+
# Keep user and assistant messages, skip system
|
|
160
|
+
converted_message = dict(message)
|
|
161
|
+
if isinstance(converted_message.get("content"), str):
|
|
162
|
+
# Claude expects content to be text for simple messages
|
|
163
|
+
pass # String content is fine
|
|
164
|
+
converted_messages.append(converted_message)
|
|
165
|
+
|
|
166
|
+
return converted_messages, system_message
|
|
167
|
+
|
|
168
|
+
async def stream_with_tools(
|
|
169
|
+
self, messages: List[Dict[str, Any]], tools: List[Dict[str, Any]], **kwargs
|
|
170
|
+
) -> AsyncGenerator[StreamChunk, None]:
|
|
171
|
+
"""Stream response using Claude's Messages API with full multi-tool support."""
|
|
172
|
+
try:
|
|
173
|
+
import anthropic
|
|
174
|
+
|
|
175
|
+
# Initialize client
|
|
176
|
+
client = anthropic.AsyncAnthropic(api_key=self.api_key)
|
|
177
|
+
|
|
178
|
+
# Extract parameters
|
|
179
|
+
model = kwargs.get(
|
|
180
|
+
"model", "claude-3-5-haiku-latest"
|
|
181
|
+
) # Use model that supports code execution
|
|
182
|
+
max_tokens = kwargs.get("max_tokens", 8192)
|
|
183
|
+
temperature = kwargs.get("temperature", None)
|
|
184
|
+
enable_web_search = kwargs.get("enable_web_search", False)
|
|
185
|
+
enable_code_execution = kwargs.get("enable_code_execution", False)
|
|
186
|
+
|
|
187
|
+
# Convert messages to Claude format and extract system message
|
|
188
|
+
converted_messages, system_message = self.convert_messages_to_claude_format(
|
|
189
|
+
messages
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Combine all tool types (Claude's key advantage!)
|
|
193
|
+
combined_tools = []
|
|
194
|
+
|
|
195
|
+
# Add server-side tools if enabled (use correct Claude format)
|
|
196
|
+
if enable_web_search:
|
|
197
|
+
combined_tools.append(
|
|
198
|
+
{"type": "web_search_20250305", "name": "web_search"}
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
if enable_code_execution:
|
|
202
|
+
combined_tools.append(
|
|
203
|
+
{"type": "code_execution_20250522", "name": "code_execution"}
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Add user-defined tools
|
|
207
|
+
if tools:
|
|
208
|
+
converted_tools = self.convert_tools_to_claude_format(tools)
|
|
209
|
+
combined_tools.extend(converted_tools)
|
|
210
|
+
|
|
211
|
+
# Build API parameters
|
|
212
|
+
api_params = {
|
|
213
|
+
"model": model,
|
|
214
|
+
"messages": converted_messages,
|
|
215
|
+
"max_tokens": max_tokens,
|
|
216
|
+
"stream": True,
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if system_message:
|
|
220
|
+
api_params["system"] = system_message
|
|
221
|
+
|
|
222
|
+
if temperature is not None:
|
|
223
|
+
api_params["temperature"] = temperature
|
|
224
|
+
|
|
225
|
+
if combined_tools:
|
|
226
|
+
api_params["tools"] = combined_tools
|
|
227
|
+
|
|
228
|
+
# Set up beta features and create stream
|
|
229
|
+
if enable_code_execution:
|
|
230
|
+
# Code execution requires beta client and beta headers
|
|
231
|
+
api_params["betas"] = ["code-execution-2025-05-22"]
|
|
232
|
+
stream = await client.beta.messages.create(**api_params)
|
|
233
|
+
else:
|
|
234
|
+
# Regular client for non-code-execution requests
|
|
235
|
+
stream = await client.messages.create(**api_params)
|
|
236
|
+
|
|
237
|
+
content = ""
|
|
238
|
+
current_tool_uses = {}
|
|
239
|
+
|
|
240
|
+
async for event in stream:
|
|
241
|
+
try:
|
|
242
|
+
if event.type == "message_start":
|
|
243
|
+
# Message started
|
|
244
|
+
continue
|
|
245
|
+
|
|
246
|
+
elif event.type == "content_block_start":
|
|
247
|
+
# Content block started (text, tool use, or tool result)
|
|
248
|
+
if hasattr(event, "content_block"):
|
|
249
|
+
if event.content_block.type == "tool_use":
|
|
250
|
+
# Regular tool use started (user-defined tools)
|
|
251
|
+
tool_id = event.content_block.id
|
|
252
|
+
tool_name = event.content_block.name
|
|
253
|
+
current_tool_uses[tool_id] = {
|
|
254
|
+
"id": tool_id,
|
|
255
|
+
"name": tool_name,
|
|
256
|
+
"input": "", # Will accumulate JSON fragments
|
|
257
|
+
"index": getattr(event, "index", None),
|
|
258
|
+
}
|
|
259
|
+
elif event.content_block.type == "server_tool_use":
|
|
260
|
+
# Server-side tool use (code execution, web search) - show status immediately
|
|
261
|
+
tool_id = event.content_block.id
|
|
262
|
+
tool_name = event.content_block.name
|
|
263
|
+
current_tool_uses[tool_id] = {
|
|
264
|
+
"id": tool_id,
|
|
265
|
+
"name": tool_name,
|
|
266
|
+
"input": "", # Will accumulate JSON fragments
|
|
267
|
+
"index": getattr(event, "index", None),
|
|
268
|
+
"server_side": True,
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
# Show tool execution starting
|
|
272
|
+
if tool_name == "code_execution":
|
|
273
|
+
yield StreamChunk(
|
|
274
|
+
type="content",
|
|
275
|
+
content=f"\n💻 [Code Execution] Starting...\n",
|
|
276
|
+
)
|
|
277
|
+
elif tool_name == "web_search":
|
|
278
|
+
yield StreamChunk(
|
|
279
|
+
type="content",
|
|
280
|
+
content=f"\n🔍 [Web Search] Starting search...\n",
|
|
281
|
+
)
|
|
282
|
+
elif (
|
|
283
|
+
event.content_block.type == "code_execution_tool_result"
|
|
284
|
+
):
|
|
285
|
+
# Code execution result - format properly
|
|
286
|
+
result_block = event.content_block
|
|
287
|
+
|
|
288
|
+
# Format execution result nicely
|
|
289
|
+
result_parts = []
|
|
290
|
+
if (
|
|
291
|
+
hasattr(result_block, "stdout")
|
|
292
|
+
and result_block.stdout
|
|
293
|
+
):
|
|
294
|
+
result_parts.append(
|
|
295
|
+
f"Output: {result_block.stdout.strip()}"
|
|
296
|
+
)
|
|
297
|
+
if (
|
|
298
|
+
hasattr(result_block, "stderr")
|
|
299
|
+
and result_block.stderr
|
|
300
|
+
):
|
|
301
|
+
result_parts.append(
|
|
302
|
+
f"Error: {result_block.stderr.strip()}"
|
|
303
|
+
)
|
|
304
|
+
if (
|
|
305
|
+
hasattr(result_block, "return_code")
|
|
306
|
+
and result_block.return_code != 0
|
|
307
|
+
):
|
|
308
|
+
result_parts.append(
|
|
309
|
+
f"Exit code: {result_block.return_code}"
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
if result_parts:
|
|
313
|
+
result_text = f"\n💻 [Code Execution Result]\n{chr(10).join(result_parts)}\n"
|
|
314
|
+
yield StreamChunk(
|
|
315
|
+
type="content", content=result_text
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
elif event.type == "content_block_delta":
|
|
319
|
+
# Content streaming
|
|
320
|
+
if hasattr(event, "delta"):
|
|
321
|
+
if event.delta.type == "text_delta":
|
|
322
|
+
# Text content
|
|
323
|
+
text_chunk = event.delta.text
|
|
324
|
+
content += text_chunk
|
|
325
|
+
yield StreamChunk(type="content", content=text_chunk)
|
|
326
|
+
|
|
327
|
+
elif event.delta.type == "input_json_delta":
|
|
328
|
+
# Tool input streaming - accumulate JSON fragments
|
|
329
|
+
if hasattr(event, "index"):
|
|
330
|
+
# Find tool by index
|
|
331
|
+
for tool_id, tool_data in current_tool_uses.items():
|
|
332
|
+
if tool_data.get("index") == event.index:
|
|
333
|
+
# Accumulate partial JSON
|
|
334
|
+
partial_json = getattr(
|
|
335
|
+
event.delta, "partial_json", ""
|
|
336
|
+
)
|
|
337
|
+
tool_data["input"] += partial_json
|
|
338
|
+
break
|
|
339
|
+
|
|
340
|
+
elif event.type == "content_block_stop":
|
|
341
|
+
# Content block completed - check if it was a server-side tool
|
|
342
|
+
if hasattr(event, "index"):
|
|
343
|
+
# Find the tool that just completed
|
|
344
|
+
for tool_id, tool_data in current_tool_uses.items():
|
|
345
|
+
if tool_data.get(
|
|
346
|
+
"index"
|
|
347
|
+
) == event.index and tool_data.get("server_side"):
|
|
348
|
+
tool_name = tool_data.get("name", "")
|
|
349
|
+
|
|
350
|
+
# Parse the accumulated input to show what was executed
|
|
351
|
+
tool_input = tool_data.get("input", "")
|
|
352
|
+
try:
|
|
353
|
+
if tool_input:
|
|
354
|
+
parsed_input = json.loads(tool_input)
|
|
355
|
+
else:
|
|
356
|
+
parsed_input = {}
|
|
357
|
+
except json.JSONDecodeError:
|
|
358
|
+
parsed_input = {"raw_input": tool_input}
|
|
359
|
+
|
|
360
|
+
if tool_name == "code_execution":
|
|
361
|
+
code = parsed_input.get("code", "")
|
|
362
|
+
if code:
|
|
363
|
+
yield StreamChunk(
|
|
364
|
+
type="content",
|
|
365
|
+
content=f"💻 [Code] {code}\n",
|
|
366
|
+
)
|
|
367
|
+
yield StreamChunk(
|
|
368
|
+
type="content",
|
|
369
|
+
content=f"✅ [Code Execution] Completed\n",
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
# Yield builtin tool result immediately
|
|
373
|
+
builtin_result = {
|
|
374
|
+
"id": tool_id,
|
|
375
|
+
"tool_type": "code_execution",
|
|
376
|
+
"status": "completed",
|
|
377
|
+
"code": code,
|
|
378
|
+
"input": parsed_input,
|
|
379
|
+
}
|
|
380
|
+
yield StreamChunk(
|
|
381
|
+
type="builtin_tool_results",
|
|
382
|
+
builtin_tool_results=[builtin_result],
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
elif tool_name == "web_search":
|
|
386
|
+
query = parsed_input.get("query", "")
|
|
387
|
+
if query:
|
|
388
|
+
yield StreamChunk(
|
|
389
|
+
type="content",
|
|
390
|
+
content=f"🔍 [Query] '{query}'\n",
|
|
391
|
+
)
|
|
392
|
+
yield StreamChunk(
|
|
393
|
+
type="content",
|
|
394
|
+
content=f"✅ [Web Search] Completed\n",
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
# Yield builtin tool result immediately
|
|
398
|
+
builtin_result = {
|
|
399
|
+
"id": tool_id,
|
|
400
|
+
"tool_type": "web_search",
|
|
401
|
+
"status": "completed",
|
|
402
|
+
"query": query,
|
|
403
|
+
"input": parsed_input,
|
|
404
|
+
}
|
|
405
|
+
yield StreamChunk(
|
|
406
|
+
type="builtin_tool_results",
|
|
407
|
+
builtin_tool_results=[builtin_result],
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
# Mark this tool as processed so we don't duplicate it later
|
|
411
|
+
tool_data["processed"] = True
|
|
412
|
+
break
|
|
413
|
+
|
|
414
|
+
elif event.type == "message_delta":
|
|
415
|
+
# Message metadata updates (usage, etc.)
|
|
416
|
+
if hasattr(event, "usage"):
|
|
417
|
+
# Track token usage
|
|
418
|
+
pass
|
|
419
|
+
|
|
420
|
+
elif event.type == "message_stop":
|
|
421
|
+
# Message completed - build final response
|
|
422
|
+
|
|
423
|
+
# Handle any completed tool uses
|
|
424
|
+
if current_tool_uses:
|
|
425
|
+
# Separate server-side tools from user-defined tools
|
|
426
|
+
builtin_tool_results = []
|
|
427
|
+
user_tool_calls = []
|
|
428
|
+
|
|
429
|
+
for tool_use in current_tool_uses.values():
|
|
430
|
+
tool_name = tool_use.get("name", "")
|
|
431
|
+
is_server_side = tool_use.get("server_side", False)
|
|
432
|
+
|
|
433
|
+
# Parse accumulated JSON input
|
|
434
|
+
tool_input = tool_use.get("input", "")
|
|
435
|
+
try:
|
|
436
|
+
if tool_input:
|
|
437
|
+
parsed_input = json.loads(tool_input)
|
|
438
|
+
else:
|
|
439
|
+
parsed_input = {}
|
|
440
|
+
except json.JSONDecodeError:
|
|
441
|
+
parsed_input = {"raw_input": tool_input}
|
|
442
|
+
|
|
443
|
+
if is_server_side or tool_name in [
|
|
444
|
+
"web_search",
|
|
445
|
+
"code_execution",
|
|
446
|
+
]:
|
|
447
|
+
# Convert server-side tools to builtin_tool_results
|
|
448
|
+
builtin_result = {
|
|
449
|
+
"id": tool_use["id"],
|
|
450
|
+
"tool_type": tool_name,
|
|
451
|
+
"status": "completed",
|
|
452
|
+
"input": parsed_input,
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
# Add tool-specific data
|
|
456
|
+
if tool_name == "code_execution":
|
|
457
|
+
builtin_result["code"] = parsed_input.get(
|
|
458
|
+
"code", ""
|
|
459
|
+
)
|
|
460
|
+
# Note: actual execution results come via content_block events
|
|
461
|
+
elif tool_name == "web_search":
|
|
462
|
+
builtin_result["query"] = parsed_input.get(
|
|
463
|
+
"query", ""
|
|
464
|
+
)
|
|
465
|
+
# Note: search results come via content_block events
|
|
466
|
+
|
|
467
|
+
builtin_tool_results.append(builtin_result)
|
|
468
|
+
else:
|
|
469
|
+
# User-defined tools that need external execution
|
|
470
|
+
user_tool_calls.append(
|
|
471
|
+
{
|
|
472
|
+
"id": tool_use["id"],
|
|
473
|
+
"type": "function",
|
|
474
|
+
"function": {
|
|
475
|
+
"name": tool_name,
|
|
476
|
+
"arguments": parsed_input,
|
|
477
|
+
},
|
|
478
|
+
}
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
# Only yield builtin tool results that weren't already processed during content_block_stop
|
|
482
|
+
unprocessed_builtin_results = []
|
|
483
|
+
for result in builtin_tool_results:
|
|
484
|
+
tool_id = result.get("id")
|
|
485
|
+
# Check if this tool was already processed during streaming
|
|
486
|
+
tool_data = current_tool_uses.get(tool_id, {})
|
|
487
|
+
if not tool_data.get("processed"):
|
|
488
|
+
unprocessed_builtin_results.append(result)
|
|
489
|
+
|
|
490
|
+
if unprocessed_builtin_results:
|
|
491
|
+
yield StreamChunk(
|
|
492
|
+
type="builtin_tool_results",
|
|
493
|
+
builtin_tool_results=unprocessed_builtin_results,
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
# Yield user tool calls if any
|
|
497
|
+
if user_tool_calls:
|
|
498
|
+
yield StreamChunk(
|
|
499
|
+
type="tool_calls", tool_calls=user_tool_calls
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
# Build complete message with only user tool calls (builtin tools are handled separately)
|
|
503
|
+
complete_message = {
|
|
504
|
+
"role": "assistant",
|
|
505
|
+
"content": content.strip(),
|
|
506
|
+
}
|
|
507
|
+
if user_tool_calls:
|
|
508
|
+
complete_message["tool_calls"] = user_tool_calls
|
|
509
|
+
yield StreamChunk(
|
|
510
|
+
type="complete_message",
|
|
511
|
+
complete_message=complete_message,
|
|
512
|
+
)
|
|
513
|
+
else:
|
|
514
|
+
# Regular text response
|
|
515
|
+
complete_message = {
|
|
516
|
+
"role": "assistant",
|
|
517
|
+
"content": content.strip(),
|
|
518
|
+
}
|
|
519
|
+
yield StreamChunk(
|
|
520
|
+
type="complete_message",
|
|
521
|
+
complete_message=complete_message,
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
# Track usage for pricing
|
|
525
|
+
if enable_web_search:
|
|
526
|
+
self.search_count += 1 # Approximate search usage
|
|
527
|
+
|
|
528
|
+
if enable_code_execution:
|
|
529
|
+
self.code_session_hours += 0.083 # 5 min minimum session
|
|
530
|
+
|
|
531
|
+
yield StreamChunk(type="done")
|
|
532
|
+
return
|
|
533
|
+
|
|
534
|
+
except Exception as event_error:
|
|
535
|
+
yield StreamChunk(
|
|
536
|
+
type="error", error=f"Event processing error: {event_error}"
|
|
537
|
+
)
|
|
538
|
+
continue
|
|
539
|
+
|
|
540
|
+
except Exception as e:
|
|
541
|
+
yield StreamChunk(type="error", error=f"Claude API error: {e}")
|
|
542
|
+
|
|
543
|
+
def get_provider_name(self) -> str:
|
|
544
|
+
"""Get the provider name."""
|
|
545
|
+
return "Claude"
|
|
546
|
+
|
|
547
|
+
def get_supported_builtin_tools(self) -> List[str]:
|
|
548
|
+
"""Get list of builtin tools supported by Claude."""
|
|
549
|
+
return ["web_search", "code_execution"]
|
|
550
|
+
|
|
551
|
+
def extract_tool_name(self, tool_call: Dict[str, Any]) -> str:
|
|
552
|
+
"""Extract tool name from tool call (handles multiple formats)."""
|
|
553
|
+
# Chat Completions format
|
|
554
|
+
if "function" in tool_call:
|
|
555
|
+
return tool_call.get("function", {}).get("name", "unknown")
|
|
556
|
+
# Claude native format
|
|
557
|
+
elif "name" in tool_call:
|
|
558
|
+
return tool_call.get("name", "unknown")
|
|
559
|
+
# Fallback
|
|
560
|
+
return "unknown"
|
|
561
|
+
|
|
562
|
+
def extract_tool_arguments(self, tool_call: Dict[str, Any]) -> Dict[str, Any]:
|
|
563
|
+
"""Extract tool arguments from tool call (handles multiple formats)."""
|
|
564
|
+
# Chat Completions format
|
|
565
|
+
if "function" in tool_call:
|
|
566
|
+
args = tool_call.get("function", {}).get("arguments", {})
|
|
567
|
+
# Claude native format
|
|
568
|
+
elif "input" in tool_call:
|
|
569
|
+
args = tool_call.get("input", {})
|
|
570
|
+
else:
|
|
571
|
+
args = {}
|
|
572
|
+
|
|
573
|
+
# Ensure JSON parsing if needed
|
|
574
|
+
if isinstance(args, str):
|
|
575
|
+
try:
|
|
576
|
+
return json.loads(args)
|
|
577
|
+
except:
|
|
578
|
+
return {}
|
|
579
|
+
return args
|
|
580
|
+
|
|
581
|
+
def extract_tool_call_id(self, tool_call: Dict[str, Any]) -> str:
|
|
582
|
+
"""Extract tool call ID from tool call."""
|
|
583
|
+
return tool_call.get("id") or tool_call.get("call_id") or ""
|
|
584
|
+
|
|
585
|
+
def create_tool_result_message(
|
|
586
|
+
self, tool_call: Dict[str, Any], result_content: str
|
|
587
|
+
) -> Dict[str, Any]:
|
|
588
|
+
"""Create tool result message in Claude's expected format."""
|
|
589
|
+
tool_call_id = self.extract_tool_call_id(tool_call)
|
|
590
|
+
return {
|
|
591
|
+
"role": "user",
|
|
592
|
+
"content": [
|
|
593
|
+
{
|
|
594
|
+
"type": "tool_result",
|
|
595
|
+
"tool_use_id": tool_call_id,
|
|
596
|
+
"content": result_content,
|
|
597
|
+
}
|
|
598
|
+
],
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
def extract_tool_result_content(self, tool_result_message: Dict[str, Any]) -> str:
|
|
602
|
+
"""Extract content from Claude tool result message."""
|
|
603
|
+
content = tool_result_message.get("content", [])
|
|
604
|
+
if isinstance(content, list) and content:
|
|
605
|
+
for item in content:
|
|
606
|
+
if isinstance(item, dict) and item.get("type") == "tool_result":
|
|
607
|
+
return item.get("content", "")
|
|
608
|
+
return ""
|
|
609
|
+
|
|
610
|
+
def estimate_tokens(self, text: str) -> int:
|
|
611
|
+
"""Estimate token count for text (Claude uses ~4 chars per token)."""
|
|
612
|
+
return len(text) // 4
|
|
613
|
+
|
|
614
|
+
def calculate_cost(
|
|
615
|
+
self, input_tokens: int, output_tokens: int, model: str
|
|
616
|
+
) -> float:
|
|
617
|
+
"""Calculate cost for Claude token usage (2025 pricing)."""
|
|
618
|
+
model_lower = model.lower()
|
|
619
|
+
|
|
620
|
+
if "claude-4" in model_lower:
|
|
621
|
+
if "opus" in model_lower:
|
|
622
|
+
# Claude 4 Opus
|
|
623
|
+
input_cost = (input_tokens / 1_000_000) * 15.0
|
|
624
|
+
output_cost = (output_tokens / 1_000_000) * 75.0
|
|
625
|
+
else:
|
|
626
|
+
# Claude 4 Sonnet
|
|
627
|
+
input_cost = (input_tokens / 1_000_000) * 3.0
|
|
628
|
+
output_cost = (output_tokens / 1_000_000) * 15.0
|
|
629
|
+
elif "claude-3.7" in model_lower or "claude-3-7" in model_lower:
|
|
630
|
+
# Claude 3.7 Sonnet
|
|
631
|
+
input_cost = (input_tokens / 1_000_000) * 3.0
|
|
632
|
+
output_cost = (output_tokens / 1_000_000) * 15.0
|
|
633
|
+
elif "claude-3.5" in model_lower or "claude-3-5" in model_lower:
|
|
634
|
+
if "haiku" in model_lower:
|
|
635
|
+
# Claude 3.5 Haiku
|
|
636
|
+
input_cost = (input_tokens / 1_000_000) * 1.0
|
|
637
|
+
output_cost = (output_tokens / 1_000_000) * 5.0
|
|
638
|
+
else:
|
|
639
|
+
# Claude 3.5 Sonnet (legacy)
|
|
640
|
+
input_cost = (input_tokens / 1_000_000) * 3.0
|
|
641
|
+
output_cost = (output_tokens / 1_000_000) * 15.0
|
|
642
|
+
else:
|
|
643
|
+
# Default fallback (assume Claude 4 Sonnet pricing)
|
|
644
|
+
input_cost = (input_tokens / 1_000_000) * 3.0
|
|
645
|
+
output_cost = (output_tokens / 1_000_000) * 15.0
|
|
646
|
+
|
|
647
|
+
# Add tool usage costs
|
|
648
|
+
tool_costs = 0.0
|
|
649
|
+
if self.search_count > 0:
|
|
650
|
+
tool_costs += (self.search_count / 1000) * 10.0 # $10 per 1,000 searches
|
|
651
|
+
|
|
652
|
+
if self.code_session_hours > 0:
|
|
653
|
+
tool_costs += self.code_session_hours * 0.05 # $0.05 per session-hour
|
|
654
|
+
|
|
655
|
+
return input_cost + output_cost + tool_costs
|
|
656
|
+
|
|
657
|
+
def reset_tool_usage(self):
|
|
658
|
+
"""Reset tool usage tracking."""
|
|
659
|
+
self.search_count = 0
|
|
660
|
+
self.code_session_hours = 0.0
|
|
661
|
+
super().reset_token_usage()
|