cua-agent 0.1.5__py3-none-any.whl → 0.1.17__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 cua-agent might be problematic. Click here for more details.
- agent/__init__.py +3 -4
- agent/core/__init__.py +3 -10
- agent/core/computer_agent.py +207 -32
- agent/core/experiment.py +20 -3
- agent/core/loop.py +78 -120
- agent/core/messages.py +279 -125
- agent/core/telemetry.py +44 -32
- agent/core/types.py +35 -0
- agent/core/visualization.py +197 -0
- agent/providers/anthropic/api/client.py +142 -1
- agent/providers/anthropic/api_handler.py +140 -0
- agent/providers/anthropic/callbacks/__init__.py +5 -0
- agent/providers/anthropic/loop.py +224 -209
- agent/providers/anthropic/messages/manager.py +3 -1
- agent/providers/anthropic/response_handler.py +229 -0
- agent/providers/anthropic/tools/base.py +1 -1
- agent/providers/anthropic/tools/bash.py +0 -97
- agent/providers/anthropic/tools/collection.py +2 -2
- agent/providers/anthropic/tools/computer.py +34 -24
- agent/providers/anthropic/tools/manager.py +2 -2
- agent/providers/anthropic/utils.py +370 -0
- agent/providers/omni/__init__.py +1 -20
- agent/providers/omni/api_handler.py +42 -0
- agent/providers/omni/clients/anthropic.py +4 -0
- agent/providers/omni/image_utils.py +0 -72
- agent/providers/omni/loop.py +497 -607
- agent/providers/omni/parser.py +60 -5
- agent/providers/omni/tools/__init__.py +25 -8
- agent/providers/omni/tools/base.py +29 -0
- agent/providers/omni/tools/bash.py +43 -38
- agent/providers/omni/tools/computer.py +144 -181
- agent/providers/omni/tools/manager.py +26 -48
- agent/providers/omni/types.py +0 -4
- agent/providers/omni/utils.py +225 -144
- {cua_agent-0.1.5.dist-info → cua_agent-0.1.17.dist-info}/METADATA +6 -36
- cua_agent-0.1.17.dist-info/RECORD +63 -0
- agent/core/agent.py +0 -252
- agent/core/base_agent.py +0 -164
- agent/core/factory.py +0 -102
- agent/providers/omni/callbacks.py +0 -78
- agent/providers/omni/clients/groq.py +0 -101
- agent/providers/omni/experiment.py +0 -273
- agent/providers/omni/messages.py +0 -171
- agent/providers/omni/tool_manager.py +0 -91
- agent/providers/omni/visualization.py +0 -130
- agent/types/__init__.py +0 -26
- agent/types/base.py +0 -53
- agent/types/messages.py +0 -36
- cua_agent-0.1.5.dist-info/RECORD +0 -67
- /agent/{types → core}/tools.py +0 -0
- {cua_agent-0.1.5.dist-info → cua_agent-0.1.17.dist-info}/WHEEL +0 -0
- {cua_agent-0.1.5.dist-info → cua_agent-0.1.17.dist-info}/entry_points.txt +0 -0
|
@@ -2,38 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import asyncio
|
|
5
|
-
import json
|
|
6
|
-
import os
|
|
7
5
|
from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple, cast
|
|
8
|
-
import base64
|
|
9
|
-
from datetime import datetime
|
|
10
|
-
from httpx import ConnectError, ReadTimeout
|
|
11
|
-
|
|
12
|
-
# Anthropic-specific imports
|
|
13
|
-
from anthropic import AsyncAnthropic
|
|
14
6
|
from anthropic.types.beta import (
|
|
15
7
|
BetaMessage,
|
|
16
8
|
BetaMessageParam,
|
|
17
9
|
BetaTextBlock,
|
|
18
|
-
|
|
19
|
-
BetaToolUseBlockParam,
|
|
10
|
+
BetaContentBlockParam,
|
|
20
11
|
)
|
|
12
|
+
import base64
|
|
13
|
+
from datetime import datetime
|
|
21
14
|
|
|
22
15
|
# Computer
|
|
23
16
|
from computer import Computer
|
|
24
17
|
|
|
25
18
|
# Base imports
|
|
26
19
|
from ...core.loop import BaseLoop
|
|
27
|
-
from ...core.messages import ImageRetentionConfig
|
|
20
|
+
from ...core.messages import StandardMessageManager, ImageRetentionConfig
|
|
21
|
+
from ...core.types import AgentResponse
|
|
28
22
|
|
|
29
23
|
# Anthropic provider-specific imports
|
|
30
24
|
from .api.client import AnthropicClientFactory, BaseAnthropicClient
|
|
31
25
|
from .tools.manager import ToolManager
|
|
32
|
-
from .messages.manager import MessageManager
|
|
33
|
-
from .callbacks.manager import CallbackManager
|
|
34
26
|
from .prompts import SYSTEM_PROMPT
|
|
35
27
|
from .types import LLMProvider
|
|
36
28
|
from .tools import ToolResult
|
|
29
|
+
from .utils import to_anthropic_format, to_agent_response_format
|
|
30
|
+
|
|
31
|
+
# Import the new modules we created
|
|
32
|
+
from .api_handler import AnthropicAPIHandler
|
|
33
|
+
from .response_handler import AnthropicResponseHandler
|
|
34
|
+
from .callbacks.manager import CallbackManager
|
|
37
35
|
|
|
38
36
|
# Constants
|
|
39
37
|
COMPUTER_USE_BETA_FLAG = "computer-use-2025-01-24"
|
|
@@ -43,13 +41,22 @@ logger = logging.getLogger(__name__)
|
|
|
43
41
|
|
|
44
42
|
|
|
45
43
|
class AnthropicLoop(BaseLoop):
|
|
46
|
-
"""Anthropic-specific implementation of the agent loop.
|
|
44
|
+
"""Anthropic-specific implementation of the agent loop.
|
|
45
|
+
|
|
46
|
+
This class extends BaseLoop to provide specialized support for Anthropic's Claude models
|
|
47
|
+
with their unique tool-use capabilities, custom message formatting, and
|
|
48
|
+
callback-driven approach to handling responses.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
###########################################
|
|
52
|
+
# INITIALIZATION AND CONFIGURATION
|
|
53
|
+
###########################################
|
|
47
54
|
|
|
48
55
|
def __init__(
|
|
49
56
|
self,
|
|
50
57
|
api_key: str,
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
computer: Computer,
|
|
59
|
+
model: str = "claude-3-7-sonnet-20250219",
|
|
53
60
|
only_n_most_recent_images: Optional[int] = 2,
|
|
54
61
|
base_dir: Optional[str] = "trajectories",
|
|
55
62
|
max_retries: int = 3,
|
|
@@ -69,7 +76,7 @@ class AnthropicLoop(BaseLoop):
|
|
|
69
76
|
retry_delay: Delay between retries in seconds
|
|
70
77
|
save_trajectory: Whether to save trajectory data
|
|
71
78
|
"""
|
|
72
|
-
# Initialize base class
|
|
79
|
+
# Initialize base class with core config
|
|
73
80
|
super().__init__(
|
|
74
81
|
computer=computer,
|
|
75
82
|
model=model,
|
|
@@ -82,27 +89,33 @@ class AnthropicLoop(BaseLoop):
|
|
|
82
89
|
**kwargs,
|
|
83
90
|
)
|
|
84
91
|
|
|
85
|
-
#
|
|
86
|
-
self.
|
|
92
|
+
# Initialize message manager
|
|
93
|
+
self.message_manager = StandardMessageManager(
|
|
94
|
+
config=ImageRetentionConfig(num_images_to_keep=only_n_most_recent_images)
|
|
95
|
+
)
|
|
87
96
|
|
|
88
97
|
# Anthropic-specific attributes
|
|
89
98
|
self.provider = LLMProvider.ANTHROPIC
|
|
90
99
|
self.client = None
|
|
91
100
|
self.retry_count = 0
|
|
92
101
|
self.tool_manager = None
|
|
93
|
-
self.message_manager = None
|
|
94
102
|
self.callback_manager = None
|
|
103
|
+
self.queue = asyncio.Queue() # Initialize queue
|
|
95
104
|
|
|
96
|
-
#
|
|
97
|
-
self.
|
|
98
|
-
|
|
99
|
-
)
|
|
105
|
+
# Initialize handlers
|
|
106
|
+
self.api_handler = AnthropicAPIHandler(self)
|
|
107
|
+
self.response_handler = AnthropicResponseHandler(self)
|
|
100
108
|
|
|
101
|
-
|
|
102
|
-
|
|
109
|
+
###########################################
|
|
110
|
+
# CLIENT INITIALIZATION - IMPLEMENTING ABSTRACT METHOD
|
|
111
|
+
###########################################
|
|
103
112
|
|
|
104
113
|
async def initialize_client(self) -> None:
|
|
105
|
-
"""Initialize the Anthropic API client and tools.
|
|
114
|
+
"""Initialize the Anthropic API client and tools.
|
|
115
|
+
|
|
116
|
+
Implements abstract method from BaseLoop to set up the Anthropic-specific
|
|
117
|
+
client, tool manager, message manager, and callback handlers.
|
|
118
|
+
"""
|
|
106
119
|
try:
|
|
107
120
|
logger.info(f"Initializing Anthropic client with model {self.model}...")
|
|
108
121
|
|
|
@@ -111,14 +124,7 @@ class AnthropicLoop(BaseLoop):
|
|
|
111
124
|
provider=self.provider, api_key=self.api_key, model=self.model
|
|
112
125
|
)
|
|
113
126
|
|
|
114
|
-
# Initialize
|
|
115
|
-
self.message_manager = MessageManager(
|
|
116
|
-
ImageRetentionConfig(
|
|
117
|
-
num_images_to_keep=self.only_n_most_recent_images, enable_caching=True
|
|
118
|
-
)
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
# Initialize callback manager
|
|
127
|
+
# Initialize callback manager with our callback handlers
|
|
122
128
|
self.callback_manager = CallbackManager(
|
|
123
129
|
content_callback=self._handle_content,
|
|
124
130
|
tool_callback=self._handle_tool_result,
|
|
@@ -135,62 +141,22 @@ class AnthropicLoop(BaseLoop):
|
|
|
135
141
|
self.client = None
|
|
136
142
|
raise RuntimeError(f"Failed to initialize Anthropic client: {str(e)}")
|
|
137
143
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
"""Process screen information and add to messages.
|
|
142
|
-
|
|
143
|
-
Args:
|
|
144
|
-
parsed_screen: Dictionary containing parsed screen info
|
|
145
|
-
messages: List of messages to update
|
|
146
|
-
"""
|
|
147
|
-
try:
|
|
148
|
-
# Extract screenshot from parsed screen
|
|
149
|
-
screenshot_base64 = parsed_screen.get("screenshot_base64")
|
|
150
|
-
|
|
151
|
-
if screenshot_base64:
|
|
152
|
-
# Remove data URL prefix if present
|
|
153
|
-
if "," in screenshot_base64:
|
|
154
|
-
screenshot_base64 = screenshot_base64.split(",")[1]
|
|
155
|
-
|
|
156
|
-
# Create Anthropic-compatible message with image
|
|
157
|
-
screen_info_msg = {
|
|
158
|
-
"role": "user",
|
|
159
|
-
"content": [
|
|
160
|
-
{
|
|
161
|
-
"type": "image",
|
|
162
|
-
"source": {
|
|
163
|
-
"type": "base64",
|
|
164
|
-
"media_type": "image/png",
|
|
165
|
-
"data": screenshot_base64,
|
|
166
|
-
},
|
|
167
|
-
}
|
|
168
|
-
],
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
# Add screen info message to messages
|
|
172
|
-
messages.append(screen_info_msg)
|
|
173
|
-
|
|
174
|
-
except Exception as e:
|
|
175
|
-
logger.error(f"Error processing screen info: {str(e)}")
|
|
176
|
-
raise
|
|
144
|
+
###########################################
|
|
145
|
+
# MAIN LOOP - IMPLEMENTING ABSTRACT METHOD
|
|
146
|
+
###########################################
|
|
177
147
|
|
|
178
|
-
async def run(self, messages: List[Dict[str, Any]]) -> AsyncGenerator[
|
|
148
|
+
async def run(self, messages: List[Dict[str, Any]]) -> AsyncGenerator[AgentResponse, None]:
|
|
179
149
|
"""Run the agent loop with provided messages.
|
|
180
150
|
|
|
181
151
|
Args:
|
|
182
|
-
messages: List of message objects
|
|
152
|
+
messages: List of message objects in standard OpenAI format
|
|
183
153
|
|
|
184
154
|
Yields:
|
|
185
|
-
|
|
155
|
+
Agent response format
|
|
186
156
|
"""
|
|
187
157
|
try:
|
|
188
158
|
logger.info("Starting Anthropic loop run")
|
|
189
159
|
|
|
190
|
-
# Reset message history and add new messages
|
|
191
|
-
self.message_history = []
|
|
192
|
-
self.message_history.extend(messages)
|
|
193
|
-
|
|
194
160
|
# Create queue for response streaming
|
|
195
161
|
queue = asyncio.Queue()
|
|
196
162
|
|
|
@@ -203,7 +169,7 @@ class AnthropicLoop(BaseLoop):
|
|
|
203
169
|
logger.info("Client initialized successfully")
|
|
204
170
|
|
|
205
171
|
# Start loop in background task
|
|
206
|
-
loop_task = asyncio.create_task(self._run_loop(queue))
|
|
172
|
+
loop_task = asyncio.create_task(self._run_loop(queue, messages))
|
|
207
173
|
|
|
208
174
|
# Process and yield messages as they arrive
|
|
209
175
|
while True:
|
|
@@ -235,33 +201,87 @@ class AnthropicLoop(BaseLoop):
|
|
|
235
201
|
"metadata": {"title": "❌ Error"},
|
|
236
202
|
}
|
|
237
203
|
|
|
238
|
-
|
|
239
|
-
|
|
204
|
+
###########################################
|
|
205
|
+
# AGENT LOOP IMPLEMENTATION
|
|
206
|
+
###########################################
|
|
207
|
+
|
|
208
|
+
async def _run_loop(self, queue: asyncio.Queue, messages: List[Dict[str, Any]]) -> None:
|
|
209
|
+
"""Run the agent loop with provided messages.
|
|
240
210
|
|
|
241
211
|
Args:
|
|
242
212
|
queue: Queue for response streaming
|
|
213
|
+
messages: List of messages in standard OpenAI format
|
|
243
214
|
"""
|
|
244
215
|
try:
|
|
245
216
|
while True:
|
|
246
|
-
#
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
217
|
+
# Capture screenshot
|
|
218
|
+
try:
|
|
219
|
+
# Take screenshot - always returns raw PNG bytes
|
|
220
|
+
screenshot = await self.computer.interface.screenshot()
|
|
221
|
+
logger.info("Screenshot captured successfully")
|
|
222
|
+
|
|
223
|
+
# Convert PNG bytes to base64
|
|
224
|
+
base64_image = base64.b64encode(screenshot).decode("utf-8")
|
|
225
|
+
logger.info(f"Screenshot converted to base64 (size: {len(base64_image)} bytes)")
|
|
226
|
+
|
|
227
|
+
# Save screenshot if requested
|
|
228
|
+
if self.save_trajectory and self.experiment_manager:
|
|
229
|
+
try:
|
|
230
|
+
self._save_screenshot(base64_image, action_type="state")
|
|
231
|
+
logger.info("Screenshot saved to trajectory")
|
|
232
|
+
except Exception as e:
|
|
233
|
+
logger.error(f"Error saving screenshot: {str(e)}")
|
|
234
|
+
|
|
235
|
+
# Create screenshot message
|
|
236
|
+
screen_info_msg = {
|
|
237
|
+
"role": "user",
|
|
238
|
+
"content": [
|
|
239
|
+
{
|
|
240
|
+
"type": "image",
|
|
241
|
+
"source": {
|
|
242
|
+
"type": "base64",
|
|
243
|
+
"media_type": "image/png",
|
|
244
|
+
"data": base64_image,
|
|
245
|
+
},
|
|
246
|
+
}
|
|
247
|
+
],
|
|
248
|
+
}
|
|
249
|
+
# Add screenshot to messages
|
|
250
|
+
messages.append(screen_info_msg)
|
|
251
|
+
logger.info("Screenshot message added to conversation")
|
|
251
252
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
)
|
|
253
|
+
except Exception as e:
|
|
254
|
+
logger.error(f"Error capturing or processing screenshot: {str(e)}")
|
|
255
|
+
raise
|
|
256
256
|
|
|
257
257
|
# Create new turn directory for this API call
|
|
258
258
|
self._create_turn_dir()
|
|
259
259
|
|
|
260
|
-
#
|
|
261
|
-
|
|
260
|
+
# Convert standard messages to Anthropic format using utility function
|
|
261
|
+
anthropic_messages, system_content = to_anthropic_format(messages.copy())
|
|
262
|
+
|
|
263
|
+
# Use API handler to make API call with Anthropic format
|
|
264
|
+
response = await self.api_handler.make_api_call(
|
|
265
|
+
messages=cast(List[BetaMessageParam], anthropic_messages),
|
|
266
|
+
system_prompt=system_content or SYSTEM_PROMPT,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# Use response handler to handle the response and get new messages
|
|
270
|
+
new_messages, should_continue = await self.response_handler.handle_response(
|
|
271
|
+
response, messages
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# Add new messages to the parent's message history
|
|
275
|
+
messages.extend(new_messages)
|
|
276
|
+
|
|
277
|
+
openai_compatible_response = await to_agent_response_format(
|
|
278
|
+
response,
|
|
279
|
+
messages,
|
|
280
|
+
model=self.model,
|
|
281
|
+
)
|
|
282
|
+
await queue.put(openai_compatible_response)
|
|
262
283
|
|
|
263
|
-
|
|
264
|
-
if not await self._handle_response(response, self.message_history):
|
|
284
|
+
if not should_continue:
|
|
265
285
|
break
|
|
266
286
|
|
|
267
287
|
# Signal completion
|
|
@@ -278,123 +298,101 @@ class AnthropicLoop(BaseLoop):
|
|
|
278
298
|
)
|
|
279
299
|
await queue.put(None)
|
|
280
300
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
Args:
|
|
285
|
-
messages: List of messages to send to the API
|
|
286
|
-
|
|
287
|
-
Returns:
|
|
288
|
-
API response
|
|
289
|
-
"""
|
|
290
|
-
last_error = None
|
|
291
|
-
|
|
292
|
-
for attempt in range(self.max_retries):
|
|
293
|
-
try:
|
|
294
|
-
# Log request
|
|
295
|
-
request_data = {
|
|
296
|
-
"messages": messages,
|
|
297
|
-
"max_tokens": self.max_tokens,
|
|
298
|
-
"system": SYSTEM_PROMPT,
|
|
299
|
-
}
|
|
300
|
-
self._log_api_call("request", request_data)
|
|
301
|
-
|
|
302
|
-
# Setup betas and system
|
|
303
|
-
system = BetaTextBlockParam(
|
|
304
|
-
type="text",
|
|
305
|
-
text=SYSTEM_PROMPT,
|
|
306
|
-
)
|
|
307
|
-
|
|
308
|
-
betas = [COMPUTER_USE_BETA_FLAG]
|
|
309
|
-
# Temporarily disable prompt caching due to "A maximum of 4 blocks with cache_control may be provided" error
|
|
310
|
-
# if self.message_manager.image_retention_config.enable_caching:
|
|
311
|
-
# betas.append(PROMPT_CACHING_BETA_FLAG)
|
|
312
|
-
# system["cache_control"] = {"type": "ephemeral"}
|
|
313
|
-
|
|
314
|
-
# Make API call
|
|
315
|
-
response = await self.client.create_message(
|
|
316
|
-
messages=messages,
|
|
317
|
-
system=[system],
|
|
318
|
-
tools=self.tool_manager.get_tool_params(),
|
|
319
|
-
max_tokens=self.max_tokens,
|
|
320
|
-
betas=betas,
|
|
321
|
-
)
|
|
322
|
-
|
|
323
|
-
# Log success response
|
|
324
|
-
self._log_api_call("response", request_data, response)
|
|
325
|
-
|
|
326
|
-
return response
|
|
327
|
-
except Exception as e:
|
|
328
|
-
last_error = e
|
|
329
|
-
logger.error(
|
|
330
|
-
f"Error in API call (attempt {attempt + 1}/{self.max_retries}): {str(e)}"
|
|
331
|
-
)
|
|
332
|
-
self._log_api_call("error", {"messages": messages}, error=e)
|
|
333
|
-
|
|
334
|
-
if attempt < self.max_retries - 1:
|
|
335
|
-
await asyncio.sleep(self.retry_delay * (attempt + 1)) # Exponential backoff
|
|
336
|
-
continue
|
|
337
|
-
|
|
338
|
-
# If we get here, all retries failed
|
|
339
|
-
error_message = f"API call failed after {self.max_retries} attempts"
|
|
340
|
-
if last_error:
|
|
341
|
-
error_message += f": {str(last_error)}"
|
|
342
|
-
|
|
343
|
-
logger.error(error_message)
|
|
344
|
-
raise RuntimeError(error_message)
|
|
301
|
+
###########################################
|
|
302
|
+
# RESPONSE AND CALLBACK HANDLING
|
|
303
|
+
###########################################
|
|
345
304
|
|
|
346
305
|
async def _handle_response(self, response: BetaMessage, messages: List[Dict[str, Any]]) -> bool:
|
|
347
|
-
"""Handle the Anthropic API
|
|
306
|
+
"""Handle a response from the Anthropic API.
|
|
348
307
|
|
|
349
308
|
Args:
|
|
350
|
-
response:
|
|
351
|
-
messages:
|
|
309
|
+
response: The response from the Anthropic API
|
|
310
|
+
messages: The message history
|
|
352
311
|
|
|
353
312
|
Returns:
|
|
354
|
-
|
|
313
|
+
bool: Whether to continue the conversation
|
|
355
314
|
"""
|
|
356
315
|
try:
|
|
357
|
-
# Convert response to
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
{
|
|
363
|
-
"role": "assistant",
|
|
364
|
-
"content": response_params,
|
|
365
|
-
}
|
|
316
|
+
# Convert response to standard format
|
|
317
|
+
openai_compatible_response = await to_agent_response_format(
|
|
318
|
+
response,
|
|
319
|
+
messages,
|
|
320
|
+
model=self.model,
|
|
366
321
|
)
|
|
367
322
|
|
|
368
|
-
#
|
|
323
|
+
# Put the response on the queue
|
|
324
|
+
await self.queue.put(openai_compatible_response)
|
|
325
|
+
|
|
326
|
+
if self.callback_manager is None:
|
|
327
|
+
raise RuntimeError(
|
|
328
|
+
"Callback manager not initialized. Call initialize_client() first."
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
# Handle tool use blocks and collect ALL results before adding to messages
|
|
369
332
|
tool_result_content = []
|
|
370
|
-
|
|
333
|
+
has_tool_use = False
|
|
334
|
+
|
|
335
|
+
for content_block in response.content:
|
|
371
336
|
# Notify callback of content
|
|
372
|
-
self.callback_manager.on_content(content_block)
|
|
337
|
+
self.callback_manager.on_content(cast(BetaContentBlockParam, content_block))
|
|
338
|
+
|
|
339
|
+
# Handle tool use - carefully check and access attributes
|
|
340
|
+
if hasattr(content_block, "type") and content_block.type == "tool_use":
|
|
341
|
+
has_tool_use = True
|
|
342
|
+
if self.tool_manager is None:
|
|
343
|
+
raise RuntimeError(
|
|
344
|
+
"Tool manager not initialized. Call initialize_client() first."
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
# Safely get attributes
|
|
348
|
+
tool_name = getattr(content_block, "name", "")
|
|
349
|
+
tool_input = getattr(content_block, "input", {})
|
|
350
|
+
tool_id = getattr(content_block, "id", "")
|
|
373
351
|
|
|
374
|
-
# Handle tool use
|
|
375
|
-
if content_block.get("type") == "tool_use":
|
|
376
352
|
result = await self.tool_manager.execute_tool(
|
|
377
|
-
name=
|
|
378
|
-
tool_input=cast(Dict[str, Any],
|
|
353
|
+
name=tool_name,
|
|
354
|
+
tool_input=cast(Dict[str, Any], tool_input),
|
|
379
355
|
)
|
|
380
356
|
|
|
381
|
-
# Create tool result
|
|
382
|
-
tool_result = self._make_tool_result(result,
|
|
357
|
+
# Create tool result
|
|
358
|
+
tool_result = self._make_tool_result(cast(ToolResult, result), tool_id)
|
|
383
359
|
tool_result_content.append(tool_result)
|
|
384
360
|
|
|
385
361
|
# Notify callback of tool result
|
|
386
|
-
self.callback_manager.on_tool_result(result,
|
|
387
|
-
|
|
388
|
-
# If
|
|
389
|
-
if
|
|
390
|
-
|
|
362
|
+
self.callback_manager.on_tool_result(cast(ToolResult, result), tool_id)
|
|
363
|
+
|
|
364
|
+
# If we had any tool_use blocks, we MUST add the tool_result message
|
|
365
|
+
# even if there were errors or no actual results
|
|
366
|
+
if has_tool_use:
|
|
367
|
+
# If somehow we have no tool results but had tool uses, add synthetic error results
|
|
368
|
+
if not tool_result_content:
|
|
369
|
+
logger.warning(
|
|
370
|
+
"Had tool uses but no tool results, adding synthetic error results"
|
|
371
|
+
)
|
|
372
|
+
for content_block in response.content:
|
|
373
|
+
if hasattr(content_block, "type") and content_block.type == "tool_use":
|
|
374
|
+
tool_id = getattr(content_block, "id", "")
|
|
375
|
+
if tool_id:
|
|
376
|
+
tool_result_content.append(
|
|
377
|
+
{
|
|
378
|
+
"type": "tool_result",
|
|
379
|
+
"tool_use_id": tool_id,
|
|
380
|
+
"content": {
|
|
381
|
+
"type": "error",
|
|
382
|
+
"text": "Tool execution was skipped or failed",
|
|
383
|
+
},
|
|
384
|
+
"is_error": True,
|
|
385
|
+
}
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
# Add ALL tool results as a SINGLE user message
|
|
389
|
+
messages.append({"role": "user", "content": tool_result_content})
|
|
390
|
+
return True
|
|
391
|
+
else:
|
|
392
|
+
# No tool uses, we're done
|
|
391
393
|
self.callback_manager.on_content({"type": "text", "text": "<DONE>"})
|
|
392
394
|
return False
|
|
393
395
|
|
|
394
|
-
# Add tool results to message history
|
|
395
|
-
messages.append({"content": tool_result_content, "role": "user"})
|
|
396
|
-
return True
|
|
397
|
-
|
|
398
396
|
except Exception as e:
|
|
399
397
|
logger.error(f"Error handling response: {str(e)}")
|
|
400
398
|
messages.append(
|
|
@@ -405,28 +403,41 @@ class AnthropicLoop(BaseLoop):
|
|
|
405
403
|
)
|
|
406
404
|
return False
|
|
407
405
|
|
|
408
|
-
def
|
|
409
|
-
|
|
410
|
-
response: BetaMessage,
|
|
411
|
-
) -> List[Dict[str, Any]]:
|
|
412
|
-
"""Convert API response to message parameters.
|
|
406
|
+
def _response_to_blocks(self, response: BetaMessage) -> List[Dict[str, Any]]:
|
|
407
|
+
"""Convert Anthropic API response to standard blocks format.
|
|
413
408
|
|
|
414
409
|
Args:
|
|
415
410
|
response: API response message
|
|
416
411
|
|
|
417
412
|
Returns:
|
|
418
|
-
List of content blocks
|
|
413
|
+
List of content blocks in standard format
|
|
419
414
|
"""
|
|
420
415
|
result = []
|
|
421
416
|
for block in response.content:
|
|
422
417
|
if isinstance(block, BetaTextBlock):
|
|
423
418
|
result.append({"type": "text", "text": block.text})
|
|
419
|
+
elif hasattr(block, "type") and block.type == "tool_use":
|
|
420
|
+
# Safely access attributes after confirming it's a tool_use
|
|
421
|
+
result.append(
|
|
422
|
+
{
|
|
423
|
+
"type": "tool_use",
|
|
424
|
+
"id": getattr(block, "id", ""),
|
|
425
|
+
"name": getattr(block, "name", ""),
|
|
426
|
+
"input": getattr(block, "input", {}),
|
|
427
|
+
}
|
|
428
|
+
)
|
|
424
429
|
else:
|
|
425
|
-
|
|
430
|
+
# For other block types, convert to dict
|
|
431
|
+
block_dict = {}
|
|
432
|
+
for key, value in vars(block).items():
|
|
433
|
+
if not key.startswith("_"):
|
|
434
|
+
block_dict[key] = value
|
|
435
|
+
result.append(block_dict)
|
|
436
|
+
|
|
426
437
|
return result
|
|
427
438
|
|
|
428
439
|
def _make_tool_result(self, result: ToolResult, tool_use_id: str) -> Dict[str, Any]:
|
|
429
|
-
"""Convert a tool result to
|
|
440
|
+
"""Convert a tool result to standard format.
|
|
430
441
|
|
|
431
442
|
Args:
|
|
432
443
|
result: Tool execution result
|
|
@@ -465,12 +476,8 @@ class AnthropicLoop(BaseLoop):
|
|
|
465
476
|
if result.base64_image:
|
|
466
477
|
tool_result_content.append(
|
|
467
478
|
{
|
|
468
|
-
"type": "
|
|
469
|
-
"
|
|
470
|
-
"type": "base64",
|
|
471
|
-
"media_type": "image/png",
|
|
472
|
-
"data": result.base64_image,
|
|
473
|
-
},
|
|
479
|
+
"type": "image_url",
|
|
480
|
+
"image_url": {"url": f"data:image/png;base64,{result.base64_image}"},
|
|
474
481
|
}
|
|
475
482
|
)
|
|
476
483
|
|
|
@@ -495,16 +502,19 @@ class AnthropicLoop(BaseLoop):
|
|
|
495
502
|
result_text = f"<s>{result.system}</s>\n{result_text}"
|
|
496
503
|
return result_text
|
|
497
504
|
|
|
498
|
-
|
|
505
|
+
###########################################
|
|
506
|
+
# CALLBACK HANDLERS
|
|
507
|
+
###########################################
|
|
508
|
+
|
|
509
|
+
def _handle_content(self, content):
|
|
499
510
|
"""Handle content updates from the assistant."""
|
|
500
511
|
if content.get("type") == "text":
|
|
501
512
|
text = content.get("text", "")
|
|
502
513
|
if text == "<DONE>":
|
|
503
514
|
return
|
|
504
|
-
|
|
505
515
|
logger.info(f"Assistant: {text}")
|
|
506
516
|
|
|
507
|
-
def _handle_tool_result(self, result
|
|
517
|
+
def _handle_tool_result(self, result, tool_id):
|
|
508
518
|
"""Handle tool execution results."""
|
|
509
519
|
if result.error:
|
|
510
520
|
logger.error(f"Tool {tool_id} error: {result.error}")
|
|
@@ -517,5 +527,10 @@ class AnthropicLoop(BaseLoop):
|
|
|
517
527
|
"""Handle API interactions."""
|
|
518
528
|
if error:
|
|
519
529
|
logger.error(f"API error: {error}")
|
|
530
|
+
self._log_api_call("error", request, error=error)
|
|
520
531
|
else:
|
|
521
532
|
logger.debug(f"API request: {request}")
|
|
533
|
+
if response:
|
|
534
|
+
self._log_api_call("response", request, response)
|
|
535
|
+
else:
|
|
536
|
+
self._log_api_call("request", request)
|
|
@@ -90,7 +90,9 @@ class MessageManager:
|
|
|
90
90
|
blocks_with_cache_control += 1
|
|
91
91
|
# Add cache control to the last content block only
|
|
92
92
|
if content and len(content) > 0:
|
|
93
|
-
content[-1]["cache_control"] =
|
|
93
|
+
content[-1]["cache_control"] = BetaCacheControlEphemeralParam(
|
|
94
|
+
type="ephemeral"
|
|
95
|
+
)
|
|
94
96
|
else:
|
|
95
97
|
# Remove any existing cache control
|
|
96
98
|
if content and len(content) > 0:
|