npcsh 1.1.12__py3-none-any.whl → 1.1.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.
Files changed (99) hide show
  1. npcsh/_state.py +700 -377
  2. npcsh/alicanto.py +54 -1153
  3. npcsh/completion.py +206 -0
  4. npcsh/config.py +163 -0
  5. npcsh/corca.py +35 -1462
  6. npcsh/execution.py +185 -0
  7. npcsh/guac.py +31 -1986
  8. npcsh/npc_team/jinxs/code/sh.jinx +11 -15
  9. npcsh/npc_team/jinxs/modes/alicanto.jinx +186 -80
  10. npcsh/npc_team/jinxs/modes/corca.jinx +243 -22
  11. npcsh/npc_team/jinxs/modes/guac.jinx +313 -42
  12. npcsh/npc_team/jinxs/modes/plonk.jinx +209 -48
  13. npcsh/npc_team/jinxs/modes/pti.jinx +167 -25
  14. npcsh/npc_team/jinxs/modes/spool.jinx +158 -37
  15. npcsh/npc_team/jinxs/modes/wander.jinx +179 -74
  16. npcsh/npc_team/jinxs/modes/yap.jinx +258 -21
  17. npcsh/npc_team/jinxs/utils/chat.jinx +39 -12
  18. npcsh/npc_team/jinxs/utils/cmd.jinx +44 -0
  19. npcsh/npc_team/jinxs/utils/search.jinx +3 -3
  20. npcsh/npc_team/jinxs/utils/usage.jinx +33 -0
  21. npcsh/npcsh.py +76 -20
  22. npcsh/parsing.py +118 -0
  23. npcsh/plonk.py +41 -329
  24. npcsh/pti.py +41 -201
  25. npcsh/spool.py +34 -239
  26. npcsh/ui.py +199 -0
  27. npcsh/wander.py +54 -542
  28. npcsh/yap.py +38 -570
  29. npcsh-1.1.14.data/data/npcsh/npc_team/alicanto.jinx +194 -0
  30. npcsh-1.1.14.data/data/npcsh/npc_team/chat.jinx +44 -0
  31. npcsh-1.1.14.data/data/npcsh/npc_team/cmd.jinx +44 -0
  32. npcsh-1.1.14.data/data/npcsh/npc_team/corca.jinx +249 -0
  33. npcsh-1.1.14.data/data/npcsh/npc_team/guac.jinx +317 -0
  34. npcsh-1.1.14.data/data/npcsh/npc_team/plonk.jinx +214 -0
  35. npcsh-1.1.14.data/data/npcsh/npc_team/pti.jinx +170 -0
  36. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/search.jinx +3 -3
  37. npcsh-1.1.14.data/data/npcsh/npc_team/sh.jinx +34 -0
  38. npcsh-1.1.14.data/data/npcsh/npc_team/spool.jinx +161 -0
  39. npcsh-1.1.14.data/data/npcsh/npc_team/usage.jinx +33 -0
  40. npcsh-1.1.14.data/data/npcsh/npc_team/wander.jinx +186 -0
  41. npcsh-1.1.14.data/data/npcsh/npc_team/yap.jinx +262 -0
  42. {npcsh-1.1.12.dist-info → npcsh-1.1.14.dist-info}/METADATA +1 -1
  43. npcsh-1.1.14.dist-info/RECORD +135 -0
  44. npcsh-1.1.12.data/data/npcsh/npc_team/alicanto.jinx +0 -88
  45. npcsh-1.1.12.data/data/npcsh/npc_team/chat.jinx +0 -17
  46. npcsh-1.1.12.data/data/npcsh/npc_team/corca.jinx +0 -28
  47. npcsh-1.1.12.data/data/npcsh/npc_team/guac.jinx +0 -46
  48. npcsh-1.1.12.data/data/npcsh/npc_team/plonk.jinx +0 -53
  49. npcsh-1.1.12.data/data/npcsh/npc_team/pti.jinx +0 -28
  50. npcsh-1.1.12.data/data/npcsh/npc_team/sh.jinx +0 -38
  51. npcsh-1.1.12.data/data/npcsh/npc_team/spool.jinx +0 -40
  52. npcsh-1.1.12.data/data/npcsh/npc_team/wander.jinx +0 -81
  53. npcsh-1.1.12.data/data/npcsh/npc_team/yap.jinx +0 -25
  54. npcsh-1.1.12.dist-info/RECORD +0 -126
  55. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/agent.jinx +0 -0
  56. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/alicanto.npc +0 -0
  57. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/alicanto.png +0 -0
  58. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/build.jinx +0 -0
  59. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/compile.jinx +0 -0
  60. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/compress.jinx +0 -0
  61. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/corca.npc +0 -0
  62. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/corca.png +0 -0
  63. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/corca_example.png +0 -0
  64. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/edit_file.jinx +0 -0
  65. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/foreman.npc +0 -0
  66. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/frederic.npc +0 -0
  67. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/frederic4.png +0 -0
  68. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/guac.png +0 -0
  69. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/help.jinx +0 -0
  70. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/init.jinx +0 -0
  71. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/jinxs.jinx +0 -0
  72. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
  73. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  74. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  75. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/npc-studio.jinx +0 -0
  76. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  77. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  78. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/ots.jinx +0 -0
  79. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/plonk.npc +0 -0
  80. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/plonk.png +0 -0
  81. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
  82. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  83. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/python.jinx +0 -0
  84. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/roll.jinx +0 -0
  85. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/sample.jinx +0 -0
  86. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/serve.jinx +0 -0
  87. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/set.jinx +0 -0
  88. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/sibiji.npc +0 -0
  89. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/sibiji.png +0 -0
  90. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/sleep.jinx +0 -0
  91. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/spool.png +0 -0
  92. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/sql.jinx +0 -0
  93. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  94. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
  95. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/yap.png +0 -0
  96. {npcsh-1.1.12.dist-info → npcsh-1.1.14.dist-info}/WHEEL +0 -0
  97. {npcsh-1.1.12.dist-info → npcsh-1.1.14.dist-info}/entry_points.txt +0 -0
  98. {npcsh-1.1.12.dist-info → npcsh-1.1.14.dist-info}/licenses/LICENSE +0 -0
  99. {npcsh-1.1.12.dist-info → npcsh-1.1.14.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,317 @@
1
+ jinx_name: guac
2
+ description: Python data analysis mode - execute Python code with persistent locals, auto-load files
3
+ inputs:
4
+ - model: null
5
+ - provider: null
6
+ - plots_dir: null
7
+
8
+ steps:
9
+ - name: guac_repl
10
+ engine: python
11
+ code: |
12
+ import os
13
+ import sys
14
+ import io
15
+ import re
16
+ import traceback
17
+ from pathlib import Path
18
+ from datetime import datetime
19
+ from termcolor import colored
20
+
21
+ import numpy as np
22
+ import pandas as pd
23
+ import matplotlib.pyplot as plt
24
+
25
+ from npcpy.llm_funcs import get_llm_response
26
+ from npcpy.npc_sysenv import render_markdown, get_system_message
27
+
28
+ npc = context.get('npc')
29
+ team = context.get('team')
30
+ messages = context.get('messages', [])
31
+ plots_dir = context.get('plots_dir') or os.path.expanduser("~/.npcsh/plots")
32
+
33
+ model = context.get('model') or (npc.model if npc else None)
34
+ provider = context.get('provider') or (npc.provider if npc else None)
35
+
36
+ # Use shared_context for persistent Python locals
37
+ shared_ctx = npc.shared_context if npc and hasattr(npc, 'shared_context') else {}
38
+ if 'locals' not in shared_ctx:
39
+ shared_ctx['locals'] = {}
40
+
41
+ # Initialize locals with useful imports
42
+ guac_locals = shared_ctx['locals']
43
+ guac_locals.update({
44
+ 'np': np,
45
+ 'pd': pd,
46
+ 'plt': plt,
47
+ 'Path': Path,
48
+ 'os': os,
49
+ })
50
+
51
+ # Also store dataframes reference
52
+ if 'dataframes' not in shared_ctx:
53
+ shared_ctx['dataframes'] = {}
54
+ guac_locals['dataframes'] = shared_ctx['dataframes']
55
+
56
+ os.makedirs(plots_dir, exist_ok=True)
57
+
58
+ print("""
59
+ ██████╗ ██╗ ██╗ █████╗ ██████╗
60
+ ██╔════╝ ██║ ██║██╔══██╗██╔════╝
61
+ ██║ ███╗██║ ██║███████║██║
62
+ ██║ ██║██║ ██║██╔══██║██║
63
+ ╚██████╔╝╚██████╔╝██║ ██║╚██████╗
64
+ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝
65
+ """)
66
+
67
+ npc_name = npc.name if npc else "guac"
68
+ print(f"Entering guac mode (NPC: {npc_name}). Type '/gq' to exit.")
69
+ print(" - Type Python code directly to execute")
70
+ print(" - Type natural language to get code suggestions")
71
+ print(" - Drop file paths to auto-load data")
72
+ print(f" - Plots saved to: {plots_dir}")
73
+
74
+ def is_python_code(text):
75
+ """Check if text looks like Python code"""
76
+ text = text.strip()
77
+ if not text:
78
+ return False
79
+ # Common Python patterns
80
+ if re.match(r'^(import |from |def |class |if |for |while |with |try:|@|#)', text):
81
+ return True
82
+ if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*\s*=', text):
83
+ return True
84
+ if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*\s*\(', text):
85
+ return True
86
+ if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*\s*\[', text):
87
+ return True
88
+ if text.startswith('print(') or text.startswith('plt.'):
89
+ return True
90
+ try:
91
+ compile(text, "<input>", "exec")
92
+ return True
93
+ except SyntaxError:
94
+ return False
95
+
96
+ def execute_python(code_str, locals_dict):
97
+ """Execute Python code and return output"""
98
+ output_capture = io.StringIO()
99
+ old_stdout, old_stderr = sys.stdout, sys.stderr
100
+
101
+ try:
102
+ sys.stdout = output_capture
103
+ sys.stderr = output_capture
104
+
105
+ # Try as expression first (for REPL-like behavior)
106
+ if '\n' not in code_str.strip():
107
+ try:
108
+ result = eval(compile(code_str, "<input>", "eval"), locals_dict)
109
+ if result is not None:
110
+ print(repr(result))
111
+ return output_capture.getvalue().strip()
112
+ except SyntaxError:
113
+ pass
114
+
115
+ # Execute as statements
116
+ exec(compile(code_str, "<input>", "exec"), locals_dict)
117
+ return output_capture.getvalue().strip()
118
+
119
+ except Exception:
120
+ traceback.print_exc(file=output_capture)
121
+ return output_capture.getvalue().strip()
122
+ finally:
123
+ sys.stdout, sys.stderr = old_stdout, old_stderr
124
+
125
+ def auto_load_file(file_path, locals_dict):
126
+ """Auto-load a file into locals based on extension"""
127
+ path = Path(file_path).expanduser()
128
+ if not path.exists():
129
+ return None
130
+
131
+ ext = path.suffix.lower()
132
+ var_name = f"data_{datetime.now().strftime('%H%M%S')}"
133
+
134
+ try:
135
+ if ext == '.csv':
136
+ df = pd.read_csv(path)
137
+ locals_dict[var_name] = df
138
+ shared_ctx['dataframes'][var_name] = df
139
+ return f"Loaded CSV as '{var_name}': {len(df)} rows, {len(df.columns)} columns\nColumns: {list(df.columns)}"
140
+
141
+ elif ext in ['.xlsx', '.xls']:
142
+ df = pd.read_excel(path)
143
+ locals_dict[var_name] = df
144
+ shared_ctx['dataframes'][var_name] = df
145
+ return f"Loaded Excel as '{var_name}': {len(df)} rows, {len(df.columns)} columns"
146
+
147
+ elif ext == '.json':
148
+ import json
149
+ with open(path) as f:
150
+ data = json.load(f)
151
+ locals_dict[var_name] = data
152
+ return f"Loaded JSON as '{var_name}': {type(data).__name__}"
153
+
154
+ elif ext in ['.png', '.jpg', '.jpeg', '.gif']:
155
+ from PIL import Image
156
+ img = Image.open(path)
157
+ arr = np.array(img)
158
+ locals_dict[f"{var_name}_img"] = img
159
+ locals_dict[f"{var_name}_arr"] = arr
160
+ return f"Loaded image as '{var_name}_img' and '{var_name}_arr': {img.size}"
161
+
162
+ elif ext == '.npy':
163
+ arr = np.load(path)
164
+ locals_dict[var_name] = arr
165
+ return f"Loaded numpy array as '{var_name}': shape {arr.shape}"
166
+
167
+ elif ext in ['.txt', '.md']:
168
+ with open(path) as f:
169
+ text = f.read()
170
+ locals_dict[var_name] = text
171
+ return f"Loaded text as '{var_name}': {len(text)} chars"
172
+
173
+ else:
174
+ with open(path, 'rb') as f:
175
+ data = f.read()
176
+ locals_dict[var_name] = data
177
+ return f"Loaded binary as '{var_name}': {len(data)} bytes"
178
+
179
+ except Exception as e:
180
+ return f"Error loading {path}: {e}"
181
+
182
+ def save_current_plot():
183
+ """Save current matplotlib figure if any"""
184
+ if plt.get_fignums():
185
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
186
+ plot_path = os.path.join(plots_dir, f"plot_{timestamp}.png")
187
+ plt.savefig(plot_path, dpi=150, bbox_inches='tight')
188
+ plt.close()
189
+ return plot_path
190
+ return None
191
+
192
+ # Ensure system message for LLM help
193
+ if not messages or messages[0].get("role") != "system":
194
+ sys_msg = """You are a Python data analysis assistant. When the user asks questions:
195
+ 1. Generate clean, executable Python code
196
+ 2. Use pandas, numpy, matplotlib as needed
197
+ 3. Reference variables already in the user's session
198
+ 4. Keep code concise and focused"""
199
+ messages.insert(0, {"role": "system", "content": sys_msg})
200
+
201
+ # REPL loop
202
+ while True:
203
+ try:
204
+ prompt_str = f"{npc_name}:guac> "
205
+ user_input = input(prompt_str).strip()
206
+
207
+ if not user_input:
208
+ continue
209
+
210
+ if user_input.lower() == "/gq":
211
+ print("Exiting guac mode.")
212
+ break
213
+
214
+ # /vars - show current variables
215
+ if user_input.lower() == "/vars":
216
+ print(colored("Current variables:", "cyan"))
217
+ for k, v in guac_locals.items():
218
+ if not k.startswith('_') and k not in ['np', 'pd', 'plt', 'Path', 'os', 'dataframes']:
219
+ vtype = type(v).__name__
220
+ if isinstance(v, pd.DataFrame):
221
+ print(f" {k}: DataFrame ({len(v)} rows)")
222
+ elif isinstance(v, np.ndarray):
223
+ print(f" {k}: ndarray {v.shape}")
224
+ else:
225
+ print(f" {k}: {vtype}")
226
+ continue
227
+
228
+ # /clear - clear variables
229
+ if user_input.lower() == "/clear":
230
+ keep = {'np', 'pd', 'plt', 'Path', 'os', 'dataframes'}
231
+ guac_locals.clear()
232
+ guac_locals.update({k: v for k, v in [('np', np), ('pd', pd), ('plt', plt), ('Path', Path), ('os', os), ('dataframes', shared_ctx['dataframes'])]})
233
+ print(colored("Variables cleared.", "yellow"))
234
+ continue
235
+
236
+ # Check if it's a file path (drag & drop)
237
+ potential_path = user_input.strip("'\"")
238
+ if os.path.exists(os.path.expanduser(potential_path)):
239
+ result = auto_load_file(potential_path, guac_locals)
240
+ if result:
241
+ print(colored(result, "green"))
242
+ continue
243
+
244
+ # Check if it's Python code
245
+ if is_python_code(user_input):
246
+ output = execute_python(user_input, guac_locals)
247
+ if output:
248
+ print(output)
249
+
250
+ # Save any plots
251
+ plot_path = save_current_plot()
252
+ if plot_path:
253
+ print(colored(f"Plot saved: {plot_path}", "green"))
254
+ continue
255
+
256
+ # Natural language - ask LLM for code
257
+ # Include current variables in context
258
+ var_context = "Current variables:\n"
259
+ for k, v in guac_locals.items():
260
+ if not k.startswith('_') and k not in ['np', 'pd', 'plt', 'Path', 'os', 'dataframes']:
261
+ if isinstance(v, pd.DataFrame):
262
+ var_context += f" {k}: DataFrame with columns {list(v.columns)}\n"
263
+ elif isinstance(v, np.ndarray):
264
+ var_context += f" {k}: ndarray shape {v.shape}\n"
265
+ else:
266
+ var_context += f" {k}: {type(v).__name__}\n"
267
+
268
+ prompt = f"{var_context}\nUser request: {user_input}\n\nGenerate Python code to accomplish this. Return ONLY the code, no explanation."
269
+
270
+ resp = get_llm_response(
271
+ prompt,
272
+ model=model,
273
+ provider=provider,
274
+ messages=messages,
275
+ npc=npc
276
+ )
277
+
278
+ messages = resp.get('messages', messages)
279
+ code_response = str(resp.get('response', ''))
280
+
281
+ # Extract code from response (strip markdown if present)
282
+ code = code_response
283
+ if '```python' in code:
284
+ code = code.split('```python')[1].split('```')[0]
285
+ elif '```' in code:
286
+ code = code.split('```')[1].split('```')[0]
287
+ code = code.strip()
288
+
289
+ if code:
290
+ print(colored("Generated code:", "cyan"))
291
+ print(code)
292
+ confirm = input("Execute? [Y/n/e(dit)]: ").strip().lower()
293
+
294
+ if confirm in ['', 'y', 'yes']:
295
+ output = execute_python(code, guac_locals)
296
+ if output:
297
+ print(output)
298
+ plot_path = save_current_plot()
299
+ if plot_path:
300
+ print(colored(f"Plot saved: {plot_path}", "green"))
301
+
302
+ elif confirm == 'e':
303
+ edited = input("Enter modified code: ").strip()
304
+ if edited:
305
+ output = execute_python(edited, guac_locals)
306
+ if output:
307
+ print(output)
308
+
309
+ except KeyboardInterrupt:
310
+ print("\nUse '/gq' to exit or continue.")
311
+ continue
312
+ except EOFError:
313
+ print("\nExiting guac mode.")
314
+ break
315
+
316
+ context['output'] = "Exited guac mode."
317
+ context['messages'] = messages
@@ -0,0 +1,214 @@
1
+ jinx_name: plonk
2
+ description: Vision-based GUI automation - use vision model to interact with screen elements
3
+ inputs:
4
+ - task: null
5
+ - vmodel: null
6
+ - vprovider: null
7
+ - max_iterations: 10
8
+ - debug: true
9
+
10
+ steps:
11
+ - name: plonk_execute
12
+ engine: python
13
+ code: |
14
+ import os
15
+ import time
16
+ import platform
17
+ from termcolor import colored
18
+
19
+ from npcpy.llm_funcs import get_llm_response
20
+ from npcpy.data.image import capture_screenshot
21
+ from npcpy.work.desktop import perform_action
22
+
23
+ npc = context.get('npc')
24
+ messages = context.get('messages', [])
25
+
26
+ task = context.get('task')
27
+ vision_model = context.get('vmodel') or (npc.model if npc else 'gpt-4o')
28
+ vision_provider = context.get('vprovider') or (npc.provider if npc else 'openai')
29
+ max_iterations = int(context.get('max_iterations', 10))
30
+ debug = context.get('debug', True)
31
+
32
+ if not task:
33
+ context['output'] = """Usage: /plonk <task description>
34
+
35
+ Options:
36
+ --vmodel MODEL Vision model to use (default: gpt-4o)
37
+ --vprovider PROV Vision provider (default: openai)
38
+ --max-iterations N Max steps (default: 10)
39
+
40
+ Example: /plonk Open Firefox and navigate to google.com"""
41
+ context['messages'] = messages
42
+ exit()
43
+
44
+ print(f"""
45
+ ██████╗ ██╗ ██████╗ ███╗ ██╗██╗ ██╗
46
+ ██╔══██╗██║ ██╔═══██╗████╗ ██║██║ ██╔╝
47
+ ██████╔╝██║ ██║ ██║██╔██╗ ██║█████╔╝
48
+ ██╔═══╝ ██║ ██║ ██║██║╚██╗██║██╔═██╗
49
+ ██║ ███████╗╚██████╔╝██║ ╚████║██║ ██╗
50
+ ╚═╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝
51
+
52
+ Vision GUI Automation
53
+ Task: {task}
54
+ Model: {vision_model} | Max iterations: {max_iterations}
55
+ """)
56
+
57
+ # System-specific examples
58
+ system = platform.system()
59
+ if system == "Windows":
60
+ app_examples = "start firefox, notepad, calc"
61
+ elif system == "Darwin":
62
+ app_examples = "open -a Firefox, open -a TextEdit"
63
+ else:
64
+ app_examples = "firefox &, gedit &, gnome-calculator &"
65
+
66
+ # Action types
67
+ ACTION_SCHEMA = {
68
+ "type": "object",
69
+ "properties": {
70
+ "action": {
71
+ "type": "string",
72
+ "enum": ["click", "type", "key", "launch", "wait", "done", "fail"],
73
+ "description": "Action to perform"
74
+ },
75
+ "x": {"type": "number", "description": "X coordinate (0-100 percentage)"},
76
+ "y": {"type": "number", "description": "Y coordinate (0-100 percentage)"},
77
+ "text": {"type": "string", "description": "Text to type or key to press"},
78
+ "command": {"type": "string", "description": "Command to launch"},
79
+ "duration": {"type": "number", "description": "Wait duration in seconds"},
80
+ "reason": {"type": "string", "description": "Explanation of action"}
81
+ },
82
+ "required": ["action", "reason"]
83
+ }
84
+
85
+ click_history = []
86
+ summary = []
87
+
88
+ for iteration in range(max_iterations):
89
+ print(colored(f"\n--- Iteration {iteration + 1}/{max_iterations} ---", "cyan"))
90
+
91
+ # Capture screenshot
92
+ ss = capture_screenshot()
93
+ if not ss or 'file_path' not in ss:
94
+ print(colored("Failed to capture screenshot", "red"))
95
+ break
96
+
97
+ screenshot_path = ss['file_path']
98
+ if debug:
99
+ print(colored(f"Screenshot: {screenshot_path}", "gray"))
100
+
101
+ # Build context from history
102
+ history_context = ""
103
+ if click_history:
104
+ history_context = f"\nPrevious actions ({len(click_history)}):\n"
105
+ for i, click in enumerate(click_history[-5:], 1):
106
+ history_context += f" {i}. {click.get('action', 'unknown')} at ({click.get('x', '?')}, {click.get('y', '?')}) - {click.get('reason', '')}\n"
107
+
108
+ prompt = f"""You are a GUI automation assistant. Analyze this screenshot and determine the next action to complete the task.
109
+
110
+ TASK: {task}
111
+
112
+ {history_context}
113
+
114
+ Available actions:
115
+ - click: Click at x,y coordinates (0-100 percentage of screen)
116
+ - type: Type text
117
+ - key: Press key (enter, tab, escape, etc.)
118
+ - launch: Launch application ({app_examples})
119
+ - wait: Wait for duration seconds
120
+ - done: Task completed successfully
121
+ - fail: Task cannot be completed
122
+
123
+ Respond with JSON: {{"action": "...", "x": N, "y": N, "text": "...", "command": "...", "duration": N, "reason": "..."}}"""
124
+
125
+ try:
126
+ resp = get_llm_response(
127
+ prompt,
128
+ model=vision_model,
129
+ provider=vision_provider,
130
+ images=[screenshot_path],
131
+ format="json",
132
+ npc=npc
133
+ )
134
+
135
+ action_response = resp.get('response', {})
136
+ if isinstance(action_response, str):
137
+ import json
138
+ try:
139
+ action_response = json.loads(action_response)
140
+ except:
141
+ print(colored(f"Invalid JSON response: {action_response[:100]}", "red"))
142
+ continue
143
+
144
+ action = action_response.get('action', 'fail')
145
+ reason = action_response.get('reason', 'No reason provided')
146
+
147
+ print(colored(f"Action: {action} - {reason}", "yellow"))
148
+
149
+ if action == 'done':
150
+ print(colored("Task completed successfully!", "green"))
151
+ summary.append({"iteration": iteration + 1, "action": "done", "reason": reason})
152
+ break
153
+
154
+ if action == 'fail':
155
+ print(colored(f"Task failed: {reason}", "red"))
156
+ summary.append({"iteration": iteration + 1, "action": "fail", "reason": reason})
157
+ break
158
+
159
+ # Execute action
160
+ if action == 'click':
161
+ x, y = action_response.get('x', 50), action_response.get('y', 50)
162
+ perform_action('click', x=x, y=y)
163
+ click_history.append({"action": "click", "x": x, "y": y, "reason": reason})
164
+ print(colored(f"Clicked at ({x}, {y})", "green"))
165
+
166
+ elif action == 'type':
167
+ text = action_response.get('text', '')
168
+ perform_action('type', text=text)
169
+ click_history.append({"action": "type", "text": text[:20], "reason": reason})
170
+ print(colored(f"Typed: {text[:30]}...", "green"))
171
+
172
+ elif action == 'key':
173
+ key = action_response.get('text', 'enter')
174
+ perform_action('key', key=key)
175
+ click_history.append({"action": "key", "key": key, "reason": reason})
176
+ print(colored(f"Pressed key: {key}", "green"))
177
+
178
+ elif action == 'launch':
179
+ cmd = action_response.get('command', '')
180
+ perform_action('launch', command=cmd)
181
+ click_history.append({"action": "launch", "command": cmd, "reason": reason})
182
+ print(colored(f"Launched: {cmd}", "green"))
183
+ time.sleep(2) # Wait for app to open
184
+
185
+ elif action == 'wait':
186
+ duration = action_response.get('duration', 1)
187
+ time.sleep(duration)
188
+ click_history.append({"action": "wait", "duration": duration, "reason": reason})
189
+ print(colored(f"Waited {duration}s", "green"))
190
+
191
+ summary.append({
192
+ "iteration": iteration + 1,
193
+ "action": action,
194
+ "last_click_coords": f"({click_history[-1].get('x', 'N/A')}, {click_history[-1].get('y', 'N/A')})" if click_history else "N/A",
195
+ "reason": reason
196
+ })
197
+
198
+ time.sleep(0.5) # Brief pause between actions
199
+
200
+ except Exception as e:
201
+ print(colored(f"Error in iteration {iteration + 1}: {e}", "red"))
202
+ summary.append({"iteration": iteration + 1, "error": str(e)})
203
+
204
+ # Generate summary
205
+ print("\n" + "="*50)
206
+ print(colored("PLONK SESSION SUMMARY", "cyan", attrs=['bold']))
207
+ print("="*50)
208
+ for s in summary:
209
+ print(f" Step {s.get('iteration', '?')}: {s.get('action', 'unknown')} - {s.get('reason', s.get('error', ''))[:60]}")
210
+
211
+ context['output'] = f"Plonk completed with {len(summary)} actions"
212
+ context['messages'] = messages
213
+ context['plonk_summary'] = summary
214
+ context['click_history'] = click_history