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.
- npcsh/_state.py +700 -377
- npcsh/alicanto.py +54 -1153
- npcsh/completion.py +206 -0
- npcsh/config.py +163 -0
- npcsh/corca.py +35 -1462
- npcsh/execution.py +185 -0
- npcsh/guac.py +31 -1986
- npcsh/npc_team/jinxs/code/sh.jinx +11 -15
- npcsh/npc_team/jinxs/modes/alicanto.jinx +186 -80
- npcsh/npc_team/jinxs/modes/corca.jinx +243 -22
- npcsh/npc_team/jinxs/modes/guac.jinx +313 -42
- npcsh/npc_team/jinxs/modes/plonk.jinx +209 -48
- npcsh/npc_team/jinxs/modes/pti.jinx +167 -25
- npcsh/npc_team/jinxs/modes/spool.jinx +158 -37
- npcsh/npc_team/jinxs/modes/wander.jinx +179 -74
- npcsh/npc_team/jinxs/modes/yap.jinx +258 -21
- npcsh/npc_team/jinxs/utils/chat.jinx +39 -12
- npcsh/npc_team/jinxs/utils/cmd.jinx +44 -0
- npcsh/npc_team/jinxs/utils/search.jinx +3 -3
- npcsh/npc_team/jinxs/utils/usage.jinx +33 -0
- npcsh/npcsh.py +76 -20
- npcsh/parsing.py +118 -0
- npcsh/plonk.py +41 -329
- npcsh/pti.py +41 -201
- npcsh/spool.py +34 -239
- npcsh/ui.py +199 -0
- npcsh/wander.py +54 -542
- npcsh/yap.py +38 -570
- npcsh-1.1.14.data/data/npcsh/npc_team/alicanto.jinx +194 -0
- npcsh-1.1.14.data/data/npcsh/npc_team/chat.jinx +44 -0
- npcsh-1.1.14.data/data/npcsh/npc_team/cmd.jinx +44 -0
- npcsh-1.1.14.data/data/npcsh/npc_team/corca.jinx +249 -0
- npcsh-1.1.14.data/data/npcsh/npc_team/guac.jinx +317 -0
- npcsh-1.1.14.data/data/npcsh/npc_team/plonk.jinx +214 -0
- npcsh-1.1.14.data/data/npcsh/npc_team/pti.jinx +170 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/search.jinx +3 -3
- npcsh-1.1.14.data/data/npcsh/npc_team/sh.jinx +34 -0
- npcsh-1.1.14.data/data/npcsh/npc_team/spool.jinx +161 -0
- npcsh-1.1.14.data/data/npcsh/npc_team/usage.jinx +33 -0
- npcsh-1.1.14.data/data/npcsh/npc_team/wander.jinx +186 -0
- npcsh-1.1.14.data/data/npcsh/npc_team/yap.jinx +262 -0
- {npcsh-1.1.12.dist-info → npcsh-1.1.14.dist-info}/METADATA +1 -1
- npcsh-1.1.14.dist-info/RECORD +135 -0
- npcsh-1.1.12.data/data/npcsh/npc_team/alicanto.jinx +0 -88
- npcsh-1.1.12.data/data/npcsh/npc_team/chat.jinx +0 -17
- npcsh-1.1.12.data/data/npcsh/npc_team/corca.jinx +0 -28
- npcsh-1.1.12.data/data/npcsh/npc_team/guac.jinx +0 -46
- npcsh-1.1.12.data/data/npcsh/npc_team/plonk.jinx +0 -53
- npcsh-1.1.12.data/data/npcsh/npc_team/pti.jinx +0 -28
- npcsh-1.1.12.data/data/npcsh/npc_team/sh.jinx +0 -38
- npcsh-1.1.12.data/data/npcsh/npc_team/spool.jinx +0 -40
- npcsh-1.1.12.data/data/npcsh/npc_team/wander.jinx +0 -81
- npcsh-1.1.12.data/data/npcsh/npc_team/yap.jinx +0 -25
- npcsh-1.1.12.dist-info/RECORD +0 -126
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/agent.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/alicanto.npc +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/alicanto.png +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/build.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/compile.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/compress.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/corca.npc +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/corca.png +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/corca_example.png +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/edit_file.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/foreman.npc +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/frederic.npc +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/frederic4.png +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/guac.png +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/help.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/init.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/jinxs.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/kadiefa.png +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/load_file.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/npc-studio.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/ots.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/plonk.npc +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/plonk.png +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/plonkjr.png +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/python.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/roll.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/sample.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/serve.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/set.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/sibiji.npc +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/sibiji.png +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/sleep.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/spool.png +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/sql.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/trigger.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/yap.png +0 -0
- {npcsh-1.1.12.dist-info → npcsh-1.1.14.dist-info}/WHEEL +0 -0
- {npcsh-1.1.12.dist-info → npcsh-1.1.14.dist-info}/entry_points.txt +0 -0
- {npcsh-1.1.12.dist-info → npcsh-1.1.14.dist-info}/licenses/LICENSE +0 -0
- {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
|