cua-agent 0.1.6__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 -2
- agent/core/__init__.py +0 -5
- agent/core/computer_agent.py +21 -28
- agent/core/loop.py +78 -124
- agent/core/messages.py +279 -125
- 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 +206 -220
- agent/providers/anthropic/response_handler.py +229 -0
- agent/providers/anthropic/tools/bash.py +0 -97
- 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 +490 -606
- agent/providers/omni/parser.py +58 -4
- agent/providers/omni/tools/__init__.py +25 -7
- agent/providers/omni/tools/base.py +29 -0
- agent/providers/omni/tools/bash.py +43 -38
- agent/providers/omni/tools/computer.py +144 -182
- agent/providers/omni/tools/manager.py +25 -45
- agent/providers/omni/types.py +0 -4
- agent/providers/omni/utils.py +224 -145
- {cua_agent-0.1.6.dist-info → cua_agent-0.1.17.dist-info}/METADATA +6 -36
- cua_agent-0.1.17.dist-info/RECORD +63 -0
- agent/providers/omni/callbacks.py +0 -78
- agent/providers/omni/clients/groq.py +0 -101
- agent/providers/omni/experiment.py +0 -276
- 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 -23
- agent/types/base.py +0 -41
- agent/types/messages.py +0 -36
- cua_agent-0.1.6.dist-info/RECORD +0 -64
- /agent/{types → core}/tools.py +0 -0
- {cua_agent-0.1.6.dist-info → cua_agent-0.1.17.dist-info}/WHEEL +0 -0
- {cua_agent-0.1.6.dist-info → cua_agent-0.1.17.dist-info}/entry_points.txt +0 -0
|
@@ -2,39 +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
|
-
BetaTextBlockParam,
|
|
19
|
-
BetaToolUseBlockParam,
|
|
20
10
|
BetaContentBlockParam,
|
|
21
11
|
)
|
|
12
|
+
import base64
|
|
13
|
+
from datetime import datetime
|
|
22
14
|
|
|
23
15
|
# Computer
|
|
24
16
|
from computer import Computer
|
|
25
17
|
|
|
26
18
|
# Base imports
|
|
27
19
|
from ...core.loop import BaseLoop
|
|
28
|
-
from ...core.messages import ImageRetentionConfig
|
|
20
|
+
from ...core.messages import StandardMessageManager, ImageRetentionConfig
|
|
21
|
+
from ...core.types import AgentResponse
|
|
29
22
|
|
|
30
23
|
# Anthropic provider-specific imports
|
|
31
24
|
from .api.client import AnthropicClientFactory, BaseAnthropicClient
|
|
32
25
|
from .tools.manager import ToolManager
|
|
33
|
-
from .messages.manager import MessageManager, ImageRetentionConfig
|
|
34
|
-
from .callbacks.manager import CallbackManager
|
|
35
26
|
from .prompts import SYSTEM_PROMPT
|
|
36
27
|
from .types import LLMProvider
|
|
37
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
|
|
38
35
|
|
|
39
36
|
# Constants
|
|
40
37
|
COMPUTER_USE_BETA_FLAG = "computer-use-2025-01-24"
|
|
@@ -44,13 +41,22 @@ logger = logging.getLogger(__name__)
|
|
|
44
41
|
|
|
45
42
|
|
|
46
43
|
class AnthropicLoop(BaseLoop):
|
|
47
|
-
"""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
|
+
###########################################
|
|
48
54
|
|
|
49
55
|
def __init__(
|
|
50
56
|
self,
|
|
51
57
|
api_key: str,
|
|
52
58
|
computer: Computer,
|
|
53
|
-
model: str = "claude-3-7-sonnet-20250219",
|
|
59
|
+
model: str = "claude-3-7-sonnet-20250219",
|
|
54
60
|
only_n_most_recent_images: Optional[int] = 2,
|
|
55
61
|
base_dir: Optional[str] = "trajectories",
|
|
56
62
|
max_retries: int = 3,
|
|
@@ -83,27 +89,33 @@ class AnthropicLoop(BaseLoop):
|
|
|
83
89
|
**kwargs,
|
|
84
90
|
)
|
|
85
91
|
|
|
86
|
-
#
|
|
87
|
-
self.
|
|
92
|
+
# Initialize message manager
|
|
93
|
+
self.message_manager = StandardMessageManager(
|
|
94
|
+
config=ImageRetentionConfig(num_images_to_keep=only_n_most_recent_images)
|
|
95
|
+
)
|
|
88
96
|
|
|
89
97
|
# Anthropic-specific attributes
|
|
90
98
|
self.provider = LLMProvider.ANTHROPIC
|
|
91
99
|
self.client = None
|
|
92
100
|
self.retry_count = 0
|
|
93
101
|
self.tool_manager = None
|
|
94
|
-
self.message_manager = None
|
|
95
102
|
self.callback_manager = None
|
|
103
|
+
self.queue = asyncio.Queue() # Initialize queue
|
|
96
104
|
|
|
97
|
-
#
|
|
98
|
-
self.
|
|
99
|
-
|
|
100
|
-
)
|
|
105
|
+
# Initialize handlers
|
|
106
|
+
self.api_handler = AnthropicAPIHandler(self)
|
|
107
|
+
self.response_handler = AnthropicResponseHandler(self)
|
|
101
108
|
|
|
102
|
-
|
|
103
|
-
|
|
109
|
+
###########################################
|
|
110
|
+
# CLIENT INITIALIZATION - IMPLEMENTING ABSTRACT METHOD
|
|
111
|
+
###########################################
|
|
104
112
|
|
|
105
113
|
async def initialize_client(self) -> None:
|
|
106
|
-
"""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
|
+
"""
|
|
107
119
|
try:
|
|
108
120
|
logger.info(f"Initializing Anthropic client with model {self.model}...")
|
|
109
121
|
|
|
@@ -112,14 +124,7 @@ class AnthropicLoop(BaseLoop):
|
|
|
112
124
|
provider=self.provider, api_key=self.api_key, model=self.model
|
|
113
125
|
)
|
|
114
126
|
|
|
115
|
-
# Initialize
|
|
116
|
-
self.message_manager = MessageManager(
|
|
117
|
-
image_retention_config=ImageRetentionConfig(
|
|
118
|
-
num_images_to_keep=self.only_n_most_recent_images, enable_caching=True
|
|
119
|
-
)
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
# Initialize callback manager
|
|
127
|
+
# Initialize callback manager with our callback handlers
|
|
123
128
|
self.callback_manager = CallbackManager(
|
|
124
129
|
content_callback=self._handle_content,
|
|
125
130
|
tool_callback=self._handle_tool_result,
|
|
@@ -136,62 +141,22 @@ class AnthropicLoop(BaseLoop):
|
|
|
136
141
|
self.client = None
|
|
137
142
|
raise RuntimeError(f"Failed to initialize Anthropic client: {str(e)}")
|
|
138
143
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
"""Process screen information and add to messages.
|
|
144
|
+
###########################################
|
|
145
|
+
# MAIN LOOP - IMPLEMENTING ABSTRACT METHOD
|
|
146
|
+
###########################################
|
|
143
147
|
|
|
144
|
-
|
|
145
|
-
parsed_screen: Dictionary containing parsed screen info
|
|
146
|
-
messages: List of messages to update
|
|
147
|
-
"""
|
|
148
|
-
try:
|
|
149
|
-
# Extract screenshot from parsed screen
|
|
150
|
-
screenshot_base64 = parsed_screen.get("screenshot_base64")
|
|
151
|
-
|
|
152
|
-
if screenshot_base64:
|
|
153
|
-
# Remove data URL prefix if present
|
|
154
|
-
if "," in screenshot_base64:
|
|
155
|
-
screenshot_base64 = screenshot_base64.split(",")[1]
|
|
156
|
-
|
|
157
|
-
# Create Anthropic-compatible message with image
|
|
158
|
-
screen_info_msg = {
|
|
159
|
-
"role": "user",
|
|
160
|
-
"content": [
|
|
161
|
-
{
|
|
162
|
-
"type": "image",
|
|
163
|
-
"source": {
|
|
164
|
-
"type": "base64",
|
|
165
|
-
"media_type": "image/png",
|
|
166
|
-
"data": screenshot_base64,
|
|
167
|
-
},
|
|
168
|
-
}
|
|
169
|
-
],
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
# Add screen info message to messages
|
|
173
|
-
messages.append(screen_info_msg)
|
|
174
|
-
|
|
175
|
-
except Exception as e:
|
|
176
|
-
logger.error(f"Error processing screen info: {str(e)}")
|
|
177
|
-
raise
|
|
178
|
-
|
|
179
|
-
async def run(self, messages: List[Dict[str, Any]]) -> AsyncGenerator[Dict[str, Any], None]:
|
|
148
|
+
async def run(self, messages: List[Dict[str, Any]]) -> AsyncGenerator[AgentResponse, None]:
|
|
180
149
|
"""Run the agent loop with provided messages.
|
|
181
150
|
|
|
182
151
|
Args:
|
|
183
|
-
messages: List of message objects
|
|
152
|
+
messages: List of message objects in standard OpenAI format
|
|
184
153
|
|
|
185
154
|
Yields:
|
|
186
|
-
|
|
155
|
+
Agent response format
|
|
187
156
|
"""
|
|
188
157
|
try:
|
|
189
158
|
logger.info("Starting Anthropic loop run")
|
|
190
159
|
|
|
191
|
-
# Reset message history and add new messages
|
|
192
|
-
self.message_history = []
|
|
193
|
-
self.message_history.extend(messages)
|
|
194
|
-
|
|
195
160
|
# Create queue for response streaming
|
|
196
161
|
queue = asyncio.Queue()
|
|
197
162
|
|
|
@@ -204,7 +169,7 @@ class AnthropicLoop(BaseLoop):
|
|
|
204
169
|
logger.info("Client initialized successfully")
|
|
205
170
|
|
|
206
171
|
# Start loop in background task
|
|
207
|
-
loop_task = asyncio.create_task(self._run_loop(queue))
|
|
172
|
+
loop_task = asyncio.create_task(self._run_loop(queue, messages))
|
|
208
173
|
|
|
209
174
|
# Process and yield messages as they arrive
|
|
210
175
|
while True:
|
|
@@ -236,37 +201,87 @@ class AnthropicLoop(BaseLoop):
|
|
|
236
201
|
"metadata": {"title": "❌ Error"},
|
|
237
202
|
}
|
|
238
203
|
|
|
239
|
-
|
|
240
|
-
|
|
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.
|
|
241
210
|
|
|
242
211
|
Args:
|
|
243
212
|
queue: Queue for response streaming
|
|
213
|
+
messages: List of messages in standard OpenAI format
|
|
244
214
|
"""
|
|
245
215
|
try:
|
|
246
216
|
while True:
|
|
247
|
-
#
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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")
|
|
252
252
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
raise
|
|
256
|
-
"Message manager not initialized. Call initialize_client() first."
|
|
257
|
-
)
|
|
258
|
-
prepared_messages = self.message_manager.prepare_messages(
|
|
259
|
-
cast(List[BetaMessageParam], self.message_history.copy())
|
|
260
|
-
)
|
|
253
|
+
except Exception as e:
|
|
254
|
+
logger.error(f"Error capturing or processing screenshot: {str(e)}")
|
|
255
|
+
raise
|
|
261
256
|
|
|
262
257
|
# Create new turn directory for this API call
|
|
263
258
|
self._create_turn_dir()
|
|
264
259
|
|
|
265
|
-
#
|
|
266
|
-
|
|
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
|
+
)
|
|
267
273
|
|
|
268
|
-
#
|
|
269
|
-
|
|
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)
|
|
283
|
+
|
|
284
|
+
if not should_continue:
|
|
270
285
|
break
|
|
271
286
|
|
|
272
287
|
# Signal completion
|
|
@@ -283,142 +298,101 @@ class AnthropicLoop(BaseLoop):
|
|
|
283
298
|
)
|
|
284
299
|
await queue.put(None)
|
|
285
300
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
Args:
|
|
290
|
-
messages: List of messages to send to the API
|
|
291
|
-
|
|
292
|
-
Returns:
|
|
293
|
-
API response
|
|
294
|
-
"""
|
|
295
|
-
if self.client is None:
|
|
296
|
-
raise RuntimeError("Client not initialized. Call initialize_client() first.")
|
|
297
|
-
if self.tool_manager is None:
|
|
298
|
-
raise RuntimeError("Tool manager not initialized. Call initialize_client() first.")
|
|
299
|
-
|
|
300
|
-
last_error = None
|
|
301
|
-
|
|
302
|
-
for attempt in range(self.max_retries):
|
|
303
|
-
try:
|
|
304
|
-
# Log request
|
|
305
|
-
request_data = {
|
|
306
|
-
"messages": messages,
|
|
307
|
-
"max_tokens": self.max_tokens,
|
|
308
|
-
"system": SYSTEM_PROMPT,
|
|
309
|
-
}
|
|
310
|
-
# Let ExperimentManager handle sanitization
|
|
311
|
-
self._log_api_call("request", request_data)
|
|
312
|
-
|
|
313
|
-
# Setup betas and system
|
|
314
|
-
system = BetaTextBlockParam(
|
|
315
|
-
type="text",
|
|
316
|
-
text=SYSTEM_PROMPT,
|
|
317
|
-
)
|
|
318
|
-
|
|
319
|
-
betas = [COMPUTER_USE_BETA_FLAG]
|
|
320
|
-
# Temporarily disable prompt caching due to "A maximum of 4 blocks with cache_control may be provided" error
|
|
321
|
-
# if self.message_manager.image_retention_config.enable_caching:
|
|
322
|
-
# betas.append(PROMPT_CACHING_BETA_FLAG)
|
|
323
|
-
# system["cache_control"] = {"type": "ephemeral"}
|
|
324
|
-
|
|
325
|
-
# Make API call
|
|
326
|
-
response = await self.client.create_message(
|
|
327
|
-
messages=messages,
|
|
328
|
-
system=[system],
|
|
329
|
-
tools=self.tool_manager.get_tool_params(),
|
|
330
|
-
max_tokens=self.max_tokens,
|
|
331
|
-
betas=betas,
|
|
332
|
-
)
|
|
333
|
-
|
|
334
|
-
# Let ExperimentManager handle sanitization
|
|
335
|
-
self._log_api_call("response", request_data, response)
|
|
336
|
-
|
|
337
|
-
return response
|
|
338
|
-
except Exception as e:
|
|
339
|
-
last_error = e
|
|
340
|
-
logger.error(
|
|
341
|
-
f"Error in API call (attempt {attempt + 1}/{self.max_retries}): {str(e)}"
|
|
342
|
-
)
|
|
343
|
-
self._log_api_call("error", {"messages": messages}, error=e)
|
|
344
|
-
|
|
345
|
-
if attempt < self.max_retries - 1:
|
|
346
|
-
await asyncio.sleep(self.retry_delay * (attempt + 1)) # Exponential backoff
|
|
347
|
-
continue
|
|
348
|
-
|
|
349
|
-
# If we get here, all retries failed
|
|
350
|
-
error_message = f"API call failed after {self.max_retries} attempts"
|
|
351
|
-
if last_error:
|
|
352
|
-
error_message += f": {str(last_error)}"
|
|
353
|
-
|
|
354
|
-
logger.error(error_message)
|
|
355
|
-
raise RuntimeError(error_message)
|
|
301
|
+
###########################################
|
|
302
|
+
# RESPONSE AND CALLBACK HANDLING
|
|
303
|
+
###########################################
|
|
356
304
|
|
|
357
305
|
async def _handle_response(self, response: BetaMessage, messages: List[Dict[str, Any]]) -> bool:
|
|
358
|
-
"""Handle the Anthropic API
|
|
306
|
+
"""Handle a response from the Anthropic API.
|
|
359
307
|
|
|
360
308
|
Args:
|
|
361
|
-
response:
|
|
362
|
-
messages:
|
|
309
|
+
response: The response from the Anthropic API
|
|
310
|
+
messages: The message history
|
|
363
311
|
|
|
364
312
|
Returns:
|
|
365
|
-
|
|
313
|
+
bool: Whether to continue the conversation
|
|
366
314
|
"""
|
|
367
315
|
try:
|
|
368
|
-
# Convert response to
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
{
|
|
374
|
-
"role": "assistant",
|
|
375
|
-
"content": response_params,
|
|
376
|
-
}
|
|
316
|
+
# Convert response to standard format
|
|
317
|
+
openai_compatible_response = await to_agent_response_format(
|
|
318
|
+
response,
|
|
319
|
+
messages,
|
|
320
|
+
model=self.model,
|
|
377
321
|
)
|
|
378
322
|
|
|
323
|
+
# Put the response on the queue
|
|
324
|
+
await self.queue.put(openai_compatible_response)
|
|
325
|
+
|
|
379
326
|
if self.callback_manager is None:
|
|
380
327
|
raise RuntimeError(
|
|
381
328
|
"Callback manager not initialized. Call initialize_client() first."
|
|
382
329
|
)
|
|
383
330
|
|
|
384
|
-
# Handle tool use blocks and collect results
|
|
331
|
+
# Handle tool use blocks and collect ALL results before adding to messages
|
|
385
332
|
tool_result_content = []
|
|
386
|
-
|
|
333
|
+
has_tool_use = False
|
|
334
|
+
|
|
335
|
+
for content_block in response.content:
|
|
387
336
|
# Notify callback of content
|
|
388
337
|
self.callback_manager.on_content(cast(BetaContentBlockParam, content_block))
|
|
389
338
|
|
|
390
|
-
# Handle tool use
|
|
391
|
-
if content_block
|
|
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
|
|
392
342
|
if self.tool_manager is None:
|
|
393
343
|
raise RuntimeError(
|
|
394
344
|
"Tool manager not initialized. Call initialize_client() first."
|
|
395
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", "")
|
|
351
|
+
|
|
396
352
|
result = await self.tool_manager.execute_tool(
|
|
397
|
-
name=
|
|
398
|
-
tool_input=cast(Dict[str, Any],
|
|
353
|
+
name=tool_name,
|
|
354
|
+
tool_input=cast(Dict[str, Any], tool_input),
|
|
399
355
|
)
|
|
400
356
|
|
|
401
|
-
# Create tool result
|
|
402
|
-
tool_result = self._make_tool_result(
|
|
403
|
-
cast(ToolResult, result), content_block["id"]
|
|
404
|
-
)
|
|
357
|
+
# Create tool result
|
|
358
|
+
tool_result = self._make_tool_result(cast(ToolResult, result), tool_id)
|
|
405
359
|
tool_result_content.append(tool_result)
|
|
406
360
|
|
|
407
361
|
# Notify callback of tool result
|
|
408
|
-
self.callback_manager.on_tool_result(
|
|
409
|
-
|
|
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"
|
|
410
371
|
)
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
|
415
393
|
self.callback_manager.on_content({"type": "text", "text": "<DONE>"})
|
|
416
394
|
return False
|
|
417
395
|
|
|
418
|
-
# Add tool results to message history
|
|
419
|
-
messages.append({"content": tool_result_content, "role": "user"})
|
|
420
|
-
return True
|
|
421
|
-
|
|
422
396
|
except Exception as e:
|
|
423
397
|
logger.error(f"Error handling response: {str(e)}")
|
|
424
398
|
messages.append(
|
|
@@ -429,28 +403,41 @@ class AnthropicLoop(BaseLoop):
|
|
|
429
403
|
)
|
|
430
404
|
return False
|
|
431
405
|
|
|
432
|
-
def
|
|
433
|
-
|
|
434
|
-
response: BetaMessage,
|
|
435
|
-
) -> List[Dict[str, Any]]:
|
|
436
|
-
"""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.
|
|
437
408
|
|
|
438
409
|
Args:
|
|
439
410
|
response: API response message
|
|
440
411
|
|
|
441
412
|
Returns:
|
|
442
|
-
List of content blocks
|
|
413
|
+
List of content blocks in standard format
|
|
443
414
|
"""
|
|
444
415
|
result = []
|
|
445
416
|
for block in response.content:
|
|
446
417
|
if isinstance(block, BetaTextBlock):
|
|
447
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
|
+
)
|
|
448
429
|
else:
|
|
449
|
-
|
|
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
|
+
|
|
450
437
|
return result
|
|
451
438
|
|
|
452
439
|
def _make_tool_result(self, result: ToolResult, tool_use_id: str) -> Dict[str, Any]:
|
|
453
|
-
"""Convert a tool result to
|
|
440
|
+
"""Convert a tool result to standard format.
|
|
454
441
|
|
|
455
442
|
Args:
|
|
456
443
|
result: Tool execution result
|
|
@@ -489,12 +476,8 @@ class AnthropicLoop(BaseLoop):
|
|
|
489
476
|
if result.base64_image:
|
|
490
477
|
tool_result_content.append(
|
|
491
478
|
{
|
|
492
|
-
"type": "
|
|
493
|
-
"
|
|
494
|
-
"type": "base64",
|
|
495
|
-
"media_type": "image/png",
|
|
496
|
-
"data": result.base64_image,
|
|
497
|
-
},
|
|
479
|
+
"type": "image_url",
|
|
480
|
+
"image_url": {"url": f"data:image/png;base64,{result.base64_image}"},
|
|
498
481
|
}
|
|
499
482
|
)
|
|
500
483
|
|
|
@@ -519,16 +502,19 @@ class AnthropicLoop(BaseLoop):
|
|
|
519
502
|
result_text = f"<s>{result.system}</s>\n{result_text}"
|
|
520
503
|
return result_text
|
|
521
504
|
|
|
522
|
-
|
|
505
|
+
###########################################
|
|
506
|
+
# CALLBACK HANDLERS
|
|
507
|
+
###########################################
|
|
508
|
+
|
|
509
|
+
def _handle_content(self, content):
|
|
523
510
|
"""Handle content updates from the assistant."""
|
|
524
511
|
if content.get("type") == "text":
|
|
525
|
-
|
|
526
|
-
text = text_content["text"]
|
|
512
|
+
text = content.get("text", "")
|
|
527
513
|
if text == "<DONE>":
|
|
528
514
|
return
|
|
529
515
|
logger.info(f"Assistant: {text}")
|
|
530
516
|
|
|
531
|
-
def _handle_tool_result(self, result
|
|
517
|
+
def _handle_tool_result(self, result, tool_id):
|
|
532
518
|
"""Handle tool execution results."""
|
|
533
519
|
if result.error:
|
|
534
520
|
logger.error(f"Tool {tool_id} error: {result.error}")
|