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
@@ -1,46 +1,317 @@
1
- jinx_name: "guac"
2
- description: "Enter guac mode for plotting and data visualization."
1
+ jinx_name: guac
2
+ description: Python data analysis mode - execute Python code with persistent locals, auto-load files
3
3
  inputs:
4
- - config_dir: "" # Optional configuration directory.
5
- - plots_dir: "" # Optional directory for plots.
6
- - refresh_period: 100 # Refresh period for guac mode.
7
- - lang: "" # Language setting for guac mode.
4
+ - model: null
5
+ - provider: null
6
+ - plots_dir: null
7
+
8
8
  steps:
9
- - name: "enter_guac"
10
- engine: "python"
9
+ - name: guac_repl
10
+ engine: python
11
11
  code: |
12
12
  import os
13
- from sqlalchemy import create_engine
14
- from npcpy.npc_compiler import NPC, Team
15
- from npcsh.guac import enter_guac_mode
16
-
17
- config_dir = context.get('config_dir')
18
- plots_dir = context.get('plots_dir')
19
- refresh_period = context.get('refresh_period')
20
- lang = context.get('lang')
21
- output_messages = context.get('messages', [])
22
-
23
- db_path = os.path.expanduser('~/npcsh_history.db')
24
- db_conn = create_engine(f'sqlite:///{db_path}')
25
-
26
- npc_file = os.path.expanduser('~/.npcsh/guac/npc_team/guac.npc')
27
- npc_team_dir = os.path.expanduser('~/.npcsh/guac/npc_team/')
28
-
29
- # Ensure directories exist for guac NPC/Team
30
- os.makedirs(os.path.dirname(npc_file), exist_ok=True)
31
-
32
- guac_npc = NPC(file=npc_file, db_conn=db_conn)
33
- guac_team = Team(npc_team_dir, db_conn=db_conn)
34
-
35
- enter_guac_mode(
36
- npc=guac_npc,
37
- team=guac_team,
38
- config_dir=config_dir,
39
- plots_dir=plots_dir,
40
- npc_team_dir=npc_team_dir,
41
- refresh_period=int(refresh_period), # Ensure int type
42
- lang=lang
43
- )
44
-
45
- context['output'] = 'Exiting Guac Mode'
46
- context['messages'] = output_messages
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
@@ -1,53 +1,214 @@
1
- jinx_name: "plonk"
2
- description: "Use vision model to interact with GUI. Usage: /plonk <task description>"
1
+ jinx_name: plonk
2
+ description: Vision-based GUI automation - use vision model to interact with screen elements
3
3
  inputs:
4
- - task_description: "" # Required task description for GUI interaction.
5
- - vmodel: "" # Vision model to use. Defaults to NPCSH_VISION_MODEL or NPC's model.
6
- - vprovider: "" # Vision model provider. Defaults to NPCSH_VISION_PROVIDER or NPC's provider.
4
+ - task: null
5
+ - vmodel: null
6
+ - vprovider: null
7
+ - max_iterations: 10
8
+ - debug: true
9
+
7
10
  steps:
8
- - name: "execute_plonk"
9
- engine: "python"
11
+ - name: plonk_execute
12
+ engine: python
10
13
  code: |
11
- import traceback
12
- from npcsh.plonk import execute_plonk_command, format_plonk_summary
13
- # Assuming NPCSH_VISION_MODEL and NPCSH_VISION_PROVIDER are accessible
14
-
15
- task_description = context.get('task_description')
16
- vision_model = context.get('vmodel')
17
- vision_provider = context.get('vprovider')
18
- plonk_context = context.get('plonk_context') # Passed from original context
19
- current_npc = context.get('npc')
20
- output_messages = context.get('messages', [])
21
-
22
- if not task_description or not task_description.strip():
23
- context['output'] = "Usage: /plonk <task_description> [--vmodel model_name] [--vprovider provider_name]"
24
- context['messages'] = output_messages
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
25
42
  exit()
26
43
 
27
- # Fallback for model/provider if not explicitly set in Jinx inputs
28
- if not vision_model and current_npc and current_npc.model:
29
- vision_model = current_npc.model
30
- if not vision_provider and current_npc and current_npc.provider:
31
- vision_provider = current_npc.provider
32
-
33
- try:
34
- summary_data = execute_plonk_command(
35
- request=task_description,
36
- model=vision_model,
37
- provider=vision_provider,
38
- npc=current_npc,
39
- plonk_context=plonk_context,
40
- debug=True # Assuming debug is often desired for plonk
41
- )
42
-
43
- if summary_data and isinstance(summary_data, list):
44
- output_report = format_plonk_summary(summary_data)
45
- context['output'] = output_report
46
- else:
47
- context['output'] = "Plonk command did not complete within the maximum number of iterations."
48
-
49
- except Exception as e:
50
- traceback.print_exc()
51
- context['output'] = f"Error executing plonk command: {e}"
52
-
53
- context['messages'] = output_messages
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