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,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()