use-computer 0.0.39__tar.gz → 0.0.41__tar.gz
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.
- {use_computer-0.0.39 → use_computer-0.0.41}/PKG-INFO +3 -1
- {use_computer-0.0.39 → use_computer-0.0.41}/pyproject.toml +3 -1
- {use_computer-0.0.39 → use_computer-0.0.41}/scripts/smoke_wheel.py +1 -1
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/base/actions.py +1 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/base/desktop.py +70 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/base/recordings.py +25 -11
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/providers/generic.py +20 -30
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/providers/openai.py +60 -23
- use_computer-0.0.41/use_computer/agents/providers/tinker_backend.py +163 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/client.py +18 -8
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/harbor/environment/platforms/__init__.py +2 -1
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/sandbox.py +14 -4
- {use_computer-0.0.39 → use_computer-0.0.41}/uv.lock +879 -29
- {use_computer-0.0.39 → use_computer-0.0.41}/.env.example +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/.gitignore +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/.pre-commit-config.yaml +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/README.md +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/__init__.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/accessibility.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/__init__.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/base/__init__.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/base/agent.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/base/artifacts.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/base/final_state.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/base/prompts.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/base/sandbox.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/base/screenshots.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/base/trajectory.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/core.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/debug/README.md +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/debug/__init__.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/debug/actions.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/debug/agent.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/debug/models.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/ios/__init__.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/ios/agent.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/ios/tools.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/prompts/anthropic.txt +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/prompts/gemini.txt +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/prompts/ios.txt +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/prompts/openai.txt +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/prompts/pyautogui.txt +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/providers/__init__.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/providers/anthropic.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/providers/gemini.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/agents/types.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/ax_transpile.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/display.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/errors.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/harbor/__init__.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/harbor/agents.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/harbor/environment/__init__.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/harbor/environment/gateway.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/harbor/environment/platforms/ios.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/harbor/environment/platforms/macos.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/harbor/environment/platforms/ubuntu.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/harbor/environment/platforms/windows.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/harbor/environment/setup_files.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/harbor/environment/use_computer.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/harbor/environment/utils/__init__.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/harbor/environment/utils/verifier.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/ios/__init__.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/ios/apps.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/ios/environment.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/ios/input.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/macos/__init__.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/macos/keyboard.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/macos/mouse.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/models.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/parsers.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/py.typed +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/recording.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/resources/__init__.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/resources/accessibility.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/resources/display.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/resources/recording.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/resources/screenshot.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/retry.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/screenshot.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/simulators.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/tasks/__init__.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/tasks/templates/pre_command.ps1 +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/tasks/templates/pre_command.sh +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/tasks/templates/task.toml +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/tasks/templates/test_ios.sh +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/tasks/templates/test_ios_nograder.sh +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/tasks/templates/test_macos.sh +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/tasks/templates/test_macos_check.sh +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/tasks/templates/test_macos_nograder.sh +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/tasks/templates/test_ubuntu.sh +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/tasks/templates/test_ubuntu_check.sh +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/tasks/templates/test_ubuntu_nograder.sh +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/tasks/templates/test_windows.sh +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/tasks/templates/test_windows_check.sh +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/tasks/templates/test_windows_nograder.sh +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/windows/__init__.py +0 -0
- {use_computer-0.0.39 → use_computer-0.0.41}/use_computer/windows/shell.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: use-computer
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.41
|
|
4
4
|
Summary: Python SDK for use.computer sandboxes across macOS, iOS simulators, Windows, and Ubuntu
|
|
5
5
|
Project-URL: Homepage, https://use.computer
|
|
6
6
|
Project-URL: Documentation, https://api.use.computer/docs
|
|
@@ -24,6 +24,8 @@ Requires-Dist: google-genai>=1; extra == 'agents'
|
|
|
24
24
|
Requires-Dist: litellm>=1; extra == 'agents'
|
|
25
25
|
Requires-Dist: openai>=1; extra == 'agents'
|
|
26
26
|
Requires-Dist: pillow>=10; extra == 'agents'
|
|
27
|
+
Requires-Dist: tinker-cookbook>=0.1.0; (python_full_version >= '3.11') and extra == 'agents'
|
|
28
|
+
Requires-Dist: tinker>=0.14.0; (python_full_version >= '3.11') and extra == 'agents'
|
|
27
29
|
Provides-Extra: harbor
|
|
28
30
|
Requires-Dist: harbor>=0.4.0; (python_version >= '3.12') and extra == 'harbor'
|
|
29
31
|
Requires-Dist: pyyaml>=6; extra == 'harbor'
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "use-computer"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.41"
|
|
8
8
|
description = "Python SDK for use.computer sandboxes across macOS, iOS simulators, Windows, and Ubuntu"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -30,6 +30,8 @@ agents = [
|
|
|
30
30
|
"anthropic>=0.86",
|
|
31
31
|
"litellm>=1",
|
|
32
32
|
"google-genai>=1",
|
|
33
|
+
"tinker>=0.14.0 ; python_full_version >= '3.11'",
|
|
34
|
+
"tinker-cookbook>=0.1.0 ; python_full_version >= '3.11'",
|
|
33
35
|
]
|
|
34
36
|
harbor = [
|
|
35
37
|
"harbor>=0.4.0; python_version >= '3.12'",
|
|
@@ -102,7 +102,7 @@ with tempfile.TemporaryDirectory() as tmp:
|
|
|
102
102
|
|
|
103
103
|
for agent_cls, model_name in [
|
|
104
104
|
(AnthropicComputerAgent, "anthropic/claude-sonnet-4-6"),
|
|
105
|
-
(OpenAIComputerAgent, "openai/
|
|
105
|
+
(OpenAIComputerAgent, "openai/gpt-5.5"),
|
|
106
106
|
(GeminiComputerAgent, "gemini/gemini-2.5-computer-use-preview-10-2025"),
|
|
107
107
|
(GenericComputerAgent, "openai/gpt-4o"),
|
|
108
108
|
(IOSComputerAgent, "anthropic/claude-sonnet-4-6"),
|
|
@@ -140,6 +140,7 @@ async def execute_action(
|
|
|
140
140
|
await sandbox.keyboard.type(command.text)
|
|
141
141
|
elif command.action is DesktopAction.KEY:
|
|
142
142
|
key = normalize_key(command.key or command.text, key_aliases)
|
|
143
|
+
action["key"] = key
|
|
143
144
|
if "+" in key:
|
|
144
145
|
await sandbox.keyboard.hotkey(key)
|
|
145
146
|
else:
|
|
@@ -12,32 +12,102 @@ from use_computer.agents.types import Platform
|
|
|
12
12
|
_KEY_ALIASES = {
|
|
13
13
|
"Escape": "esc",
|
|
14
14
|
"escape": "esc",
|
|
15
|
+
"ESCAPE": "esc",
|
|
16
|
+
"ESC": "esc",
|
|
15
17
|
"Return": "enter",
|
|
16
18
|
"return": "enter",
|
|
19
|
+
"RETURN": "enter",
|
|
20
|
+
"Enter": "enter",
|
|
21
|
+
"enter": "enter",
|
|
22
|
+
"ENTER": "enter",
|
|
23
|
+
"Command": "cmd",
|
|
24
|
+
"command": "cmd",
|
|
25
|
+
"COMMAND": "cmd",
|
|
26
|
+
"Cmd": "cmd",
|
|
27
|
+
"cmd": "cmd",
|
|
28
|
+
"CMD": "cmd",
|
|
17
29
|
"super": "cmd",
|
|
30
|
+
"SUPER": "cmd",
|
|
18
31
|
"meta": "cmd",
|
|
32
|
+
"META": "cmd",
|
|
19
33
|
"Super_L": "cmd",
|
|
20
34
|
"Meta_L": "cmd",
|
|
21
35
|
"Control": "ctrl",
|
|
36
|
+
"control": "ctrl",
|
|
37
|
+
"CONTROL": "ctrl",
|
|
38
|
+
"Ctrl": "ctrl",
|
|
39
|
+
"ctrl": "ctrl",
|
|
40
|
+
"CTRL": "ctrl",
|
|
22
41
|
"Shift": "shift",
|
|
42
|
+
"shift": "shift",
|
|
43
|
+
"SHIFT": "shift",
|
|
23
44
|
"Alt": "alt",
|
|
45
|
+
"alt": "alt",
|
|
46
|
+
"ALT": "alt",
|
|
47
|
+
"Option": "alt",
|
|
48
|
+
"option": "alt",
|
|
49
|
+
"OPTION": "alt",
|
|
24
50
|
"Backspace": "backspace",
|
|
51
|
+
"backspace": "backspace",
|
|
52
|
+
"BACKSPACE": "backspace",
|
|
25
53
|
"Delete": "delete",
|
|
54
|
+
"delete": "delete",
|
|
55
|
+
"DELETE": "delete",
|
|
56
|
+
"Tab": "tab",
|
|
57
|
+
"tab": "tab",
|
|
58
|
+
"TAB": "tab",
|
|
59
|
+
"Space": "space",
|
|
60
|
+
"space": "space",
|
|
61
|
+
"SPACE": "space",
|
|
26
62
|
"ArrowUp": "up",
|
|
63
|
+
"ARROWUP": "up",
|
|
64
|
+
"Up": "up",
|
|
65
|
+
"up": "up",
|
|
66
|
+
"UP": "up",
|
|
27
67
|
"ArrowDown": "down",
|
|
68
|
+
"ARROWDOWN": "down",
|
|
69
|
+
"Down": "down",
|
|
70
|
+
"down": "down",
|
|
71
|
+
"DOWN": "down",
|
|
28
72
|
"ArrowLeft": "left",
|
|
73
|
+
"ARROWLEFT": "left",
|
|
74
|
+
"Left": "left",
|
|
75
|
+
"left": "left",
|
|
76
|
+
"LEFT": "left",
|
|
29
77
|
"ArrowRight": "right",
|
|
78
|
+
"ARROWRIGHT": "right",
|
|
79
|
+
"Right": "right",
|
|
80
|
+
"right": "right",
|
|
81
|
+
"RIGHT": "right",
|
|
30
82
|
"Page_Up": "pageup",
|
|
83
|
+
"PAGE_UP": "pageup",
|
|
84
|
+
"PAGEUP": "pageup",
|
|
31
85
|
"Page_Down": "pagedown",
|
|
86
|
+
"PAGE_DOWN": "pagedown",
|
|
87
|
+
"PAGEDOWN": "pagedown",
|
|
32
88
|
}
|
|
33
89
|
|
|
34
90
|
# Windows: the modifier is the Windows key, not Cmd.
|
|
35
91
|
_WINDOWS_KEY_ALIASES = {
|
|
36
92
|
**_KEY_ALIASES,
|
|
93
|
+
"Command": "win",
|
|
94
|
+
"command": "win",
|
|
95
|
+
"COMMAND": "win",
|
|
96
|
+
"Cmd": "win",
|
|
97
|
+
"cmd": "win",
|
|
98
|
+
"CMD": "win",
|
|
37
99
|
"super": "win",
|
|
100
|
+
"SUPER": "win",
|
|
38
101
|
"meta": "win",
|
|
102
|
+
"META": "win",
|
|
39
103
|
"Super_L": "win",
|
|
40
104
|
"Meta_L": "win",
|
|
105
|
+
"Win": "win",
|
|
106
|
+
"win": "win",
|
|
107
|
+
"WIN": "win",
|
|
108
|
+
"Windows": "win",
|
|
109
|
+
"windows": "win",
|
|
110
|
+
"WINDOWS": "win",
|
|
41
111
|
}
|
|
42
112
|
|
|
43
113
|
|
|
@@ -11,6 +11,11 @@ from use_computer.sandbox import AsyncSandbox
|
|
|
11
11
|
|
|
12
12
|
class RecordingMixin:
|
|
13
13
|
async def start_recording(self: Any, sandbox: AsyncSandbox) -> None:
|
|
14
|
+
if getattr(getattr(sandbox, "type", None), "value", "") == "ios":
|
|
15
|
+
try:
|
|
16
|
+
await sandbox.screenshot.take_full_screen()
|
|
17
|
+
except Exception as e:
|
|
18
|
+
self.logger.warning(f"iOS recording warm-up screenshot failed: {e}")
|
|
14
19
|
try:
|
|
15
20
|
result = await sandbox.recording.start(name="trial")
|
|
16
21
|
self._recording_id = result.id
|
|
@@ -22,6 +27,7 @@ class RecordingMixin:
|
|
|
22
27
|
if not self._recording_id:
|
|
23
28
|
return
|
|
24
29
|
await asyncio.sleep(3.0)
|
|
30
|
+
rec_path = self.logs_dir / "recording.mp4"
|
|
25
31
|
try:
|
|
26
32
|
info = await sandbox.recording.stop(self._recording_id)
|
|
27
33
|
self.logger.info(
|
|
@@ -33,16 +39,24 @@ class RecordingMixin:
|
|
|
33
39
|
|
|
34
40
|
await asyncio.sleep(2)
|
|
35
41
|
|
|
36
|
-
|
|
37
|
-
self.logger.info(f"Downloading recording → {rec_path}")
|
|
38
|
-
t_dl = time.monotonic()
|
|
39
|
-
await sandbox.recording.download(self._recording_id, str(rec_path), timeout=180.0)
|
|
40
|
-
dl_elapsed = time.monotonic() - t_dl
|
|
41
|
-
local_size = rec_path.stat().st_size
|
|
42
|
-
self.logger.info(
|
|
43
|
-
f"Recording saved: {rec_path} "
|
|
44
|
-
f"local_size={local_size}B ({local_size / (1024 * 1024):.2f} MB) "
|
|
45
|
-
f"download_elapsed={dl_elapsed:.1f}s"
|
|
46
|
-
)
|
|
42
|
+
await self._download_recording(sandbox, rec_path)
|
|
47
43
|
except Exception as e:
|
|
48
44
|
self.logger.warning(f"Recording save failed: {type(e).__name__}: {e}")
|
|
45
|
+
try:
|
|
46
|
+
await self._download_recording(sandbox, rec_path)
|
|
47
|
+
except Exception as fallback:
|
|
48
|
+
self.logger.warning(
|
|
49
|
+
f"Recording fallback download failed: {type(fallback).__name__}: {fallback}"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
async def _download_recording(self: Any, sandbox: AsyncSandbox, rec_path) -> None:
|
|
53
|
+
self.logger.info(f"Downloading recording → {rec_path}")
|
|
54
|
+
t_dl = time.monotonic()
|
|
55
|
+
await sandbox.recording.download(self._recording_id, str(rec_path), timeout=180.0)
|
|
56
|
+
dl_elapsed = time.monotonic() - t_dl
|
|
57
|
+
local_size = rec_path.stat().st_size
|
|
58
|
+
self.logger.info(
|
|
59
|
+
f"Recording saved: {rec_path} "
|
|
60
|
+
f"local_size={local_size}B ({local_size / (1024 * 1024):.2f} MB) "
|
|
61
|
+
f"download_elapsed={dl_elapsed:.1f}s"
|
|
62
|
+
)
|
|
@@ -83,6 +83,11 @@ class GenericCUAAgent(BaseCUAAgent):
|
|
|
83
83
|
coord_mode: str | None = None,
|
|
84
84
|
reasoning_effort: str | None = None,
|
|
85
85
|
reasoning_history: str | None = None,
|
|
86
|
+
renderer_name: str | None = None,
|
|
87
|
+
model_path: str | None = None,
|
|
88
|
+
temperature: float = 1.0,
|
|
89
|
+
top_p: float = 0.95,
|
|
90
|
+
top_k: int = -1,
|
|
86
91
|
**kwargs: Any,
|
|
87
92
|
):
|
|
88
93
|
super().__init__(logs_dir=logs_dir, model_name=model_name, **kwargs)
|
|
@@ -106,7 +111,12 @@ class GenericCUAAgent(BaseCUAAgent):
|
|
|
106
111
|
ReasoningHistory(reasoning_history) if reasoning_history is not None else None
|
|
107
112
|
),
|
|
108
113
|
).as_dict()
|
|
109
|
-
self.
|
|
114
|
+
self._tinker_backend = None
|
|
115
|
+
self._renderer_name = renderer_name
|
|
116
|
+
self._model_path = model_path
|
|
117
|
+
self._temperature = temperature
|
|
118
|
+
self._top_p = top_p
|
|
119
|
+
self._top_k = top_k
|
|
110
120
|
# Coordinate convention emitted by the model:
|
|
111
121
|
# "api_pixel" — Kimi default: 0..1 floats OR pixel ints in API space (1280x800)
|
|
112
122
|
# "normalized_1000" — Qwen-VL default: 0..1000 ints as per-mille of screen
|
|
@@ -168,39 +178,19 @@ class GenericCUAAgent(BaseCUAAgent):
|
|
|
168
178
|
async def _tinker_completion(
|
|
169
179
|
self, model: str, messages: list[dict], max_tokens: int
|
|
170
180
|
) -> tuple[str, int, int]:
|
|
171
|
-
if self.
|
|
172
|
-
from
|
|
181
|
+
if self._tinker_backend is None:
|
|
182
|
+
from use_computer.agents.providers.tinker_backend import TinkerBackend
|
|
173
183
|
|
|
174
|
-
self.
|
|
184
|
+
self._tinker_backend = TinkerBackend(
|
|
175
185
|
model_name=model,
|
|
186
|
+
model_path=self._model_path,
|
|
187
|
+
renderer_name=self._renderer_name,
|
|
188
|
+
temperature=self._temperature,
|
|
189
|
+
top_p=self._top_p,
|
|
190
|
+
top_k=self._top_k,
|
|
176
191
|
max_tokens=max_tokens,
|
|
177
192
|
)
|
|
178
|
-
|
|
179
|
-
# TinkerLLM.call() expects (prompt, message_history)
|
|
180
|
-
# Split system + history from the final user message
|
|
181
|
-
history = messages[:-1] if len(messages) > 1 else []
|
|
182
|
-
prompt = ""
|
|
183
|
-
last_msg = messages[-1] if messages else {"content": ""}
|
|
184
|
-
content = last_msg.get("content")
|
|
185
|
-
if isinstance(content, str):
|
|
186
|
-
prompt = content
|
|
187
|
-
elif isinstance(content, list):
|
|
188
|
-
prompt = " ".join(
|
|
189
|
-
p.get("text", "")
|
|
190
|
-
for p in content
|
|
191
|
-
if isinstance(p, dict) and p.get("type") == "text"
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
response = await self._tinker_llm.call(prompt=prompt, message_history=history)
|
|
195
|
-
content = response.content or ""
|
|
196
|
-
text = (
|
|
197
|
-
content
|
|
198
|
-
if isinstance(content, str)
|
|
199
|
-
else "".join(p.get("text", "") if isinstance(p, dict) else str(p) for p in content)
|
|
200
|
-
)
|
|
201
|
-
prompt_tokens = response.usage.prompt_tokens if response.usage else 0
|
|
202
|
-
completion_tokens = response.usage.completion_tokens if response.usage else 0
|
|
203
|
-
return text, prompt_tokens, completion_tokens
|
|
193
|
+
return await self._tinker_backend.complete(messages, max_tokens)
|
|
204
194
|
|
|
205
195
|
async def _run_agent(
|
|
206
196
|
self,
|
|
@@ -19,6 +19,9 @@ from use_computer.agents.base import (
|
|
|
19
19
|
)
|
|
20
20
|
from use_computer.agents.types import DesktopAction, ScrollDirection
|
|
21
21
|
|
|
22
|
+
DEFAULT_OPENAI_COMPUTER_MODEL = "gpt-5.5"
|
|
23
|
+
LEGACY_OPENAI_COMPUTER_MODEL = "computer-use-preview"
|
|
24
|
+
|
|
22
25
|
|
|
23
26
|
class OpenAIToolType(str, Enum):
|
|
24
27
|
COMPUTER = "computer"
|
|
@@ -31,6 +34,7 @@ class OpenAIItemType(str, Enum):
|
|
|
31
34
|
COMPUTER_SCREENSHOT = "computer_screenshot"
|
|
32
35
|
INPUT_IMAGE = "input_image"
|
|
33
36
|
MESSAGE = "message"
|
|
37
|
+
OUTPUT_TEXT = "output_text"
|
|
34
38
|
|
|
35
39
|
|
|
36
40
|
@dataclass(frozen=True)
|
|
@@ -46,7 +50,6 @@ class OpenAIComputerObservation:
|
|
|
46
50
|
"output": {
|
|
47
51
|
"type": OpenAIItemType.COMPUTER_SCREENSHOT.value,
|
|
48
52
|
"image_url": f"data:image/png;base64,{self.screenshot_b64}",
|
|
49
|
-
"detail": "original",
|
|
50
53
|
},
|
|
51
54
|
}
|
|
52
55
|
if self.pending_safety_checks:
|
|
@@ -69,7 +72,7 @@ class OpenAICUAAgent(BaseCUAAgent):
|
|
|
69
72
|
self.steps[0]["message"] = instruction
|
|
70
73
|
|
|
71
74
|
client = openai.OpenAI()
|
|
72
|
-
model = self._parsed_model_name or
|
|
75
|
+
model = self._parsed_model_name or DEFAULT_OPENAI_COMPUTER_MODEL
|
|
73
76
|
|
|
74
77
|
system = build_system_prompt(
|
|
75
78
|
load_prompt("openai.txt"),
|
|
@@ -109,21 +112,14 @@ class OpenAICUAAgent(BaseCUAAgent):
|
|
|
109
112
|
|
|
110
113
|
for step_idx in range(self.max_steps):
|
|
111
114
|
await self._fire_in_process(environment, step_idx)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
model
|
|
115
|
-
tools=tools,
|
|
116
|
-
|
|
117
|
-
truncation="auto",
|
|
118
|
-
)
|
|
119
|
-
else:
|
|
120
|
-
response = client.responses.create(
|
|
121
|
-
model=model,
|
|
122
|
-
tools=tools, # type: ignore
|
|
123
|
-
input=input_items,
|
|
115
|
+
response = client.responses.create(
|
|
116
|
+
**_response_create_kwargs(
|
|
117
|
+
model,
|
|
118
|
+
tools=tools,
|
|
119
|
+
input_items=input_items,
|
|
124
120
|
previous_response_id=previous_response_id,
|
|
125
|
-
truncation="auto",
|
|
126
121
|
)
|
|
122
|
+
)
|
|
127
123
|
previous_response_id = response.id
|
|
128
124
|
|
|
129
125
|
self.total_in += response.usage.input_tokens if response.usage else 0
|
|
@@ -136,16 +132,11 @@ class OpenAICUAAgent(BaseCUAAgent):
|
|
|
136
132
|
]
|
|
137
133
|
|
|
138
134
|
if not computer_calls:
|
|
139
|
-
text_parts = [
|
|
140
|
-
getattr(item, "text", "")
|
|
141
|
-
for item in response.output
|
|
142
|
-
if getattr(item, "type", None) == OpenAIItemType.MESSAGE.value
|
|
143
|
-
]
|
|
144
135
|
self.steps.append(
|
|
145
136
|
{
|
|
146
137
|
"step_id": len(self.steps) + 1,
|
|
147
138
|
"source": "agent",
|
|
148
|
-
"message":
|
|
139
|
+
"message": _response_text(response) or "Task complete.",
|
|
149
140
|
}
|
|
150
141
|
)
|
|
151
142
|
break
|
|
@@ -222,7 +213,7 @@ def _openai_tools_for_model(
|
|
|
222
213
|
height: int,
|
|
223
214
|
environment: str,
|
|
224
215
|
) -> list[dict[str, Any]]:
|
|
225
|
-
if model
|
|
216
|
+
if _uses_legacy_preview_tool(model):
|
|
226
217
|
return [
|
|
227
218
|
{
|
|
228
219
|
"type": OpenAIToolType.COMPUTER_USE_PREVIEW.value,
|
|
@@ -234,6 +225,29 @@ def _openai_tools_for_model(
|
|
|
234
225
|
return [{"type": OpenAIToolType.COMPUTER.value}]
|
|
235
226
|
|
|
236
227
|
|
|
228
|
+
def _response_create_kwargs(
|
|
229
|
+
model: str,
|
|
230
|
+
*,
|
|
231
|
+
tools: list[dict[str, Any]],
|
|
232
|
+
input_items: list[Any],
|
|
233
|
+
previous_response_id: str | None,
|
|
234
|
+
) -> dict[str, Any]:
|
|
235
|
+
kwargs: dict[str, Any] = {
|
|
236
|
+
"model": model,
|
|
237
|
+
"tools": tools,
|
|
238
|
+
"input": input_items,
|
|
239
|
+
}
|
|
240
|
+
if previous_response_id is not None:
|
|
241
|
+
kwargs["previous_response_id"] = previous_response_id
|
|
242
|
+
if _uses_legacy_preview_tool(model):
|
|
243
|
+
kwargs["truncation"] = "auto"
|
|
244
|
+
return kwargs
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _uses_legacy_preview_tool(model: str) -> bool:
|
|
248
|
+
return model in {LEGACY_OPENAI_COMPUTER_MODEL, "computer-use-preview-2025-03-11"}
|
|
249
|
+
|
|
250
|
+
|
|
237
251
|
def _openai_actions(call: Any) -> list[Any]:
|
|
238
252
|
actions = getattr(call, "actions", None)
|
|
239
253
|
if isinstance(actions, Iterable) and not isinstance(actions, (str, bytes)):
|
|
@@ -255,8 +269,31 @@ def _pending_safety_checks(call: Any) -> tuple[dict[str, Any], ...]:
|
|
|
255
269
|
return tuple(normalized)
|
|
256
270
|
|
|
257
271
|
|
|
272
|
+
def _response_text(response: Any) -> str:
|
|
273
|
+
output_text = getattr(response, "output_text", "")
|
|
274
|
+
if isinstance(output_text, str) and output_text:
|
|
275
|
+
return output_text
|
|
276
|
+
|
|
277
|
+
text_parts = []
|
|
278
|
+
for item in getattr(response, "output", []):
|
|
279
|
+
if getattr(item, "type", None) != OpenAIItemType.MESSAGE.value:
|
|
280
|
+
continue
|
|
281
|
+
content = getattr(item, "content", None)
|
|
282
|
+
if isinstance(content, Iterable) and not isinstance(content, (str, bytes)):
|
|
283
|
+
for block in content:
|
|
284
|
+
if getattr(block, "type", None) == OpenAIItemType.OUTPUT_TEXT.value:
|
|
285
|
+
text = getattr(block, "text", "")
|
|
286
|
+
if text:
|
|
287
|
+
text_parts.append(text)
|
|
288
|
+
else:
|
|
289
|
+
text = getattr(item, "text", "")
|
|
290
|
+
if text:
|
|
291
|
+
text_parts.append(text)
|
|
292
|
+
return "\n".join(text_parts)
|
|
293
|
+
|
|
294
|
+
|
|
258
295
|
def _map_openai_action(action: Any) -> dict[str, Any]:
|
|
259
|
-
"""Map OpenAI
|
|
296
|
+
"""Map OpenAI computer actions to our standard format."""
|
|
260
297
|
action_type = action.type
|
|
261
298
|
|
|
262
299
|
type_map = {
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Image-aware Tinker sampling backend for the generic CUA agent.
|
|
2
|
+
|
|
3
|
+
harbor's TinkerLLM forwards text only, so a vision agent loses its screenshots
|
|
4
|
+
on the tinker path. This backend builds the generation prompt through a
|
|
5
|
+
tinker-cookbook renderer with the model's image processor attached, so the
|
|
6
|
+
OpenAI-style `image_url` parts the generic agent emits reach the model.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import base64
|
|
13
|
+
import importlib
|
|
14
|
+
import io
|
|
15
|
+
from typing import Any, cast
|
|
16
|
+
|
|
17
|
+
from PIL import Image
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TinkerBackend:
|
|
21
|
+
"""Vision-capable tinker sampler that consumes OpenAI chat messages."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
*,
|
|
26
|
+
model_name: str,
|
|
27
|
+
model_path: str | None = None,
|
|
28
|
+
renderer_name: str | None = None,
|
|
29
|
+
temperature: float = 1.0,
|
|
30
|
+
top_p: float = 0.95,
|
|
31
|
+
top_k: int = -1,
|
|
32
|
+
max_tokens: int = 2048,
|
|
33
|
+
retry_times: int = 5,
|
|
34
|
+
retry_interval: float = 5.0,
|
|
35
|
+
) -> None:
|
|
36
|
+
self.model_name = model_name
|
|
37
|
+
self.model_path = model_path
|
|
38
|
+
self.renderer_name = renderer_name
|
|
39
|
+
self.temperature = temperature
|
|
40
|
+
self.top_p = top_p
|
|
41
|
+
self.top_k = top_k
|
|
42
|
+
self.max_tokens = max_tokens
|
|
43
|
+
self.retry_times = retry_times
|
|
44
|
+
self.retry_interval = retry_interval
|
|
45
|
+
self._sampling_client = None
|
|
46
|
+
self._renderer = None
|
|
47
|
+
|
|
48
|
+
async def _ensure_ready(self) -> None:
|
|
49
|
+
if self._sampling_client is not None and self._renderer is not None:
|
|
50
|
+
return
|
|
51
|
+
try:
|
|
52
|
+
tinker = importlib.import_module("tinker")
|
|
53
|
+
get_image_processor = importlib.import_module(
|
|
54
|
+
"tinker_cookbook.image_processing_utils"
|
|
55
|
+
).get_image_processor
|
|
56
|
+
get_model_attributes = importlib.import_module(
|
|
57
|
+
"tinker_cookbook.model_info"
|
|
58
|
+
).get_model_attributes
|
|
59
|
+
get_renderer = importlib.import_module("tinker_cookbook.renderers").get_renderer
|
|
60
|
+
get_tokenizer = importlib.import_module("tinker_cookbook.tokenizer_utils").get_tokenizer
|
|
61
|
+
except ImportError as exc:
|
|
62
|
+
raise ImportError(
|
|
63
|
+
"TinkerBackend requires `tinker` and `tinker-cookbook`. "
|
|
64
|
+
"Install them with `uv pip install harbor[tinker]`."
|
|
65
|
+
) from exc
|
|
66
|
+
|
|
67
|
+
service_client = tinker.ServiceClient()
|
|
68
|
+
if self.model_path:
|
|
69
|
+
self._sampling_client = await service_client.create_sampling_client_async(
|
|
70
|
+
model_path=self.model_path
|
|
71
|
+
)
|
|
72
|
+
else:
|
|
73
|
+
self._sampling_client = await service_client.create_sampling_client_async(
|
|
74
|
+
base_model=self.model_name
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
tokenizer = get_tokenizer(self.model_name)
|
|
78
|
+
attrs = get_model_attributes(self.model_name)
|
|
79
|
+
image_processor = get_image_processor(self.model_name) if attrs.is_vl else None
|
|
80
|
+
renderer_name = self.renderer_name or _default_renderer(self.model_name)
|
|
81
|
+
self._renderer = get_renderer(
|
|
82
|
+
renderer_name,
|
|
83
|
+
tokenizer,
|
|
84
|
+
image_processor=image_processor,
|
|
85
|
+
model_name=self.model_name,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
async def complete(
|
|
89
|
+
self, messages: list[dict[str, Any]], max_tokens: int
|
|
90
|
+
) -> tuple[str, int, int]:
|
|
91
|
+
"""Sample one response; returns (text, prompt_tokens, completion_tokens)."""
|
|
92
|
+
await self._ensure_ready()
|
|
93
|
+
assert self._sampling_client is not None
|
|
94
|
+
assert self._renderer is not None
|
|
95
|
+
|
|
96
|
+
tinker = importlib.import_module("tinker")
|
|
97
|
+
get_text_content = importlib.import_module("tinker_cookbook.renderers").get_text_content
|
|
98
|
+
|
|
99
|
+
model_input = self._renderer.build_generation_prompt(
|
|
100
|
+
cast(Any, _to_tinker_messages(messages))
|
|
101
|
+
)
|
|
102
|
+
stop = self._renderer.get_stop_sequences()
|
|
103
|
+
|
|
104
|
+
last_error: Exception | None = None
|
|
105
|
+
for attempt in range(self.retry_times):
|
|
106
|
+
try:
|
|
107
|
+
response = await self._sampling_client.sample_async(
|
|
108
|
+
prompt=model_input,
|
|
109
|
+
num_samples=1,
|
|
110
|
+
sampling_params=tinker.SamplingParams(
|
|
111
|
+
temperature=self.temperature,
|
|
112
|
+
top_p=self.top_p,
|
|
113
|
+
top_k=self.top_k,
|
|
114
|
+
max_tokens=max_tokens,
|
|
115
|
+
stop=stop,
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
sequence = response.sequences[0]
|
|
119
|
+
parsed_message, _termination = self._renderer.parse_response(sequence.tokens)
|
|
120
|
+
return get_text_content(parsed_message), model_input.length, len(sequence.tokens)
|
|
121
|
+
except Exception as exc:
|
|
122
|
+
last_error = exc
|
|
123
|
+
if attempt == self.retry_times - 1:
|
|
124
|
+
break
|
|
125
|
+
await asyncio.sleep(self.retry_interval)
|
|
126
|
+
assert last_error is not None
|
|
127
|
+
raise last_error
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _to_tinker_messages(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
131
|
+
"""Convert OpenAI chat messages to tinker-cookbook messages (text + PIL images)."""
|
|
132
|
+
converted: list[dict[str, Any]] = []
|
|
133
|
+
for message in messages:
|
|
134
|
+
content = message.get("content")
|
|
135
|
+
if isinstance(content, str):
|
|
136
|
+
converted.append({"role": message["role"], "content": content})
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
parts: list[dict[str, Any]] = []
|
|
140
|
+
for part in content or []:
|
|
141
|
+
if part.get("type") == "text":
|
|
142
|
+
parts.append({"type": "text", "text": part.get("text", "")})
|
|
143
|
+
elif part.get("type") == "image_url":
|
|
144
|
+
url = part.get("image_url", {}).get("url", "")
|
|
145
|
+
b64 = url.split(",", 1)[1] if "," in url else url
|
|
146
|
+
image = Image.open(io.BytesIO(base64.b64decode(b64))).convert("RGB")
|
|
147
|
+
parts.append({"type": "image", "image": image})
|
|
148
|
+
if len(parts) == 1 and parts[0]["type"] == "text":
|
|
149
|
+
converted.append({"role": message["role"], "content": parts[0]["text"]})
|
|
150
|
+
else:
|
|
151
|
+
converted.append({"role": message["role"], "content": parts})
|
|
152
|
+
return converted
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _default_renderer(model_name: str) -> str:
|
|
156
|
+
get_recommended_renderer_names = importlib.import_module(
|
|
157
|
+
"tinker_cookbook.model_info"
|
|
158
|
+
).get_recommended_renderer_names
|
|
159
|
+
|
|
160
|
+
recommended = get_recommended_renderer_names(model_name)
|
|
161
|
+
if "qwen3_5_disable_thinking" in recommended:
|
|
162
|
+
return "qwen3_5_disable_thinking"
|
|
163
|
+
return recommended[0]
|