llms-py 3.0.13__py3-none-any.whl → 3.0.14__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.
- llms/extensions/computer_use/__init__.py +27 -0
- llms/extensions/computer_use/base.py +80 -0
- llms/extensions/computer_use/bash.py +185 -0
- llms/extensions/computer_use/computer.py +523 -0
- llms/extensions/computer_use/edit.py +303 -0
- llms/extensions/computer_use/platform.py +461 -0
- llms/extensions/computer_use/run.py +37 -0
- llms/extensions/providers/anthropic.py +22 -3
- llms/main.py +82 -8
- llms/ui/ai.mjs +1 -1
- llms/ui/app.css +25 -0
- llms/ui/modules/chat/ChatBody.mjs +27 -9
- {llms_py-3.0.13.dist-info → llms_py-3.0.14.dist-info}/METADATA +1 -1
- {llms_py-3.0.13.dist-info → llms_py-3.0.14.dist-info}/RECORD +18 -11
- {llms_py-3.0.13.dist-info → llms_py-3.0.14.dist-info}/WHEEL +0 -0
- {llms_py-3.0.13.dist-info → llms_py-3.0.14.dist-info}/entry_points.txt +0 -0
- {llms_py-3.0.13.dist-info → llms_py-3.0.14.dist-info}/licenses/LICENSE +0 -0
- {llms_py-3.0.13.dist-info → llms_py-3.0.14.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Optional, Tuple
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_screen_resolution() -> Tuple[int, int]:
|
|
12
|
+
"""
|
|
13
|
+
Get the current screen resolution (width, height).
|
|
14
|
+
|
|
15
|
+
Supports Linux (Wayland/Hyprland, X11), macOS, and Windows.
|
|
16
|
+
Returns the primary monitor's resolution.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Tuple[int, int]: (width, height) in pixels
|
|
20
|
+
|
|
21
|
+
Raises:
|
|
22
|
+
RuntimeError: If unable to determine screen resolution
|
|
23
|
+
"""
|
|
24
|
+
if sys.platform == "linux":
|
|
25
|
+
return _get_linux_resolution()
|
|
26
|
+
elif sys.platform == "darwin":
|
|
27
|
+
return _get_macos_resolution()
|
|
28
|
+
elif sys.platform == "win32":
|
|
29
|
+
return _get_windows_resolution()
|
|
30
|
+
else:
|
|
31
|
+
raise RuntimeError(f"Unsupported platform: {sys.platform}")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_display_num() -> int:
|
|
35
|
+
"""
|
|
36
|
+
Get the display number for the current session.
|
|
37
|
+
|
|
38
|
+
On Linux (X11): Returns the X display number from $DISPLAY (e.g., :0 -> 0)
|
|
39
|
+
On Linux (Wayland): Returns the Wayland display number from $WAYLAND_DISPLAY (e.g., wayland-0 -> 0)
|
|
40
|
+
On macOS: Returns the display ID of the main display
|
|
41
|
+
On Windows: Returns the index of the primary monitor (typically 0)
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
int: The display number
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
RuntimeError: If unable to determine display number
|
|
48
|
+
"""
|
|
49
|
+
if sys.platform == "linux":
|
|
50
|
+
return _get_linux_display_num()
|
|
51
|
+
elif sys.platform == "darwin":
|
|
52
|
+
return _get_macos_display_num()
|
|
53
|
+
elif sys.platform == "win32":
|
|
54
|
+
return _get_windows_display_num()
|
|
55
|
+
else:
|
|
56
|
+
raise RuntimeError(f"Unsupported platform: {sys.platform}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _get_linux_display_num() -> int:
|
|
60
|
+
"""Get display number on Linux (X11 or Wayland)."""
|
|
61
|
+
|
|
62
|
+
# Try X11 DISPLAY environment variable first
|
|
63
|
+
display = os.environ.get("DISPLAY")
|
|
64
|
+
if display:
|
|
65
|
+
# DISPLAY format: [hostname]:displaynumber[.screennumber]
|
|
66
|
+
# Examples: :0, :1, localhost:0, :0.0
|
|
67
|
+
match = re.search(r":(\d+)", display)
|
|
68
|
+
if match:
|
|
69
|
+
return int(match.group(1))
|
|
70
|
+
|
|
71
|
+
# Try Wayland display
|
|
72
|
+
wayland_display = os.environ.get("WAYLAND_DISPLAY")
|
|
73
|
+
if wayland_display:
|
|
74
|
+
# WAYLAND_DISPLAY format: wayland-N or just a socket name
|
|
75
|
+
# Examples: wayland-0, wayland-1
|
|
76
|
+
match = re.search(r"wayland-(\d+)", wayland_display)
|
|
77
|
+
if match:
|
|
78
|
+
return int(match.group(1))
|
|
79
|
+
# If it's just "wayland-0" style, try to extract number
|
|
80
|
+
match = re.search(r"(\d+)", wayland_display)
|
|
81
|
+
if match:
|
|
82
|
+
return int(match.group(1))
|
|
83
|
+
# Default to 0 if we have a Wayland display but can't parse number
|
|
84
|
+
return 0
|
|
85
|
+
|
|
86
|
+
# Try Hyprland-specific: get focused monitor ID
|
|
87
|
+
if shutil.which("hyprctl"):
|
|
88
|
+
try:
|
|
89
|
+
result = subprocess.run(["hyprctl", "monitors", "-j"], capture_output=True, text=True, timeout=5)
|
|
90
|
+
if result.returncode == 0:
|
|
91
|
+
import json
|
|
92
|
+
|
|
93
|
+
monitors = json.loads(result.stdout)
|
|
94
|
+
if monitors:
|
|
95
|
+
# Return focused monitor ID or first monitor's ID
|
|
96
|
+
monitor = next((m for m in monitors if m.get("focused")), monitors[0])
|
|
97
|
+
return monitor.get("id", 0)
|
|
98
|
+
except (subprocess.TimeoutExpired, json.JSONDecodeError, KeyError):
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
raise RuntimeError("Could not determine display number. Neither DISPLAY nor WAYLAND_DISPLAY is set.")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _get_macos_display_num() -> int:
|
|
105
|
+
"""Get display number on macOS."""
|
|
106
|
+
|
|
107
|
+
# Try using CoreGraphics via ctypes
|
|
108
|
+
try:
|
|
109
|
+
import ctypes
|
|
110
|
+
import ctypes.util
|
|
111
|
+
|
|
112
|
+
cg_path = ctypes.util.find_library("CoreGraphics")
|
|
113
|
+
if cg_path:
|
|
114
|
+
cg = ctypes.CDLL(cg_path)
|
|
115
|
+
# CGMainDisplayID returns the display ID of the main display
|
|
116
|
+
cg.CGMainDisplayID.restype = ctypes.c_uint32
|
|
117
|
+
return cg.CGMainDisplayID()
|
|
118
|
+
except (OSError, AttributeError):
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
# Try using AppKit
|
|
122
|
+
try:
|
|
123
|
+
from AppKit import NSScreen
|
|
124
|
+
|
|
125
|
+
main_screen = NSScreen.mainScreen()
|
|
126
|
+
# Get the display ID from the screen's deviceDescription
|
|
127
|
+
device_desc = main_screen.deviceDescription()
|
|
128
|
+
display_id = device_desc.get("NSScreenNumber", 0)
|
|
129
|
+
return int(display_id)
|
|
130
|
+
except ImportError:
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
# Try using system_profiler
|
|
134
|
+
try:
|
|
135
|
+
result = subprocess.run(["system_profiler", "SPDisplaysDataType"], capture_output=True, text=True, timeout=10)
|
|
136
|
+
if result.returncode == 0:
|
|
137
|
+
# Look for Display ID or just return 0 for main display
|
|
138
|
+
match = re.search(r"Display ID:\s*(\d+)", result.stdout)
|
|
139
|
+
if match:
|
|
140
|
+
return int(match.group(1))
|
|
141
|
+
# If we found display info but no ID, assume main display is 0
|
|
142
|
+
if "Resolution:" in result.stdout:
|
|
143
|
+
return 0
|
|
144
|
+
except subprocess.TimeoutExpired:
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
# Default to 0 for main display
|
|
148
|
+
return 0
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _get_windows_display_num() -> int:
|
|
152
|
+
"""Get display number on Windows."""
|
|
153
|
+
|
|
154
|
+
# Method 1: Try ctypes to enumerate monitors
|
|
155
|
+
try:
|
|
156
|
+
import ctypes
|
|
157
|
+
from ctypes import wintypes
|
|
158
|
+
|
|
159
|
+
user32 = ctypes.windll.user32
|
|
160
|
+
|
|
161
|
+
# Get the primary monitor handle
|
|
162
|
+
# MonitorFromPoint with MONITOR_DEFAULTTOPRIMARY (0x00000001)
|
|
163
|
+
primary_monitor = user32.MonitorFromPoint(
|
|
164
|
+
ctypes.wintypes.POINT(0, 0),
|
|
165
|
+
1, # MONITOR_DEFAULTTOPRIMARY
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Enumerate all monitors to find the index of the primary
|
|
169
|
+
monitors = []
|
|
170
|
+
|
|
171
|
+
def callback(hMonitor, hdcMonitor, lprcMonitor, dwData):
|
|
172
|
+
monitors.append(hMonitor)
|
|
173
|
+
return True
|
|
174
|
+
|
|
175
|
+
MONITORENUMPROC = ctypes.WINFUNCTYPE(
|
|
176
|
+
ctypes.c_bool,
|
|
177
|
+
ctypes.wintypes.HMONITOR,
|
|
178
|
+
ctypes.wintypes.HDC,
|
|
179
|
+
ctypes.POINTER(ctypes.wintypes.RECT),
|
|
180
|
+
ctypes.wintypes.LPARAM,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
user32.EnumDisplayMonitors(None, None, MONITORENUMPROC(callback), 0)
|
|
184
|
+
|
|
185
|
+
# Find the index of the primary monitor
|
|
186
|
+
for i, mon in enumerate(monitors):
|
|
187
|
+
if mon == primary_monitor:
|
|
188
|
+
return i
|
|
189
|
+
|
|
190
|
+
# Primary not found in list, return 0
|
|
191
|
+
return 0
|
|
192
|
+
|
|
193
|
+
except (AttributeError, OSError):
|
|
194
|
+
pass
|
|
195
|
+
|
|
196
|
+
# Method 2: Try win32api
|
|
197
|
+
try:
|
|
198
|
+
import win32api
|
|
199
|
+
|
|
200
|
+
# Get all monitors
|
|
201
|
+
monitors = win32api.EnumDisplayMonitors()
|
|
202
|
+
# Find primary (usually the first one, or check flags)
|
|
203
|
+
for i, (handle, dc, rect) in enumerate(monitors):
|
|
204
|
+
info = win32api.GetMonitorInfo(handle)
|
|
205
|
+
if info.get("Flags", 0) & 1: # MONITORINFOF_PRIMARY
|
|
206
|
+
return i
|
|
207
|
+
return 0
|
|
208
|
+
except ImportError:
|
|
209
|
+
pass
|
|
210
|
+
|
|
211
|
+
# Method 3: PowerShell to get monitor index
|
|
212
|
+
try:
|
|
213
|
+
ps_script = """
|
|
214
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
215
|
+
$screens = [System.Windows.Forms.Screen]::AllScreens
|
|
216
|
+
for ($i = 0; $i -lt $screens.Count; $i++) {
|
|
217
|
+
if ($screens[$i].Primary) {
|
|
218
|
+
Write-Output $i
|
|
219
|
+
break
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
"""
|
|
223
|
+
result = subprocess.run(
|
|
224
|
+
["powershell", "-NoProfile", "-Command", ps_script], capture_output=True, text=True, timeout=10
|
|
225
|
+
)
|
|
226
|
+
if result.returncode == 0:
|
|
227
|
+
return int(result.stdout.strip())
|
|
228
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, ValueError):
|
|
229
|
+
pass
|
|
230
|
+
|
|
231
|
+
# Default to 0 (primary monitor)
|
|
232
|
+
return 0
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _get_linux_resolution() -> Tuple[int, int]:
|
|
236
|
+
"""Get resolution on Linux, trying Wayland first, then X11."""
|
|
237
|
+
|
|
238
|
+
# Try Hyprland first
|
|
239
|
+
resolution = _try_hyprctl()
|
|
240
|
+
if resolution:
|
|
241
|
+
return resolution
|
|
242
|
+
|
|
243
|
+
# Try wlr-randr (generic Wayland)
|
|
244
|
+
resolution = _try_wlr_randr()
|
|
245
|
+
if resolution:
|
|
246
|
+
return resolution
|
|
247
|
+
|
|
248
|
+
# Try xrandr (X11)
|
|
249
|
+
resolution = _try_xrandr()
|
|
250
|
+
if resolution:
|
|
251
|
+
return resolution
|
|
252
|
+
|
|
253
|
+
# Try xdpyinfo (X11 fallback)
|
|
254
|
+
resolution = _try_xdpyinfo()
|
|
255
|
+
if resolution:
|
|
256
|
+
return resolution
|
|
257
|
+
|
|
258
|
+
raise RuntimeError("Could not determine screen resolution. No supported display server found.")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _try_hyprctl() -> Optional[Tuple[int, int]]:
|
|
262
|
+
"""Try getting resolution via hyprctl (Hyprland)."""
|
|
263
|
+
if not shutil.which("hyprctl"):
|
|
264
|
+
return None
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
result = subprocess.run(["hyprctl", "monitors", "-j"], capture_output=True, text=True, timeout=5)
|
|
268
|
+
if result.returncode == 0:
|
|
269
|
+
import json
|
|
270
|
+
|
|
271
|
+
monitors = json.loads(result.stdout)
|
|
272
|
+
if monitors:
|
|
273
|
+
# Get the focused monitor or first one
|
|
274
|
+
monitor = next((m for m in monitors if m.get("focused")), monitors[0])
|
|
275
|
+
return (monitor["width"], monitor["height"])
|
|
276
|
+
except (subprocess.TimeoutExpired, json.JSONDecodeError, KeyError):
|
|
277
|
+
pass
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _try_wlr_randr() -> Optional[Tuple[int, int]]:
|
|
282
|
+
"""Try getting resolution via wlr-randr (Wayland)."""
|
|
283
|
+
if not shutil.which("wlr-randr"):
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
result = subprocess.run(["wlr-randr"], capture_output=True, text=True, timeout=5)
|
|
288
|
+
if result.returncode == 0:
|
|
289
|
+
# Parse output like: " 3840x2160 px, 60.000000 Hz (current)"
|
|
290
|
+
match = re.search(r"(\d+)x(\d+)\s+px.*current", result.stdout)
|
|
291
|
+
if match:
|
|
292
|
+
return (int(match.group(1)), int(match.group(2)))
|
|
293
|
+
except subprocess.TimeoutExpired:
|
|
294
|
+
pass
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _try_xrandr() -> Optional[Tuple[int, int]]:
|
|
299
|
+
"""Try getting resolution via xrandr (X11)."""
|
|
300
|
+
if not shutil.which("xrandr"):
|
|
301
|
+
return None
|
|
302
|
+
|
|
303
|
+
try:
|
|
304
|
+
result = subprocess.run(["xrandr", "--current"], capture_output=True, text=True, timeout=5)
|
|
305
|
+
if result.returncode == 0:
|
|
306
|
+
# Look for current resolution marked with *
|
|
307
|
+
match = re.search(r"(\d+)x(\d+).*\*", result.stdout)
|
|
308
|
+
if match:
|
|
309
|
+
return (int(match.group(1)), int(match.group(2)))
|
|
310
|
+
except subprocess.TimeoutExpired:
|
|
311
|
+
pass
|
|
312
|
+
return None
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _try_xdpyinfo() -> Optional[Tuple[int, int]]:
|
|
316
|
+
"""Try getting resolution via xdpyinfo (X11)."""
|
|
317
|
+
if not shutil.which("xdpyinfo"):
|
|
318
|
+
return None
|
|
319
|
+
|
|
320
|
+
try:
|
|
321
|
+
result = subprocess.run(["xdpyinfo"], capture_output=True, text=True, timeout=5)
|
|
322
|
+
if result.returncode == 0:
|
|
323
|
+
match = re.search(r"dimensions:\s+(\d+)x(\d+)", result.stdout)
|
|
324
|
+
if match:
|
|
325
|
+
return (int(match.group(1)), int(match.group(2)))
|
|
326
|
+
except subprocess.TimeoutExpired:
|
|
327
|
+
pass
|
|
328
|
+
return None
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _get_macos_resolution() -> Tuple[int, int]:
|
|
332
|
+
"""Get resolution on macOS using system_profiler."""
|
|
333
|
+
try:
|
|
334
|
+
result = subprocess.run(["system_profiler", "SPDisplaysDataType"], capture_output=True, text=True, timeout=10)
|
|
335
|
+
if result.returncode == 0:
|
|
336
|
+
# Look for "Resolution: 2560 x 1440" or similar
|
|
337
|
+
match = re.search(r"Resolution:\s+(\d+)\s*x\s*(\d+)", result.stdout)
|
|
338
|
+
if match:
|
|
339
|
+
return (int(match.group(1)), int(match.group(2)))
|
|
340
|
+
except subprocess.TimeoutExpired:
|
|
341
|
+
pass
|
|
342
|
+
|
|
343
|
+
# Fallback: try using AppKit via Python (if available)
|
|
344
|
+
try:
|
|
345
|
+
from AppKit import NSScreen
|
|
346
|
+
|
|
347
|
+
frame = NSScreen.mainScreen().frame()
|
|
348
|
+
return (int(frame.size.width), int(frame.size.height))
|
|
349
|
+
except ImportError:
|
|
350
|
+
pass
|
|
351
|
+
|
|
352
|
+
raise RuntimeError("Could not determine screen resolution on macOS")
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _get_windows_resolution() -> Tuple[int, int]:
|
|
356
|
+
"""Get resolution on Windows."""
|
|
357
|
+
|
|
358
|
+
# Method 1: Try ctypes (no external dependencies)
|
|
359
|
+
resolution = _try_windows_ctypes()
|
|
360
|
+
if resolution:
|
|
361
|
+
return resolution
|
|
362
|
+
|
|
363
|
+
# Method 2: Try win32api (pywin32)
|
|
364
|
+
resolution = _try_windows_win32api()
|
|
365
|
+
if resolution:
|
|
366
|
+
return resolution
|
|
367
|
+
|
|
368
|
+
# Method 3: Try PowerShell
|
|
369
|
+
resolution = _try_windows_powershell()
|
|
370
|
+
if resolution:
|
|
371
|
+
return resolution
|
|
372
|
+
|
|
373
|
+
# Method 4: Try wmic (deprecated but still works on older systems)
|
|
374
|
+
resolution = _try_windows_wmic()
|
|
375
|
+
if resolution:
|
|
376
|
+
return resolution
|
|
377
|
+
|
|
378
|
+
raise RuntimeError("Could not determine screen resolution on Windows")
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _try_windows_ctypes() -> Optional[Tuple[int, int]]:
|
|
382
|
+
"""Try getting resolution via ctypes (built-in)."""
|
|
383
|
+
try:
|
|
384
|
+
import ctypes
|
|
385
|
+
|
|
386
|
+
user32 = ctypes.windll.user32
|
|
387
|
+
user32.SetProcessDPIAware() # Handle DPI scaling
|
|
388
|
+
width = user32.GetSystemMetrics(0) # SM_CXSCREEN
|
|
389
|
+
height = user32.GetSystemMetrics(1) # SM_CYSCREEN
|
|
390
|
+
if width > 0 and height > 0:
|
|
391
|
+
return (width, height)
|
|
392
|
+
except (AttributeError, OSError):
|
|
393
|
+
pass
|
|
394
|
+
return None
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _try_windows_win32api() -> Optional[Tuple[int, int]]:
|
|
398
|
+
"""Try getting resolution via win32api (pywin32)."""
|
|
399
|
+
try:
|
|
400
|
+
import win32api
|
|
401
|
+
|
|
402
|
+
width = win32api.GetSystemMetrics(0)
|
|
403
|
+
height = win32api.GetSystemMetrics(1)
|
|
404
|
+
if width > 0 and height > 0:
|
|
405
|
+
return (width, height)
|
|
406
|
+
except ImportError:
|
|
407
|
+
pass
|
|
408
|
+
return None
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _try_windows_powershell() -> Optional[Tuple[int, int]]:
|
|
412
|
+
"""Try getting resolution via PowerShell."""
|
|
413
|
+
try:
|
|
414
|
+
# Using Add-Type to access System.Windows.Forms
|
|
415
|
+
ps_script = """
|
|
416
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
417
|
+
$screen = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
|
|
418
|
+
"$($screen.Width)x$($screen.Height)"
|
|
419
|
+
"""
|
|
420
|
+
result = subprocess.run(
|
|
421
|
+
["powershell", "-NoProfile", "-Command", ps_script], capture_output=True, text=True, timeout=10
|
|
422
|
+
)
|
|
423
|
+
if result.returncode == 0:
|
|
424
|
+
match = re.match(r"(\d+)x(\d+)", result.stdout.strip())
|
|
425
|
+
if match:
|
|
426
|
+
return (int(match.group(1)), int(match.group(2)))
|
|
427
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
428
|
+
pass
|
|
429
|
+
return None
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _try_windows_wmic() -> Optional[Tuple[int, int]]:
|
|
433
|
+
"""Try getting resolution via wmic (legacy, but works on older Windows)."""
|
|
434
|
+
try:
|
|
435
|
+
result = subprocess.run(
|
|
436
|
+
["wmic", "path", "Win32_VideoController", "get", "CurrentHorizontalResolution,CurrentVerticalResolution"],
|
|
437
|
+
capture_output=True,
|
|
438
|
+
text=True,
|
|
439
|
+
timeout=10,
|
|
440
|
+
)
|
|
441
|
+
if result.returncode == 0:
|
|
442
|
+
lines = result.stdout.strip().split("\n")
|
|
443
|
+
for line in lines[1:]: # Skip header
|
|
444
|
+
match = re.match(r"(\d+)\s+(\d+)", line.strip())
|
|
445
|
+
if match:
|
|
446
|
+
return (int(match.group(1)), int(match.group(2)))
|
|
447
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
448
|
+
pass
|
|
449
|
+
return None
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
if __name__ == "__main__":
|
|
453
|
+
try:
|
|
454
|
+
width, height = get_screen_resolution()
|
|
455
|
+
print(f"Screen resolution: {width}x{height}")
|
|
456
|
+
|
|
457
|
+
display_num = get_display_num()
|
|
458
|
+
print(f"Display number: {display_num}")
|
|
459
|
+
except RuntimeError as e:
|
|
460
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
461
|
+
sys.exit(1)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Utility to run shell commands asynchronously with a timeout."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import contextlib
|
|
5
|
+
|
|
6
|
+
TRUNCATED_MESSAGE: str = "<response clipped><NOTE>To save on context only part of this file has been shown to you. You should retry this tool after you have searched inside the file with `grep -n` in order to find the line numbers of what you are looking for.</NOTE>"
|
|
7
|
+
MAX_RESPONSE_LEN: int = 16000
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def maybe_truncate(content: str, truncate_after: int | None = MAX_RESPONSE_LEN):
|
|
11
|
+
"""Truncate content and append a notice if content exceeds the specified length."""
|
|
12
|
+
return (
|
|
13
|
+
content
|
|
14
|
+
if not truncate_after or len(content) <= truncate_after
|
|
15
|
+
else content[:truncate_after] + TRUNCATED_MESSAGE
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def run(
|
|
20
|
+
cmd: str,
|
|
21
|
+
timeout: float | None = 120.0, # seconds
|
|
22
|
+
truncate_after: int | None = MAX_RESPONSE_LEN,
|
|
23
|
+
):
|
|
24
|
+
"""Run a shell command asynchronously with a timeout."""
|
|
25
|
+
process = await asyncio.create_subprocess_shell(cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout)
|
|
29
|
+
return (
|
|
30
|
+
process.returncode or 0,
|
|
31
|
+
maybe_truncate(stdout.decode(), truncate_after=truncate_after),
|
|
32
|
+
maybe_truncate(stderr.decode(), truncate_after=truncate_after),
|
|
33
|
+
)
|
|
34
|
+
except asyncio.TimeoutError as exc:
|
|
35
|
+
with contextlib.suppress(ProcessLookupError):
|
|
36
|
+
process.kill()
|
|
37
|
+
raise TimeoutError(f"Command '{cmd}' timed out after {timeout} seconds") from exc
|
|
@@ -83,9 +83,10 @@ def install_anthropic(ctx):
|
|
|
83
83
|
|
|
84
84
|
content = message.get("content", "")
|
|
85
85
|
if isinstance(content, str):
|
|
86
|
-
if anthropic_message["content"]:
|
|
87
|
-
# If we have thinking, we must use blocks for text
|
|
88
|
-
|
|
86
|
+
if anthropic_message["content"] or message.get("tool_calls"):
|
|
87
|
+
# If we have thinking or tools, we must use blocks for text
|
|
88
|
+
if content:
|
|
89
|
+
anthropic_message["content"].append({"type": "text", "text": content})
|
|
89
90
|
else:
|
|
90
91
|
anthropic_message["content"] = content
|
|
91
92
|
elif isinstance(content, list):
|
|
@@ -108,6 +109,24 @@ def install_anthropic(ctx):
|
|
|
108
109
|
}
|
|
109
110
|
)
|
|
110
111
|
|
|
112
|
+
# Handle tool_calls
|
|
113
|
+
if "tool_calls" in message and message["tool_calls"]:
|
|
114
|
+
# specific check for content being a string and not empty, because we might have converted it above
|
|
115
|
+
if isinstance(anthropic_message["content"], str):
|
|
116
|
+
anthropic_message["content"] = []
|
|
117
|
+
if content:
|
|
118
|
+
anthropic_message["content"].append({"type": "text", "text": content})
|
|
119
|
+
|
|
120
|
+
for tool_call in message["tool_calls"]:
|
|
121
|
+
function = tool_call.get("function", {})
|
|
122
|
+
tool_use = {
|
|
123
|
+
"type": "tool_use",
|
|
124
|
+
"id": tool_call.get("id"),
|
|
125
|
+
"name": function.get("name"),
|
|
126
|
+
"input": json.loads(function.get("arguments", "{}")),
|
|
127
|
+
}
|
|
128
|
+
anthropic_message["content"].append(tool_use)
|
|
129
|
+
|
|
111
130
|
anthropic_request["messages"].append(anthropic_message)
|
|
112
131
|
|
|
113
132
|
# Handle max_tokens (required by Anthropic, uses max_tokens not max_completion_tokens)
|
llms/main.py
CHANGED
|
@@ -26,11 +26,24 @@ import sys
|
|
|
26
26
|
import time
|
|
27
27
|
import traceback
|
|
28
28
|
from datetime import datetime
|
|
29
|
-
from enum import IntEnum
|
|
29
|
+
from enum import Enum, IntEnum
|
|
30
30
|
from importlib import resources # Py≥3.9 (pip install importlib_resources for 3.7/3.8)
|
|
31
31
|
from io import BytesIO
|
|
32
32
|
from pathlib import Path
|
|
33
|
-
from typing import
|
|
33
|
+
from typing import (
|
|
34
|
+
Annotated,
|
|
35
|
+
Any,
|
|
36
|
+
Callable,
|
|
37
|
+
Dict,
|
|
38
|
+
List,
|
|
39
|
+
Literal,
|
|
40
|
+
Optional,
|
|
41
|
+
Tuple,
|
|
42
|
+
Union,
|
|
43
|
+
get_args,
|
|
44
|
+
get_origin,
|
|
45
|
+
get_type_hints,
|
|
46
|
+
)
|
|
34
47
|
from urllib.parse import parse_qs, urlencode, urljoin
|
|
35
48
|
|
|
36
49
|
import aiohttp
|
|
@@ -43,7 +56,7 @@ try:
|
|
|
43
56
|
except ImportError:
|
|
44
57
|
HAS_PIL = False
|
|
45
58
|
|
|
46
|
-
VERSION = "3.0.
|
|
59
|
+
VERSION = "3.0.14"
|
|
47
60
|
_ROOT = None
|
|
48
61
|
DEBUG = os.getenv("DEBUG") == "1"
|
|
49
62
|
MOCK = os.getenv("MOCK") == "1"
|
|
@@ -348,22 +361,75 @@ def to_content(result):
|
|
|
348
361
|
return str(result)
|
|
349
362
|
|
|
350
363
|
|
|
364
|
+
def get_literal_values(typ):
|
|
365
|
+
"""Recursively extract values from Literal and Union types."""
|
|
366
|
+
origin = get_origin(typ)
|
|
367
|
+
if origin is Literal:
|
|
368
|
+
return list(get_args(typ))
|
|
369
|
+
elif origin is Union:
|
|
370
|
+
values = []
|
|
371
|
+
for arg in get_args(typ):
|
|
372
|
+
# Recurse for nested Unions or Literals
|
|
373
|
+
nested_values = get_literal_values(arg)
|
|
374
|
+
if nested_values:
|
|
375
|
+
for v in nested_values:
|
|
376
|
+
if v not in values:
|
|
377
|
+
values.append(v)
|
|
378
|
+
return values
|
|
379
|
+
return None
|
|
380
|
+
|
|
381
|
+
|
|
351
382
|
def function_to_tool_definition(func):
|
|
352
|
-
type_hints = get_type_hints(func)
|
|
383
|
+
type_hints = get_type_hints(func, include_extras=True)
|
|
353
384
|
signature = inspect.signature(func)
|
|
354
385
|
parameters = {"type": "object", "properties": {}, "required": []}
|
|
355
386
|
|
|
356
387
|
for name, param in signature.parameters.items():
|
|
357
388
|
param_type = type_hints.get(name, str)
|
|
358
389
|
param_type_name = "string"
|
|
359
|
-
|
|
390
|
+
enum_values = None
|
|
391
|
+
description = None
|
|
392
|
+
|
|
393
|
+
# Check for Annotated (for description)
|
|
394
|
+
if get_origin(param_type) is Annotated:
|
|
395
|
+
args = get_args(param_type)
|
|
396
|
+
param_type = args[0]
|
|
397
|
+
for arg in args[1:]:
|
|
398
|
+
if isinstance(arg, str):
|
|
399
|
+
description = arg
|
|
400
|
+
break
|
|
401
|
+
|
|
402
|
+
# Check for Enum
|
|
403
|
+
if inspect.isclass(param_type) and issubclass(param_type, Enum):
|
|
404
|
+
enum_values = [e.value for e in param_type]
|
|
405
|
+
else:
|
|
406
|
+
# Check for Literal / Union[Literal]
|
|
407
|
+
enum_values = get_literal_values(param_type)
|
|
408
|
+
|
|
409
|
+
if enum_values:
|
|
410
|
+
# Infer type from the first value
|
|
411
|
+
value_type = type(enum_values[0])
|
|
412
|
+
if value_type is int:
|
|
413
|
+
param_type_name = "integer"
|
|
414
|
+
elif value_type is float:
|
|
415
|
+
param_type_name = "number"
|
|
416
|
+
elif value_type is bool:
|
|
417
|
+
param_type_name = "boolean"
|
|
418
|
+
|
|
419
|
+
elif param_type is int:
|
|
360
420
|
param_type_name = "integer"
|
|
361
421
|
elif param_type is float:
|
|
362
422
|
param_type_name = "number"
|
|
363
423
|
elif param_type is bool:
|
|
364
424
|
param_type_name = "boolean"
|
|
365
425
|
|
|
366
|
-
|
|
426
|
+
prop = {"type": param_type_name}
|
|
427
|
+
if description:
|
|
428
|
+
prop["description"] = description
|
|
429
|
+
if enum_values:
|
|
430
|
+
prop["enum"] = enum_values
|
|
431
|
+
parameters["properties"][name] = prop
|
|
432
|
+
|
|
367
433
|
if param.default == inspect.Parameter.empty:
|
|
368
434
|
parameters["required"].append(name)
|
|
369
435
|
|
|
@@ -371,7 +437,7 @@ def function_to_tool_definition(func):
|
|
|
371
437
|
"type": "function",
|
|
372
438
|
"function": {
|
|
373
439
|
"name": func.__name__,
|
|
374
|
-
"description": func.__doc__ or "",
|
|
440
|
+
"description": (func.__doc__ or "").strip(),
|
|
375
441
|
"parameters": parameters,
|
|
376
442
|
},
|
|
377
443
|
}
|
|
@@ -1585,6 +1651,9 @@ async def g_chat_completion(chat, context=None):
|
|
|
1585
1651
|
if context is None:
|
|
1586
1652
|
context = {"chat": chat, "tools": "all"}
|
|
1587
1653
|
|
|
1654
|
+
if "request_id" not in context:
|
|
1655
|
+
context["request_id"] = str(int(time.time() * 1000))
|
|
1656
|
+
|
|
1588
1657
|
# get first provider that has the model
|
|
1589
1658
|
candidate_providers = [name for name, provider in g_handlers.items() if provider.provider_model(model)]
|
|
1590
1659
|
if len(candidate_providers) == 0:
|
|
@@ -1736,7 +1805,9 @@ async def g_chat_completion(chat, context=None):
|
|
|
1736
1805
|
continue
|
|
1737
1806
|
|
|
1738
1807
|
# If we get here, all providers failed
|
|
1739
|
-
|
|
1808
|
+
if first_exception:
|
|
1809
|
+
raise first_exception
|
|
1810
|
+
raise Exception("All providers failed")
|
|
1740
1811
|
|
|
1741
1812
|
|
|
1742
1813
|
async def cli_chat(chat, tools=None, image=None, audio=None, file=None, args=None, raw=False):
|
|
@@ -2918,6 +2989,9 @@ class ExtensionContext:
|
|
|
2918
2989
|
def add_index_footer(self, html: str):
|
|
2919
2990
|
self.app.index_footers.append(html)
|
|
2920
2991
|
|
|
2992
|
+
def get_home_path(self, name: str = "") -> str:
|
|
2993
|
+
return home_llms_path(name)
|
|
2994
|
+
|
|
2921
2995
|
def get_config(self) -> Optional[Dict[str, Any]]:
|
|
2922
2996
|
return g_config
|
|
2923
2997
|
|