pygpt-net 2.6.58__py3-none-any.whl → 2.6.60__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.
Files changed (72) hide show
  1. pygpt_net/CHANGELOG.txt +10 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app.py +9 -5
  4. pygpt_net/controller/__init__.py +1 -0
  5. pygpt_net/controller/presets/editor.py +442 -39
  6. pygpt_net/core/agents/custom/__init__.py +275 -0
  7. pygpt_net/core/agents/custom/debug.py +64 -0
  8. pygpt_net/core/agents/custom/factory.py +109 -0
  9. pygpt_net/core/agents/custom/graph.py +71 -0
  10. pygpt_net/core/agents/custom/llama_index/__init__.py +10 -0
  11. pygpt_net/core/agents/custom/llama_index/factory.py +89 -0
  12. pygpt_net/core/agents/custom/llama_index/router_streamer.py +106 -0
  13. pygpt_net/core/agents/custom/llama_index/runner.py +529 -0
  14. pygpt_net/core/agents/custom/llama_index/stream.py +56 -0
  15. pygpt_net/core/agents/custom/llama_index/utils.py +242 -0
  16. pygpt_net/core/agents/custom/logging.py +50 -0
  17. pygpt_net/core/agents/custom/memory.py +51 -0
  18. pygpt_net/core/agents/custom/router.py +116 -0
  19. pygpt_net/core/agents/custom/router_streamer.py +187 -0
  20. pygpt_net/core/agents/custom/runner.py +454 -0
  21. pygpt_net/core/agents/custom/schema.py +125 -0
  22. pygpt_net/core/agents/custom/utils.py +181 -0
  23. pygpt_net/core/agents/provider.py +72 -7
  24. pygpt_net/core/agents/runner.py +7 -4
  25. pygpt_net/core/agents/runners/helpers.py +1 -1
  26. pygpt_net/core/agents/runners/llama_workflow.py +3 -0
  27. pygpt_net/core/agents/runners/openai_workflow.py +8 -1
  28. pygpt_net/core/filesystem/parser.py +37 -24
  29. pygpt_net/{ui/widget/builder → core/node_editor}/__init__.py +2 -2
  30. pygpt_net/core/{builder → node_editor}/graph.py +11 -218
  31. pygpt_net/core/node_editor/models.py +111 -0
  32. pygpt_net/core/node_editor/types.py +76 -0
  33. pygpt_net/core/node_editor/utils.py +17 -0
  34. pygpt_net/core/render/web/renderer.py +10 -8
  35. pygpt_net/data/config/config.json +3 -3
  36. pygpt_net/data/config/models.json +3 -3
  37. pygpt_net/data/locale/locale.en.ini +4 -4
  38. pygpt_net/data/locale/plugin.cmd_system.en.ini +68 -0
  39. pygpt_net/item/agent.py +5 -1
  40. pygpt_net/item/preset.py +19 -1
  41. pygpt_net/plugin/cmd_system/config.py +377 -1
  42. pygpt_net/plugin/cmd_system/plugin.py +52 -8
  43. pygpt_net/plugin/cmd_system/runner.py +508 -32
  44. pygpt_net/plugin/cmd_system/winapi.py +481 -0
  45. pygpt_net/plugin/cmd_system/worker.py +88 -15
  46. pygpt_net/provider/agents/base.py +33 -2
  47. pygpt_net/provider/agents/llama_index/flow_from_schema.py +92 -0
  48. pygpt_net/provider/agents/llama_index/workflow/supervisor.py +0 -0
  49. pygpt_net/provider/agents/openai/flow_from_schema.py +96 -0
  50. pygpt_net/provider/core/agent/json_file.py +11 -5
  51. pygpt_net/provider/llms/openai.py +6 -4
  52. pygpt_net/tools/agent_builder/tool.py +217 -52
  53. pygpt_net/tools/agent_builder/ui/dialogs.py +119 -24
  54. pygpt_net/tools/agent_builder/ui/list.py +37 -10
  55. pygpt_net/tools/code_interpreter/ui/html.py +2 -1
  56. pygpt_net/ui/dialog/preset.py +16 -1
  57. pygpt_net/ui/main.py +1 -1
  58. pygpt_net/{core/builder → ui/widget/node_editor}/__init__.py +2 -2
  59. pygpt_net/ui/widget/node_editor/command.py +373 -0
  60. pygpt_net/ui/widget/node_editor/editor.py +2038 -0
  61. pygpt_net/ui/widget/node_editor/item.py +492 -0
  62. pygpt_net/ui/widget/node_editor/node.py +1205 -0
  63. pygpt_net/ui/widget/node_editor/utils.py +17 -0
  64. pygpt_net/ui/widget/node_editor/view.py +247 -0
  65. pygpt_net/ui/widget/textarea/web.py +1 -1
  66. {pygpt_net-2.6.58.dist-info → pygpt_net-2.6.60.dist-info}/METADATA +135 -61
  67. {pygpt_net-2.6.58.dist-info → pygpt_net-2.6.60.dist-info}/RECORD +69 -42
  68. pygpt_net/core/agents/custom.py +0 -150
  69. pygpt_net/ui/widget/builder/editor.py +0 -2001
  70. {pygpt_net-2.6.58.dist-info → pygpt_net-2.6.60.dist-info}/LICENSE +0 -0
  71. {pygpt_net-2.6.58.dist-info → pygpt_net-2.6.60.dist-info}/WHEEL +0 -0
  72. {pygpt_net-2.6.58.dist-info → pygpt_net-2.6.60.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,481 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # ================================================== #
4
+ # This file is a part of PYGPT package #
5
+ # Website: https://pygpt.net #
6
+ # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
+ # MIT License #
8
+ # Created By : Marcin Szczygliński #
9
+ # Updated Date: 2025.09.23 07:00:00 #
10
+ # ================================================== #
11
+
12
+ import platform
13
+ import ctypes
14
+ from ctypes import wintypes
15
+ from typing import Dict, List, Optional, Tuple
16
+
17
+ # Guard for non-Windows platforms: provide stubs that fail at call-time
18
+ if platform.system() != "Windows":
19
+ class WinAPI:
20
+ def __init__(self): raise RuntimeError("Windows only")
21
+ class InputSender:
22
+ def __init__(self): raise RuntimeError("Windows only")
23
+ else:
24
+ user32 = ctypes.windll.user32
25
+ kernel32 = ctypes.windll.kernel32
26
+ psapi = ctypes.windll.psapi
27
+ gdi32 = ctypes.windll.gdi32
28
+
29
+ # Constants
30
+ GWL_EXSTYLE = -20
31
+ WS_EX_LAYERED = 0x00080000
32
+ LWA_ALPHA = 0x00000002
33
+
34
+ SW_HIDE = 0
35
+ SW_SHOWNORMAL = 1
36
+ SW_SHOWMINIMIZED = 2
37
+ SW_SHOWMAXIMIZED = 3
38
+ SW_RESTORE = 9
39
+
40
+ SWP_NOSIZE = 0x0001
41
+ SWP_NOMOVE = 0x0002
42
+ SWP_NOZORDER = 0x0004
43
+ SWP_NOACTIVATE = 0x0010
44
+ HWND_TOP = 0
45
+ HWND_TOPMOST = -1
46
+ HWND_NOTOPMOST = -2
47
+
48
+ WM_CLOSE = 0x0010
49
+ SMTO_BLOCK = 0x0001
50
+ SMTO_ABORTIFHUNG = 0x0002
51
+ SEND_TIMEOUT = 2000
52
+
53
+ PROCESS_QUERY_INFORMATION = 0x0400
54
+ PROCESS_VM_READ = 0x0010
55
+ PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
56
+
57
+ # Input constants
58
+ INPUT_MOUSE = 0
59
+ INPUT_KEYBOARD = 1
60
+
61
+ KEYEVENTF_KEYUP = 0x0002
62
+ KEYEVENTF_UNICODE = 0x0004
63
+
64
+ MOUSEEVENTF_MOVE = 0x0001
65
+ MOUSEEVENTF_LEFTDOWN = 0x0002
66
+ MOUSEEVENTF_LEFTUP = 0x0004
67
+ MOUSEEVENTF_RIGHTDOWN = 0x0008
68
+ MOUSEEVENTF_RIGHTUP = 0x0010
69
+ MOUSEEVENTF_MIDDLEDOWN = 0x0020
70
+ MOUSEEVENTF_MIDDLEUP = 0x0040
71
+
72
+ # VK map
73
+ VK = {
74
+ "SHIFT": 0x10, "CTRL": 0x11, "ALT": 0x12, "WIN": 0x5B,
75
+ "ENTER": 0x0D, "ESC": 0x1B, "TAB": 0x09, "SPACE": 0x20, "BACKSPACE": 0x08, "DELETE": 0x2E,
76
+ "UP": 0x26, "DOWN": 0x28, "LEFT": 0x25, "RIGHT": 0x27,
77
+ "HOME": 0x24, "END": 0x23, "PGUP": 0x21, "PGDN": 0x22,
78
+ "F1": 0x70, "F2": 0x71, "F3": 0x72, "F4": 0x73, "F5": 0x74, "F6": 0x75,
79
+ "F7": 0x76, "F8": 0x77, "F9": 0x78, "F10": 0x79, "F11": 0x7A, "F12": 0x7B,
80
+ "F13": 0x7C, "F14": 0x7D, "F15": 0x7E, "F16": 0x7F, "F17": 0x80, "F18": 0x81,
81
+ "F19": 0x82, "F20": 0x83, "F21": 0x84, "F22": 0x85, "F23": 0x86, "F24": 0x87,
82
+ }
83
+
84
+ if hasattr(wintypes, "ULONG_PTR"):
85
+ ULONG_PTR = wintypes.ULONG_PTR
86
+ else:
87
+ ULONG_PTR = ctypes.c_ulonglong if ctypes.sizeof(ctypes.c_void_p) == 8 else ctypes.c_ulong
88
+
89
+ if ctypes.sizeof(ctypes.c_void_p) == 8:
90
+ LONG_PTR = ctypes.c_longlong
91
+ else:
92
+ LONG_PTR = ctypes.c_long
93
+
94
+ # Structures
95
+ class KEYBDINPUT(ctypes.Structure):
96
+ _fields_ = [
97
+ ("wVk", wintypes.WORD),
98
+ ("wScan", wintypes.WORD),
99
+ ("dwFlags", wintypes.DWORD),
100
+ ("time", wintypes.DWORD),
101
+ ("dwExtraInfo", ULONG_PTR),
102
+ ]
103
+
104
+ class MOUSEINPUT(ctypes.Structure):
105
+ _fields_ = [
106
+ ("dx", wintypes.LONG),
107
+ ("dy", wintypes.LONG),
108
+ ("mouseData", wintypes.DWORD),
109
+ ("dwFlags", wintypes.DWORD),
110
+ ("time", wintypes.DWORD),
111
+ ("dwExtraInfo", ULONG_PTR),
112
+ ]
113
+
114
+ class HARDWAREINPUT(ctypes.Structure):
115
+ _fields_ = [
116
+ ("uMsg", wintypes.DWORD),
117
+ ("wParamL", wintypes.WORD),
118
+ ("wParamH", wintypes.WORD),
119
+ ]
120
+
121
+ class INPUT_union(ctypes.Union):
122
+ _fields_ = [("ki", KEYBDINPUT), ("mi", MOUSEINPUT), ("hi", HARDWAREINPUT)]
123
+
124
+ class INPUT(ctypes.Structure):
125
+ _fields_ = [("type", wintypes.DWORD), ("union", INPUT_union)]
126
+
127
+ # Function prototypes (partial)
128
+ EnumWindowsProc = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
129
+ EnumChildProc = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
130
+
131
+ user32.EnumWindows.restype = wintypes.BOOL
132
+ user32.EnumWindows.argtypes = (EnumWindowsProc, wintypes.LPARAM)
133
+ user32.EnumChildWindows.restype = wintypes.BOOL
134
+ user32.EnumChildWindows.argtypes = (wintypes.HWND, EnumChildProc, wintypes.LPARAM)
135
+
136
+ user32.SendInput.restype = wintypes.UINT
137
+ user32.SendInput.argtypes = (wintypes.UINT, ctypes.POINTER(INPUT), ctypes.c_int)
138
+
139
+ # Helpers for Get/SetWindowLongPtr
140
+ try:
141
+ GetWindowLongPtrW = user32.GetWindowLongPtrW
142
+ SetWindowLongPtrW = user32.SetWindowLongPtrW
143
+ GetWindowLongPtrW.restype = LONG_PTR
144
+ GetWindowLongPtrW.argtypes = (wintypes.HWND, ctypes.c_int)
145
+ SetWindowLongPtrW.restype = LONG_PTR
146
+ SetWindowLongPtrW.argtypes = (wintypes.HWND, ctypes.c_int, LONG_PTR)
147
+ _use_long_ptr = True
148
+ except AttributeError:
149
+ GetWindowLongW = user32.GetWindowLongW
150
+ SetWindowLongW = user32.SetWindowLongW
151
+ GetWindowLongW.restype = ctypes.c_long
152
+ GetWindowLongW.argtypes = (wintypes.HWND, ctypes.c_int)
153
+ SetWindowLongW.restype = ctypes.c_long
154
+ SetWindowLongW.argtypes = (wintypes.HWND, ctypes.c_int, ctypes.c_long)
155
+ _use_long_ptr = False
156
+
157
+ def _get_window_text(hwnd: int) -> str:
158
+ buf = ctypes.create_unicode_buffer(512)
159
+ user32.GetWindowTextW(wintypes.HWND(hwnd), buf, 512)
160
+ return buf.value or ""
161
+
162
+ def _get_class_name(hwnd: int) -> str:
163
+ buf = ctypes.create_unicode_buffer(256)
164
+ user32.GetClassNameW(wintypes.HWND(hwnd), buf, 256)
165
+ return buf.value or ""
166
+
167
+ def _get_pid_exe(hwnd: int) -> Tuple[int, Optional[str]]:
168
+ pid = wintypes.DWORD()
169
+ user32.GetWindowThreadProcessId(wintypes.HWND(hwnd), ctypes.byref(pid))
170
+ exe_path = None
171
+ access = PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_VM_READ | PROCESS_QUERY_INFORMATION
172
+ hProcess = kernel32.OpenProcess(access, False, pid.value)
173
+ if hProcess:
174
+ try:
175
+ buf_len = wintypes.DWORD(4096)
176
+ buf = ctypes.create_unicode_buffer(4096)
177
+ QueryFullProcessImageNameW = getattr(kernel32, "QueryFullProcessImageNameW", None)
178
+ if QueryFullProcessImageNameW:
179
+ if QueryFullProcessImageNameW(hProcess, 0, buf, ctypes.byref(buf_len)):
180
+ exe_path = buf.value
181
+ if not exe_path:
182
+ GetModuleFileNameExW = getattr(psapi, "GetModuleFileNameExW", None)
183
+ if GetModuleFileNameExW:
184
+ if GetModuleFileNameExW(hProcess, 0, buf, 4096):
185
+ exe_path = buf.value
186
+ finally:
187
+ kernel32.CloseHandle(hProcess)
188
+ return pid.value, exe_path
189
+
190
+ def _get_rect(hwnd: int) -> Dict:
191
+ rect = wintypes.RECT()
192
+ user32.GetWindowRect(wintypes.HWND(hwnd), ctypes.byref(rect))
193
+ width = rect.right - rect.left
194
+ height = rect.bottom - rect.top
195
+ return {"left": rect.left, "top": rect.top, "right": rect.right, "bottom": rect.bottom,
196
+ "width": width, "height": height}
197
+
198
+ def _is_visible(hwnd: int) -> bool:
199
+ return bool(user32.IsWindowVisible(wintypes.HWND(hwnd)))
200
+
201
+ def _is_minimized(hwnd: int) -> bool:
202
+ return bool(user32.IsIconic(wintypes.HWND(hwnd)))
203
+
204
+ class WinAPI:
205
+ def enum_windows(self, visible_only: bool = True) -> List[Dict]:
206
+ items: List[Dict] = []
207
+
208
+ @EnumWindowsProc
209
+ def callback(hWnd, lParam):
210
+ try:
211
+ if visible_only and not _is_visible(hWnd):
212
+ return True
213
+ title = _get_window_text(hWnd)
214
+ cls = _get_class_name(hWnd)
215
+ pid, exe = _get_pid_exe(hWnd)
216
+ rect = _get_rect(hWnd)
217
+ minimized = _is_minimized(hWnd)
218
+ items.append({
219
+ "hwnd": int(hWnd),
220
+ "title": title,
221
+ "class_name": cls,
222
+ "pid": pid,
223
+ "exe": exe or "",
224
+ "rect": rect,
225
+ "is_visible": _is_visible(hWnd),
226
+ "is_minimized": minimized,
227
+ })
228
+ except Exception:
229
+ return True
230
+ return True
231
+
232
+ user32.EnumWindows(callback, 0)
233
+ return items
234
+
235
+ def enum_child_windows(self, parent_hwnd: int) -> List[Dict]:
236
+ items: List[Dict] = []
237
+
238
+ @EnumChildProc
239
+ def callback(hChild, lParam):
240
+ try:
241
+ title = _get_window_text(hChild)
242
+ cls = _get_class_name(hChild)
243
+ pid, exe = _get_pid_exe(hChild)
244
+ rect = _get_rect(hChild)
245
+ items.append({
246
+ "hwnd": int(hChild),
247
+ "title": title,
248
+ "class_name": cls,
249
+ "pid": pid,
250
+ "exe": exe or "",
251
+ "rect": rect,
252
+ "is_visible": _is_visible(hChild),
253
+ "is_minimized": _is_minimized(hChild),
254
+ })
255
+ except Exception:
256
+ return True
257
+ return True
258
+
259
+ user32.EnumChildWindows(wintypes.HWND(parent_hwnd), callback, 0)
260
+ return items
261
+
262
+ def is_window(self, hwnd: int) -> bool:
263
+ return bool(user32.IsWindow(wintypes.HWND(hwnd)))
264
+
265
+ def get_window_info(self, hwnd: int) -> Dict:
266
+ return {
267
+ "hwnd": int(hwnd),
268
+ "title": _get_window_text(hwnd),
269
+ "class_name": _get_class_name(hwnd),
270
+ "pid": _get_pid_exe(hwnd)[0],
271
+ "exe": _get_pid_exe(hwnd)[1] or "",
272
+ "rect": _get_rect(hwnd),
273
+ "is_visible": _is_visible(hwnd),
274
+ "is_minimized": _is_minimized(hwnd),
275
+ }
276
+
277
+ def bring_to_foreground(self, hwnd: int) -> Tuple[bool, str]:
278
+ if _is_minimized(hwnd):
279
+ user32.ShowWindow(wintypes.HWND(hwnd), SW_RESTORE)
280
+ if user32.SetForegroundWindow(wintypes.HWND(hwnd)):
281
+ return True, "SetForegroundWindow succeeded."
282
+ fg = user32.GetForegroundWindow()
283
+ if fg:
284
+ target_tid = user32.GetWindowThreadProcessId(wintypes.HWND(hwnd), None)
285
+ fg_tid = user32.GetWindowThreadProcessId(wintypes.HWND(fg), None)
286
+ cur_tid = kernel32.GetCurrentThreadId()
287
+ user32.AttachThreadInput(fg_tid, cur_tid, True)
288
+ user32.AttachThreadInput(target_tid, cur_tid, True)
289
+ user32.BringWindowToTop(wintypes.HWND(hwnd))
290
+ ok = user32.SetForegroundWindow(wintypes.HWND(hwnd))
291
+ user32.AttachThreadInput(fg_tid, cur_tid, False)
292
+ user32.AttachThreadInput(target_tid, cur_tid, False)
293
+ if ok:
294
+ return True, "Foreground set via AttachThreadInput."
295
+ return False, "Could not bring window to foreground (OS prevented focus change)."
296
+
297
+ def move_resize(self, hwnd: int,
298
+ x: Optional[int], y: Optional[int],
299
+ width: Optional[int], height: Optional[int]) -> Tuple[bool, str]:
300
+ rect = _get_rect(hwnd)
301
+ new_x = rect["left"] if x is None else int(x)
302
+ new_y = rect["top"] if y is None else int(y)
303
+ new_w = rect["width"] if width is None else max(1, int(width))
304
+ new_h = rect["height"] if height is None else max(1, int(height))
305
+ ok = user32.SetWindowPos(wintypes.HWND(hwnd), HWND_TOP, new_x, new_y, new_w, new_h, 0)
306
+ return (ok != 0), f"Moved to ({new_x},{new_y}) size ({new_w}x{new_h})."
307
+
308
+ def show_window(self, hwnd: int, state: str) -> Tuple[bool, str]:
309
+ m = {
310
+ "minimize": SW_SHOWMINIMIZED,
311
+ "maximize": SW_SHOWMAXIMIZED,
312
+ "restore": SW_RESTORE,
313
+ "show": SW_SHOWNORMAL,
314
+ "hide": SW_HIDE,
315
+ }
316
+ if state not in m:
317
+ return False, f"Invalid state: {state}"
318
+ ok = user32.ShowWindow(wintypes.HWND(hwnd), m[state])
319
+ return (ok != 0), f"ShowWindow {state}"
320
+
321
+ def close_window(self, hwnd: int) -> Tuple[bool, str]:
322
+ res = user32.SendMessageTimeoutW(
323
+ wintypes.HWND(hwnd), WM_CLOSE, 0, 0,
324
+ SMTO_BLOCK | SMTO_ABORTIFHUNG, SEND_TIMEOUT, None
325
+ )
326
+ return (res != 0), "WM_CLOSE sent."
327
+
328
+ def set_topmost(self, hwnd: int, topmost: bool) -> Tuple[bool, str]:
329
+ flag = HWND_TOPMOST if topmost else HWND_NOTOPMOST
330
+ ok = user32.SetWindowPos(wintypes.HWND(hwnd), flag, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE)
331
+ return (ok != 0), ("Topmost set" if topmost else "Topmost cleared")
332
+
333
+ def _get_exstyle(self, hwnd: int) -> int:
334
+ if '_use_long_ptr' in globals() and _use_long_ptr:
335
+ return int(GetWindowLongPtrW(wintypes.HWND(hwnd), GWL_EXSTYLE))
336
+ return int(GetWindowLongW(wintypes.HWND(hwnd), GWL_EXSTYLE))
337
+
338
+ def _set_exstyle(self, hwnd: int, val: int) -> int:
339
+ if '_use_long_ptr' in globals() and _use_long_ptr:
340
+ return int(SetWindowLongPtrW(wintypes.HWND(hwnd), GWL_EXSTYLE, val))
341
+ return int(SetWindowLongW(wintypes.HWND(hwnd), GWL_EXSTYLE, val))
342
+
343
+ def set_opacity(self, hwnd: int, alpha: int) -> Tuple[bool, str]:
344
+ ex = self._get_exstyle(hwnd)
345
+ if (ex & WS_EX_LAYERED) == 0:
346
+ self._set_exstyle(hwnd, ex | WS_EX_LAYERED)
347
+ ok = user32.SetLayeredWindowAttributes(wintypes.HWND(hwnd), 0, wintypes.BYTE(alpha), LWA_ALPHA)
348
+ return (ok != 0), f"Opacity set to alpha={alpha}"
349
+
350
+ def get_foreground_window(self) -> Optional[int]:
351
+ h = user32.GetForegroundWindow()
352
+ return int(h) if h else None
353
+
354
+ def get_window_rect(self, hwnd: int) -> Dict:
355
+ return _get_rect(hwnd)
356
+
357
+ def get_cursor_pos(self) -> Tuple[int, int]:
358
+ pt = wintypes.POINT()
359
+ user32.GetCursorPos(ctypes.byref(pt))
360
+ return pt.x, pt.y
361
+
362
+ def set_cursor_pos(self, x: int, y: int) -> bool:
363
+ return bool(user32.SetCursorPos(int(x), int(y)))
364
+
365
+ class InputSender:
366
+ def __init__(self):
367
+ pass
368
+
369
+ def _send_input(self, inputs):
370
+ n = len(inputs)
371
+ arr = (INPUT * n)(*inputs)
372
+ sent = user32.SendInput(n, arr, ctypes.sizeof(INPUT))
373
+ return sent == n
374
+
375
+ # ---- Keyboard ----
376
+ def send_unicode_text(self, text: str, per_char_delay_ms: int = 2):
377
+ for ch in text:
378
+ code = ord(ch)
379
+ down = INPUT(type=INPUT_KEYBOARD, union=INPUT_union(ki=KEYBDINPUT(wVk=0, wScan=code, dwFlags=KEYEVENTF_UNICODE, time=0, dwExtraInfo=0)))
380
+ up = INPUT(type=INPUT_KEYBOARD, union=INPUT_union(ki=KEYBDINPUT(wVk=0, wScan=code, dwFlags=KEYEVENTF_UNICODE | KEYEVENTF_KEYUP, time=0, dwExtraInfo=0)))
381
+ self._send_input([down, up])
382
+ if per_char_delay_ms > 0:
383
+ kernel32.Sleep(per_char_delay_ms)
384
+
385
+ def send_keys(self, keys: List[str], hold_ms: int = 50, gap_ms: int = 30) -> Tuple[bool, str]:
386
+ tokens = [str(k).strip().upper() for k in keys if str(k).strip()]
387
+ if not tokens:
388
+ return False, "No key tokens."
389
+ modifiers = [t for t in tokens if t in ("CTRL", "ALT", "SHIFT", "WIN")]
390
+ normals = [t for t in tokens if t not in modifiers]
391
+
392
+ def mk_down(vk):
393
+ return INPUT(type=INPUT_KEYBOARD, union=INPUT_union(ki=KEYBDINPUT(wVk=vk, wScan=0, dwFlags=0, time=0, dwExtraInfo=0)))
394
+
395
+ def mk_up(vk):
396
+ return INPUT(type=INPUT_KEYBOARD, union=INPUT_union(ki=KEYBDINPUT(wVk=vk, wScan=0, dwFlags=KEYEVENTF_KEYUP, time=0, dwExtraInfo=0)))
397
+
398
+ for mod in modifiers:
399
+ vk = VK.get(mod)
400
+ if vk is None: return False, f"Unsupported modifier: {mod}"
401
+ if not self._send_input([mk_down(vk)]):
402
+ return False, f"Failed to press modifier: {mod}"
403
+
404
+ for key in normals:
405
+ vk = None
406
+ if key in VK:
407
+ vk = VK[key]
408
+ elif len(key) == 1:
409
+ c = key.upper()
410
+ if 'A' <= c <= 'Z' or '0' <= c <= '9':
411
+ vk = ord(c)
412
+ if vk is None:
413
+ for mod in reversed(modifiers):
414
+ vkmod = VK.get(mod)
415
+ if vkmod is not None:
416
+ self._send_input([mk_up(vkmod)])
417
+ return False, f"Unsupported key token: {key}"
418
+
419
+ if not self._send_input([mk_down(vk), mk_up(vk)]):
420
+ for mod in reversed(modifiers):
421
+ vkmod = VK.get(mod)
422
+ if vkmod is not None:
423
+ self._send_input([mk_up(vkmod)])
424
+ return False, f"Failed to tap: {key}"
425
+ if gap_ms > 0:
426
+ kernel32.Sleep(gap_ms)
427
+
428
+ for mod in reversed(modifiers):
429
+ vk = VK.get(mod)
430
+ if vk is not None:
431
+ self._send_input([mk_up(vk)])
432
+
433
+ return True, "Keys sent."
434
+
435
+ # ---- Mouse ----
436
+ def _mouse_event(self, flags: int):
437
+ mi = MOUSEINPUT(dx=0, dy=0, mouseData=0, dwFlags=flags, time=0, dwExtraInfo=0)
438
+ inp = INPUT(type=INPUT_MOUSE, union=INPUT_union(mi=mi))
439
+ return self._send_input([inp])
440
+
441
+ def move_cursor(self, x: int, y: int) -> bool:
442
+ return bool(user32.SetCursorPos(int(x), int(y)))
443
+
444
+ def click_at(self, x: int, y: int, button: str = "left", double: bool = False) -> Tuple[bool, str]:
445
+ self.move_cursor(x, y)
446
+ btn = (button or "left").lower()
447
+ if btn == "left":
448
+ down, up = MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP
449
+ elif btn == "right":
450
+ down, up = MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP
451
+ elif btn == "middle":
452
+ down, up = MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP
453
+ else:
454
+ return False, f"Unsupported button: {button}"
455
+ if not self._mouse_event(down) or not self._mouse_event(up):
456
+ return False, "Mouse click failed."
457
+ if double:
458
+ kernel32.Sleep(80)
459
+ if not self._mouse_event(down) or not self._mouse_event(up):
460
+ return False, "Mouse double-click failed."
461
+ return True, "Click sent."
462
+
463
+ def drag_and_drop(self, x1: int, y1: int, x2: int, y2: int, steps: int = 20, step_delay_ms: int = 10) -> Tuple[bool, str]:
464
+ self.move_cursor(x1, y1)
465
+ if not self._mouse_event(MOUSEEVENTF_LEFTDOWN):
466
+ return False, "Mouse down failed."
467
+ try:
468
+ if steps < 1:
469
+ steps = 1
470
+ dx = (x2 - x1) / float(steps)
471
+ dy = (y2 - y1) / float(steps)
472
+ cx, cy = float(x1), float(y1)
473
+ for i in range(steps):
474
+ cx += dx
475
+ cy += dy
476
+ self.move_cursor(int(cx), int(cy))
477
+ if step_delay_ms > 0:
478
+ kernel32.Sleep(step_delay_ms)
479
+ finally:
480
+ self._mouse_event(MOUSEEVENTF_LEFTUP)
481
+ return True, "Drag-and-drop completed."
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.08.11 14:00:00 #
9
+ # Updated Date: 2025.09.23 07:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtCore import Slot, Signal
@@ -44,9 +44,75 @@ class Worker(BaseWorker):
44
44
  if (item["cmd"] in self.plugin.allowed_cmds
45
45
  and (self.plugin.has_cmd(item["cmd"]) or 'force' in item)):
46
46
 
47
- if item["cmd"] == "sys_exec":
47
+ cmd = item["cmd"]
48
+ params = item.get("params", {}) or {}
49
+
50
+ # System command
51
+ if cmd == "sys_exec":
48
52
  response = self.cmd_sys_exec(item)
49
53
 
54
+ # WinAPI commands
55
+ elif cmd == "win_list":
56
+ response = self._wrap(item, self.plugin.runner.win_list, params)
57
+ elif cmd == "win_find":
58
+ response = self._wrap(item, self.plugin.runner.win_find, params)
59
+ elif cmd == "win_children":
60
+ response = self._wrap(item, self.plugin.runner.win_children, params)
61
+ elif cmd == "win_foreground":
62
+ response = self._wrap(item, self.plugin.runner.win_foreground, params)
63
+ elif cmd == "win_rect":
64
+ response = self._wrap(item, self.plugin.runner.win_rect, params)
65
+ elif cmd == "win_get_state":
66
+ response = self._wrap(item, self.plugin.runner.win_get_state, params)
67
+
68
+ elif cmd == "win_focus":
69
+ response = self._wrap(item, self.plugin.runner.win_focus, params)
70
+ elif cmd == "win_move_resize":
71
+ response = self._wrap(item, self.plugin.runner.win_move_resize, params)
72
+ elif cmd == "win_minimize":
73
+ response = self._wrap(item, self.plugin.runner.win_minimize, params)
74
+ elif cmd == "win_maximize":
75
+ response = self._wrap(item, self.plugin.runner.win_maximize, params)
76
+ elif cmd == "win_restore":
77
+ response = self._wrap(item, self.plugin.runner.win_restore, params)
78
+ elif cmd == "win_close":
79
+ response = self._wrap(item, self.plugin.runner.win_close, params)
80
+ elif cmd == "win_show":
81
+ response = self._wrap(item, self.plugin.runner.win_show, params)
82
+ elif cmd == "win_hide":
83
+ response = self._wrap(item, self.plugin.runner.win_hide, params)
84
+ elif cmd == "win_always_on_top":
85
+ response = self._wrap(item, self.plugin.runner.win_always_on_top, params)
86
+ elif cmd == "win_set_opacity":
87
+ response = self._wrap(item, self.plugin.runner.win_set_opacity, params)
88
+
89
+ elif cmd == "win_screenshot":
90
+ response = self._wrap(item, self.plugin.runner.win_screenshot, params)
91
+ elif cmd == "win_area_screenshot":
92
+ response = self._wrap(item, self.plugin.runner.win_area_screenshot, params)
93
+
94
+ elif cmd == "win_clipboard_get":
95
+ response = self._wrap(item, self.plugin.runner.win_clipboard_get, params)
96
+ elif cmd == "win_clipboard_set":
97
+ response = self._wrap(item, self.plugin.runner.win_clipboard_set, params)
98
+
99
+ elif cmd == "win_cursor_get":
100
+ response = self._wrap(item, self.plugin.runner.win_cursor_get, params)
101
+ elif cmd == "win_cursor_set":
102
+ response = self._wrap(item, self.plugin.runner.win_cursor_set, params)
103
+
104
+ elif cmd == "win_keys_text":
105
+ response = self._wrap(item, self.plugin.runner.win_keys_text, params)
106
+ elif cmd == "win_keys_send":
107
+ response = self._wrap(item, self.plugin.runner.win_keys_send, params)
108
+ elif cmd == "win_click":
109
+ response = self._wrap(item, self.plugin.runner.win_click, params)
110
+ elif cmd == "win_drag":
111
+ response = self._wrap(item, self.plugin.runner.win_drag, params)
112
+
113
+ elif cmd == "win_monitors":
114
+ response = self._wrap(item, self.plugin.runner.win_monitors, params)
115
+
50
116
  if response:
51
117
  responses.append(response)
52
118
 
@@ -69,9 +135,6 @@ class Worker(BaseWorker):
69
135
  def cmd_sys_exec(self, item: dict) -> dict:
70
136
  """
71
137
  Execute system command
72
-
73
- :param item: command item
74
- :return: response item
75
138
  """
76
139
  request = self.from_request(item)
77
140
  try:
@@ -93,13 +156,20 @@ class Worker(BaseWorker):
93
156
  extra = self.prepare_extra(item, result)
94
157
  return self.make_response(item, result, extra=extra)
95
158
 
159
+ def _wrap(self, item: dict, fn, params: dict) -> dict:
160
+ """Execute runner function and adapt to response format"""
161
+ request = self.from_request(item)
162
+ try:
163
+ result = fn(**(params or {}))
164
+ except Exception as e:
165
+ result = self.throw_error(e)
166
+
167
+ extra = self.prepare_extra(item, result)
168
+ return self.make_response(item, result, extra=extra)
169
+
96
170
  def prepare_extra(self, item: dict, result: dict) -> dict:
97
171
  """
98
172
  Prepare extra data for response
99
-
100
- :param item: command item
101
- :param result: response data
102
- :return: extra data
103
173
  """
104
174
  cmd = item["cmd"]
105
175
  extra = {
@@ -107,19 +177,22 @@ class Worker(BaseWorker):
107
177
  'cmd': cmd,
108
178
  'code': {}
109
179
  }
110
- lang = "python"
180
+ # Language hint for renderer
111
181
  if cmd in ["sys_exec"]:
112
182
  lang = "bash"
183
+ elif cmd in ["win_keys_text"]:
184
+ lang = "text"
185
+ else:
186
+ lang = "json"
187
+
113
188
  if "params" in item and "code" in item["params"]:
114
189
  extra["code"]["input"] = {}
115
190
  extra["code"]["input"]["lang"] = lang
116
191
  extra["code"]["input"]["content"] = str(item["params"]["code"])
117
- if "result" in result:
192
+ if isinstance(result, dict) and "result" in result:
118
193
  extra["code"]["output"] = {}
119
194
  extra["code"]["output"]["lang"] = lang
120
195
  extra["code"]["output"]["content"] = str(result["result"])
121
- if "context" in result:
196
+ if isinstance(result, dict) and "context" in result:
122
197
  extra["context"] = str(result["context"])
123
- return extra
124
-
125
-
198
+ return extra
@@ -21,6 +21,9 @@ class BaseAgent:
21
21
  self.type = ""
22
22
  self.mode = ""
23
23
  self.name = ""
24
+ self.custom_id = None
25
+ self.custom_options = None
26
+ self.custom_schema = None
24
27
 
25
28
  def get_mode(self) -> str:
26
29
  """
@@ -44,12 +47,39 @@ class BaseAgent:
44
47
  """
45
48
  pass
46
49
 
50
+ def set_id(self, id: str):
51
+ """
52
+ Set custom ID for the agent
53
+
54
+ :param id: Custom ID
55
+ """
56
+ self.custom_id = id
57
+
58
+ def set_schema(self, schema: list):
59
+ """
60
+ Set custom schema for the agent
61
+
62
+ :param schema: Custom schema
63
+ """
64
+ self.custom_schema = schema
65
+
66
+
67
+ def set_options(self, options: dict):
68
+ """
69
+ Set custom options for the agent
70
+
71
+ :param options: Custom options
72
+ """
73
+ self.custom_options = options
74
+
47
75
  def get_options(self) -> dict:
48
76
  """
49
77
  Return Agent options
50
78
 
51
79
  :return: Agent options
52
80
  """
81
+ if self.custom_options is not None:
82
+ return self.custom_options
53
83
  return {}
54
84
 
55
85
  async def run(
@@ -89,9 +119,10 @@ class BaseAgent:
89
119
  print("No preset provided, returning default option value")
90
120
  return None
91
121
  extra = preset.extra
92
- if not isinstance(extra, dict) or self.id not in extra:
122
+ agent_id = self.custom_id if self.custom_id is not None else self.id
123
+ if not isinstance(extra, dict) or agent_id not in extra:
93
124
  return self.get_default(section, key)
94
- options = extra[self.id]
125
+ options = extra[agent_id]
95
126
  if section not in options:
96
127
  return self.get_default(section, key)
97
128
  if key not in options[section]: