npcsh 1.0.26__py3-none-any.whl → 1.0.28__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 +115 -111
- npcsh/alicanto.py +88 -88
- npcsh/corca.py +423 -95
- npcsh/guac.py +110 -107
- npcsh/mcp_helpers.py +45 -45
- npcsh/mcp_server.py +16 -17
- npcsh/npc.py +16 -17
- npcsh/npc_team/jinxs/bash_executer.jinx +1 -1
- npcsh/npc_team/jinxs/edit_file.jinx +6 -6
- npcsh/npc_team/jinxs/image_generation.jinx +5 -5
- npcsh/npc_team/jinxs/screen_cap.jinx +2 -2
- npcsh/npcsh.py +15 -6
- npcsh/plonk.py +8 -8
- npcsh/routes.py +77 -77
- npcsh/spool.py +13 -13
- npcsh/wander.py +37 -37
- npcsh/yap.py +72 -72
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/bash_executer.jinx +1 -1
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/edit_file.jinx +6 -6
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/image_generation.jinx +5 -5
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/screen_cap.jinx +2 -2
- {npcsh-1.0.26.dist-info → npcsh-1.0.28.dist-info}/METADATA +1 -1
- npcsh-1.0.28.dist-info/RECORD +73 -0
- npcsh-1.0.26.dist-info/RECORD +0 -73
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/alicanto.npc +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/alicanto.png +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/corca.npc +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/corca.png +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/foreman.npc +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/frederic.npc +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/frederic4.png +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/guac.png +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/internet_search.jinx +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/kadiefa.png +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/plonk.npc +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/plonk.png +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/plonkjr.png +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/python_executor.jinx +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/sibiji.npc +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/sibiji.png +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/spool.png +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/yap.png +0 -0
- {npcsh-1.0.26.dist-info → npcsh-1.0.28.dist-info}/WHEEL +0 -0
- {npcsh-1.0.26.dist-info → npcsh-1.0.28.dist-info}/entry_points.txt +0 -0
- {npcsh-1.0.26.dist-info → npcsh-1.0.28.dist-info}/licenses/LICENSE +0 -0
- {npcsh-1.0.26.dist-info → npcsh-1.0.28.dist-info}/top_level.txt +0 -0
npcsh/guac.py
CHANGED
|
@@ -59,7 +59,7 @@ except importlib.metadata.PackageNotFoundError:
|
|
|
59
59
|
|
|
60
60
|
GUAC_REFRESH_PERIOD = os.environ.get('GUAC_REFRESH_PERIOD', 100)
|
|
61
61
|
READLINE_HISTORY_FILE = os.path.expanduser("~/.guac_readline_history")
|
|
62
|
-
|
|
62
|
+
|
|
63
63
|
EXTENSION_MAP = {
|
|
64
64
|
"PNG": "images", "JPG": "images", "JPEG": "images", "GIF": "images", "SVG": "images",
|
|
65
65
|
"MP4": "videos", "AVI": "videos", "MOV": "videos", "WMV": "videos", "MPG": "videos", "MPEG": "videos",
|
|
@@ -75,7 +75,7 @@ _guac_monitor_stop_event = None
|
|
|
75
75
|
def _clear_readline_buffer():
|
|
76
76
|
"""Clear the current readline input buffer and redisplay prompt."""
|
|
77
77
|
try:
|
|
78
|
-
|
|
78
|
+
|
|
79
79
|
if hasattr(readline, "replace_line") and hasattr(readline, "redisplay"):
|
|
80
80
|
readline.replace_line("", 0)
|
|
81
81
|
readline.redisplay()
|
|
@@ -83,11 +83,11 @@ def _clear_readline_buffer():
|
|
|
83
83
|
except Exception:
|
|
84
84
|
pass
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
|
|
87
87
|
try:
|
|
88
88
|
libname = ctypes.util.find_library("readline") or ctypes.util.find_library("edit") or "readline"
|
|
89
89
|
rl = ctypes.CDLL(libname)
|
|
90
|
-
|
|
90
|
+
|
|
91
91
|
rl.rl_replace_line.argtypes = [ctypes.c_char_p, ctypes.c_int]
|
|
92
92
|
rl.rl_redisplay.argtypes = []
|
|
93
93
|
rl.rl_replace_line(b"", 0)
|
|
@@ -113,44 +113,44 @@ def _file_drop_monitor(npc_team_dir: Path, state: ShellState, locals_dict: Dict[
|
|
|
113
113
|
time.sleep(poll_interval)
|
|
114
114
|
continue
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
|
|
117
117
|
candidate = buf.strip()
|
|
118
|
-
|
|
118
|
+
|
|
119
119
|
if (candidate.startswith("'") and candidate.endswith("'")) or (candidate.startswith('"') and candidate.endswith('"')):
|
|
120
120
|
inner = candidate[1:-1]
|
|
121
121
|
else:
|
|
122
122
|
inner = candidate
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
|
|
125
125
|
if " " not in inner and Path(inner.replace('~', str(Path.home()))).expanduser().exists() and Path(inner.replace('~', str(Path.home()))).expanduser().is_file():
|
|
126
|
-
|
|
126
|
+
|
|
127
127
|
if buf in processed_bufs:
|
|
128
128
|
time.sleep(poll_interval)
|
|
129
129
|
continue
|
|
130
130
|
processed_bufs.add(buf)
|
|
131
131
|
|
|
132
|
-
|
|
132
|
+
|
|
133
133
|
try:
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
|
|
135
|
+
|
|
136
136
|
modified_input, processed_files = _handle_file_drop(buf, npc_team_dir)
|
|
137
137
|
if processed_files:
|
|
138
138
|
target_path = processed_files[0]
|
|
139
|
-
|
|
139
|
+
|
|
140
140
|
loading_code = _generate_file_analysis_code(inner, target_path)
|
|
141
|
-
|
|
141
|
+
|
|
142
142
|
print("\n[guac] Detected file drop — processing automatically...")
|
|
143
|
-
|
|
143
|
+
|
|
144
144
|
_state, exec_output = execute_python_code(loading_code, state, locals_dict)
|
|
145
|
-
|
|
145
|
+
|
|
146
146
|
if exec_output:
|
|
147
147
|
print(exec_output)
|
|
148
|
-
|
|
148
|
+
|
|
149
149
|
_clear_readline_buffer()
|
|
150
150
|
except Exception as e:
|
|
151
151
|
print(f"[guac][ERROR] file drop processing failed: {e}")
|
|
152
152
|
except Exception:
|
|
153
|
-
|
|
153
|
+
|
|
154
154
|
pass
|
|
155
155
|
time.sleep(poll_interval)
|
|
156
156
|
|
|
@@ -213,27 +213,27 @@ def execute_python_code(code_str: str, state: ShellState, locals_dict: Dict[str,
|
|
|
213
213
|
final_output_str = output_capture.getvalue().strip()
|
|
214
214
|
output_capture.close()
|
|
215
215
|
|
|
216
|
-
|
|
216
|
+
|
|
217
217
|
_capture_plot_state(state.conversation_id, state.command_history.db_path, Path.cwd() / "npc_team")
|
|
218
218
|
|
|
219
219
|
if state.command_history:
|
|
220
220
|
state.command_history.add_command(code_str, [final_output_str if final_output_str else ""], "", state.current_path)
|
|
221
221
|
return state, final_output_str
|
|
222
222
|
|
|
223
|
-
|
|
223
|
+
|
|
224
224
|
def _generate_file_analysis_code(file_path: str, target_path: str) -> str:
|
|
225
225
|
"""Generate Python code to load and analyze the dropped file"""
|
|
226
226
|
ext = Path(file_path).suffix.lower()
|
|
227
227
|
file_var_name = f"file_{datetime.now().strftime('%H%M%S')}"
|
|
228
228
|
|
|
229
229
|
capture_code = f"""
|
|
230
|
-
|
|
230
|
+
|
|
231
231
|
_capture_file_state('{state.conversation_id}', '{state.command_history.db_path}', r'{target_path}', '''AUTO_GENERATED_CODE''', locals())
|
|
232
232
|
"""
|
|
233
233
|
|
|
234
234
|
if ext == '.pdf':
|
|
235
235
|
return f"""
|
|
236
|
-
|
|
236
|
+
|
|
237
237
|
import PyPDF2
|
|
238
238
|
import pandas as pd
|
|
239
239
|
try:
|
|
@@ -255,7 +255,7 @@ except Exception as e:
|
|
|
255
255
|
|
|
256
256
|
elif ext in ['.csv']:
|
|
257
257
|
return f"""
|
|
258
|
-
|
|
258
|
+
|
|
259
259
|
import pandas as pd
|
|
260
260
|
try:
|
|
261
261
|
{file_var_name}_df = pd.read_csv(r'{target_path}')
|
|
@@ -272,7 +272,7 @@ except Exception as e:
|
|
|
272
272
|
|
|
273
273
|
elif ext in ['.xlsx', '.xls']:
|
|
274
274
|
return f"""
|
|
275
|
-
|
|
275
|
+
|
|
276
276
|
import pandas as pd
|
|
277
277
|
try:
|
|
278
278
|
{file_var_name}_df = pd.read_excel(r'{target_path}')
|
|
@@ -289,7 +289,7 @@ except Exception as e:
|
|
|
289
289
|
|
|
290
290
|
elif ext in ['.json']:
|
|
291
291
|
return f"""
|
|
292
|
-
|
|
292
|
+
|
|
293
293
|
import json
|
|
294
294
|
try:
|
|
295
295
|
with open(r'{target_path}', 'r') as file:
|
|
@@ -308,7 +308,7 @@ except Exception as e:
|
|
|
308
308
|
|
|
309
309
|
elif ext in ['.txt', '.md']:
|
|
310
310
|
return f"""
|
|
311
|
-
|
|
311
|
+
|
|
312
312
|
try:
|
|
313
313
|
with open(r'{target_path}', 'r', encoding='utf-8') as file:
|
|
314
314
|
{file_var_name}_text = file.read()
|
|
@@ -324,7 +324,7 @@ except Exception as e:
|
|
|
324
324
|
|
|
325
325
|
elif ext in ['.png', '.jpg', '.jpeg', '.gif']:
|
|
326
326
|
return f"""
|
|
327
|
-
|
|
327
|
+
|
|
328
328
|
import matplotlib.pyplot as plt
|
|
329
329
|
from PIL import Image
|
|
330
330
|
import numpy as np
|
|
@@ -349,7 +349,7 @@ except Exception as e:
|
|
|
349
349
|
|
|
350
350
|
else:
|
|
351
351
|
return f"""
|
|
352
|
-
|
|
352
|
+
|
|
353
353
|
try:
|
|
354
354
|
with open(r'{target_path}', 'rb') as file:
|
|
355
355
|
{file_var_name}_data = file.read()
|
|
@@ -396,7 +396,7 @@ def _handle_guac_refresh(state: ShellState, project_name: str, src_dir: Path):
|
|
|
396
396
|
prompt = "\n".join(prompt_parts)
|
|
397
397
|
|
|
398
398
|
try:
|
|
399
|
-
|
|
399
|
+
|
|
400
400
|
npc_model = state.npc.model if state.npc and state.npc.model else state.chat_model
|
|
401
401
|
npc_provider = state.npc.provider if state.npc and state.npc.provider else state.chat_provider
|
|
402
402
|
|
|
@@ -480,7 +480,7 @@ def setup_guac_mode(config_dir=None, plots_dir=None, npc_team_dir=None,
|
|
|
480
480
|
lang='python', default_mode_choice=None):
|
|
481
481
|
base_dir = Path.cwd()
|
|
482
482
|
|
|
483
|
-
|
|
483
|
+
|
|
484
484
|
if GUAC_GLOBAL_FLAG_FILE.exists():
|
|
485
485
|
print("💡 Using global Guac team as default (previously set).")
|
|
486
486
|
team_dir = ensure_global_guac_team()
|
|
@@ -490,7 +490,7 @@ def setup_guac_mode(config_dir=None, plots_dir=None, npc_team_dir=None,
|
|
|
490
490
|
"project_description": "Global guac team for analysis.", "package_name": "guac"
|
|
491
491
|
}
|
|
492
492
|
|
|
493
|
-
|
|
493
|
+
|
|
494
494
|
if npc_team_dir is None:
|
|
495
495
|
npc_team_dir = base_dir / "npc_team"
|
|
496
496
|
else:
|
|
@@ -519,7 +519,7 @@ def setup_guac_mode(config_dir=None, plots_dir=None, npc_team_dir=None,
|
|
|
519
519
|
package_name = response if response else "project"
|
|
520
520
|
except (KeyboardInterrupt, EOFError):
|
|
521
521
|
print("⚠️ Project setup interrupted. Falling back to global guac team...")
|
|
522
|
-
GUAC_GLOBAL_FLAG_FILE.touch()
|
|
522
|
+
GUAC_GLOBAL_FLAG_FILE.touch()
|
|
523
523
|
team_dir = ensure_global_guac_team()
|
|
524
524
|
return {
|
|
525
525
|
"language": lang, "package_root": team_dir, "plots_dir": plots_dir,
|
|
@@ -574,7 +574,7 @@ setup(name="{package_name}", version="0.0.1", description="{desc}", packages=fin
|
|
|
574
574
|
"project_description": project_description, "package_name": package_name
|
|
575
575
|
}
|
|
576
576
|
def setup_npc_team(npc_team_dir, lang, is_subteam=False):
|
|
577
|
-
|
|
577
|
+
|
|
578
578
|
guac_npc = {
|
|
579
579
|
"name": "guac",
|
|
580
580
|
"primary_directive": (
|
|
@@ -602,14 +602,14 @@ def setup_npc_team(npc_team_dir, lang, is_subteam=False):
|
|
|
602
602
|
|
|
603
603
|
for npc_data in [guac_npc, caug_npc, parsely_npc, toon_npc]:
|
|
604
604
|
npc_file = npc_team_dir / f"{npc_data['name']}.npc"
|
|
605
|
-
if not npc_file.exists():
|
|
605
|
+
if not npc_file.exists():
|
|
606
606
|
with open(npc_file, "w") as f:
|
|
607
607
|
yaml.dump(npc_data, f, default_flow_style=False)
|
|
608
608
|
print(f"Created NPC: {npc_data['name']}")
|
|
609
609
|
else:
|
|
610
610
|
print(f"NPC already exists: {npc_data['name']}")
|
|
611
611
|
|
|
612
|
-
|
|
612
|
+
|
|
613
613
|
if is_subteam:
|
|
614
614
|
team_ctx_model = os.environ.get("NPCSH_CHAT_MODEL", "gemma3:4b")
|
|
615
615
|
team_ctx_provider = os.environ.get("NPCSH_CHAT_PROVIDER", "ollama")
|
|
@@ -644,22 +644,22 @@ def _detect_file_drop(input_text: str) -> bool:
|
|
|
644
644
|
|
|
645
645
|
stripped = input_text.strip()
|
|
646
646
|
|
|
647
|
-
|
|
647
|
+
|
|
648
648
|
if stripped.startswith("'") and stripped.endswith("'"):
|
|
649
649
|
stripped = stripped[1:-1]
|
|
650
650
|
elif stripped.startswith('"') and stripped.endswith('"'):
|
|
651
651
|
stripped = stripped[1:-1]
|
|
652
652
|
|
|
653
|
-
|
|
653
|
+
|
|
654
654
|
if len(stripped.split()) != 1:
|
|
655
655
|
return False
|
|
656
656
|
|
|
657
|
-
|
|
657
|
+
|
|
658
658
|
python_indicators = ['(', ')', '[', ']', '{', '}', '=', '+', '-', '*', '/', '%', '&', '|', '^', '<', '>', '!', '?', ':', ';', ',']
|
|
659
659
|
if any(indicator in stripped for indicator in python_indicators):
|
|
660
660
|
return False
|
|
661
661
|
|
|
662
|
-
|
|
662
|
+
|
|
663
663
|
python_keywords = ['import', 'from', 'def', 'class', 'if', 'for', 'while', 'try', 'with', 'lambda', 'print', 'len', 'str', 'int', 'float', 'list', 'dict', 'set', 'tuple']
|
|
664
664
|
if any(stripped.startswith(keyword) for keyword in python_keywords):
|
|
665
665
|
return False
|
|
@@ -670,7 +670,7 @@ from sqlalchemy import create_engine, Column, Integer, String, Text, Float, Date
|
|
|
670
670
|
from sqlalchemy.ext.declarative import declarative_base
|
|
671
671
|
from sqlalchemy.orm import sessionmaker
|
|
672
672
|
|
|
673
|
-
|
|
673
|
+
|
|
674
674
|
Base = declarative_base()
|
|
675
675
|
|
|
676
676
|
class PlotState(Base):
|
|
@@ -704,12 +704,12 @@ def _capture_plot_state(session_id: str, db_path: str, npc_team_dir: Path):
|
|
|
704
704
|
Session = sessionmaker(bind=engine)
|
|
705
705
|
session = Session()
|
|
706
706
|
|
|
707
|
-
|
|
707
|
+
|
|
708
708
|
fig = plt.gcf()
|
|
709
709
|
axes = fig.get_axes()
|
|
710
710
|
data_points = sum(len(line.get_xdata()) for ax in axes for line in ax.get_lines())
|
|
711
711
|
|
|
712
|
-
|
|
712
|
+
|
|
713
713
|
plot_hash = hashlib.md5(f"{len(axes)}{data_points}".encode()).hexdigest()
|
|
714
714
|
|
|
715
715
|
last = session.query(PlotState).filter(PlotState.session_id == session_id).order_by(PlotState.timestamp.desc()).first()
|
|
@@ -717,13 +717,13 @@ def _capture_plot_state(session_id: str, db_path: str, npc_team_dir: Path):
|
|
|
717
717
|
session.close()
|
|
718
718
|
return
|
|
719
719
|
|
|
720
|
-
|
|
720
|
+
|
|
721
721
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
722
722
|
workspace_dirs = _get_workspace_dirs(npc_team_dir)
|
|
723
723
|
plot_path = workspace_dirs["plots"] / f"state_{timestamp}.png"
|
|
724
724
|
plt.savefig(plot_path, dpi=150, bbox_inches='tight')
|
|
725
725
|
|
|
726
|
-
|
|
726
|
+
|
|
727
727
|
plot_state = PlotState(
|
|
728
728
|
session_id=session_id,
|
|
729
729
|
plot_hash=plot_hash,
|
|
@@ -745,14 +745,14 @@ def _capture_file_state(session_id: str, db_path: str, file_path: str, analysis_
|
|
|
745
745
|
Session = sessionmaker(bind=engine)
|
|
746
746
|
session = Session()
|
|
747
747
|
|
|
748
|
-
|
|
748
|
+
|
|
749
749
|
try:
|
|
750
750
|
with open(file_path, 'rb') as f:
|
|
751
751
|
file_hash = hashlib.md5(f.read()).hexdigest()
|
|
752
752
|
except:
|
|
753
753
|
file_hash = "unknown"
|
|
754
754
|
|
|
755
|
-
|
|
755
|
+
|
|
756
756
|
file_stem = Path(file_path).stem.lower()
|
|
757
757
|
vars_created = [k for k in locals_dict.keys() if not k.startswith('_') and file_stem in k.lower()]
|
|
758
758
|
|
|
@@ -812,7 +812,7 @@ def _generate_file_analysis_code(file_path: str, target_path: str) -> str:
|
|
|
812
812
|
|
|
813
813
|
if ext == '.pdf':
|
|
814
814
|
return f"""
|
|
815
|
-
|
|
815
|
+
|
|
816
816
|
import PyPDF2
|
|
817
817
|
import pandas as pd
|
|
818
818
|
try:
|
|
@@ -833,7 +833,7 @@ except Exception as e:
|
|
|
833
833
|
|
|
834
834
|
elif ext in ['.csv']:
|
|
835
835
|
return f"""
|
|
836
|
-
|
|
836
|
+
|
|
837
837
|
import pandas as pd
|
|
838
838
|
try:
|
|
839
839
|
{file_var_name}_df = pd.read_csv(r'{target_path}')
|
|
@@ -849,7 +849,7 @@ except Exception as e:
|
|
|
849
849
|
|
|
850
850
|
elif ext in ['.xlsx', '.xls']:
|
|
851
851
|
return f"""
|
|
852
|
-
|
|
852
|
+
|
|
853
853
|
import pandas as pd
|
|
854
854
|
try:
|
|
855
855
|
{file_var_name}_df = pd.read_excel(r'{target_path}')
|
|
@@ -865,7 +865,7 @@ except Exception as e:
|
|
|
865
865
|
|
|
866
866
|
elif ext in ['.json']:
|
|
867
867
|
return f"""
|
|
868
|
-
|
|
868
|
+
|
|
869
869
|
import json
|
|
870
870
|
try:
|
|
871
871
|
with open(r'{target_path}', 'r') as file:
|
|
@@ -883,7 +883,7 @@ except Exception as e:
|
|
|
883
883
|
|
|
884
884
|
elif ext in ['.txt', '.md']:
|
|
885
885
|
return f"""
|
|
886
|
-
|
|
886
|
+
|
|
887
887
|
try:
|
|
888
888
|
with open(r'{target_path}', 'r', encoding='utf-8') as file:
|
|
889
889
|
{file_var_name}_text = file.read()
|
|
@@ -898,7 +898,7 @@ except Exception as e:
|
|
|
898
898
|
|
|
899
899
|
elif ext in ['.png', '.jpg', '.jpeg', '.gif']:
|
|
900
900
|
return f"""
|
|
901
|
-
|
|
901
|
+
|
|
902
902
|
import matplotlib.pyplot as plt
|
|
903
903
|
from PIL import Image
|
|
904
904
|
import numpy as np
|
|
@@ -922,7 +922,7 @@ except Exception as e:
|
|
|
922
922
|
|
|
923
923
|
else:
|
|
924
924
|
return f"""
|
|
925
|
-
|
|
925
|
+
|
|
926
926
|
try:
|
|
927
927
|
with open(r'{target_path}', 'rb') as file:
|
|
928
928
|
{file_var_name}_data = file.read()
|
|
@@ -935,9 +935,9 @@ except Exception as e:
|
|
|
935
935
|
"""
|
|
936
936
|
def _handle_file_drop(input_text: str, npc_team_dir: Path) -> Tuple[str, List[str]]:
|
|
937
937
|
"""Handle file drops by copying files to appropriate workspace directories"""
|
|
938
|
-
|
|
938
|
+
|
|
939
939
|
|
|
940
|
-
|
|
940
|
+
|
|
941
941
|
stripped = input_text.strip("'\"")
|
|
942
942
|
if os.path.exists(stripped) and os.path.isfile(stripped):
|
|
943
943
|
print(f"[DEBUG] Direct file drop detected: {stripped}")
|
|
@@ -959,11 +959,11 @@ def _handle_file_drop(input_text: str, npc_team_dir: Path) -> Tuple[str, List[st
|
|
|
959
959
|
shutil.copy2(expanded_path, target_path)
|
|
960
960
|
print(f"📁 Copied {expanded_path.name} to workspace: {target_path}")
|
|
961
961
|
|
|
962
|
-
|
|
962
|
+
|
|
963
963
|
loading_code = _generate_file_analysis_code(str(expanded_path), str(target_path))
|
|
964
964
|
print(f"\n# Auto-generated file loading code:\n---\n{loading_code}\n---\n")
|
|
965
965
|
|
|
966
|
-
|
|
966
|
+
|
|
967
967
|
exec(loading_code)
|
|
968
968
|
|
|
969
969
|
return "", [str(target_path)]
|
|
@@ -971,12 +971,12 @@ def _handle_file_drop(input_text: str, npc_team_dir: Path) -> Tuple[str, List[st
|
|
|
971
971
|
print(f"[ERROR] Failed to process file drop: {e}")
|
|
972
972
|
return input_text, []
|
|
973
973
|
|
|
974
|
-
|
|
974
|
+
|
|
975
975
|
processed_files = []
|
|
976
976
|
file_paths = re.findall(r"'([^']+)'|\"([^\"]+)\"|(\S+)", input_text)
|
|
977
977
|
file_paths = [path for group in file_paths for path in group if path]
|
|
978
978
|
|
|
979
|
-
|
|
979
|
+
|
|
980
980
|
|
|
981
981
|
if not file_paths:
|
|
982
982
|
|
|
@@ -998,12 +998,12 @@ def _capture_plot_state(session_id: str, db_path: str, npc_team_dir: Path):
|
|
|
998
998
|
Session = sessionmaker(bind=engine)
|
|
999
999
|
session = Session()
|
|
1000
1000
|
|
|
1001
|
-
|
|
1001
|
+
|
|
1002
1002
|
fig = plt.gcf()
|
|
1003
1003
|
axes = fig.get_axes()
|
|
1004
1004
|
data_points = sum(len(line.get_xdata()) for ax in axes for line in ax.get_lines())
|
|
1005
1005
|
|
|
1006
|
-
|
|
1006
|
+
|
|
1007
1007
|
plot_hash = hashlib.md5(f"{len(axes)}{data_points}".encode()).hexdigest()
|
|
1008
1008
|
|
|
1009
1009
|
last = session.query(PlotState).filter(PlotState.session_id == session_id).order_by(PlotState.timestamp.desc()).first()
|
|
@@ -1011,13 +1011,13 @@ def _capture_plot_state(session_id: str, db_path: str, npc_team_dir: Path):
|
|
|
1011
1011
|
session.close()
|
|
1012
1012
|
return
|
|
1013
1013
|
|
|
1014
|
-
|
|
1014
|
+
|
|
1015
1015
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
1016
1016
|
workspace_dirs = _get_workspace_dirs(npc_team_dir)
|
|
1017
1017
|
plot_path = workspace_dirs["plots"] / f"state_{timestamp}.png"
|
|
1018
1018
|
plt.savefig(plot_path, dpi=150, bbox_inches='tight')
|
|
1019
1019
|
|
|
1020
|
-
|
|
1020
|
+
|
|
1021
1021
|
plot_state = PlotState(
|
|
1022
1022
|
session_id=session_id,
|
|
1023
1023
|
plot_hash=plot_hash,
|
|
@@ -1043,14 +1043,14 @@ def _capture_file_state(session_id: str, db_path: str, file_path: str, analysis_
|
|
|
1043
1043
|
Session = sessionmaker(bind=engine)
|
|
1044
1044
|
session = Session()
|
|
1045
1045
|
|
|
1046
|
-
|
|
1046
|
+
|
|
1047
1047
|
try:
|
|
1048
1048
|
with open(file_path, 'rb') as f:
|
|
1049
1049
|
file_hash = hashlib.md5(f.read()).hexdigest()
|
|
1050
1050
|
except:
|
|
1051
1051
|
file_hash = "unknown"
|
|
1052
1052
|
|
|
1053
|
-
|
|
1053
|
+
|
|
1054
1054
|
file_stem = Path(file_path).stem.lower()
|
|
1055
1055
|
vars_created = [k for k in locals_dict.keys() if not k.startswith('_') and file_stem in k.lower()]
|
|
1056
1056
|
|
|
@@ -1143,28 +1143,28 @@ import sys
|
|
|
1143
1143
|
from io import StringIO
|
|
1144
1144
|
from contextlib import redirect_stdout, redirect_stderr
|
|
1145
1145
|
|
|
1146
|
-
|
|
1146
|
+
|
|
1147
1147
|
def _get_guac_agent_emoji(failures: int, max_fail: int = 3) -> str:
|
|
1148
1148
|
"""
|
|
1149
1149
|
Returns an avocado emoji representing the state based on consecutive failures.
|
|
1150
1150
|
Includes "puke" emoji for max_fail, and "skull" for exceeding max_fail + 20.
|
|
1151
1151
|
"""
|
|
1152
1152
|
if failures == 0:
|
|
1153
|
-
return "🥑"
|
|
1153
|
+
return "🥑"
|
|
1154
1154
|
elif failures == 1:
|
|
1155
|
-
return "🥑🔪"
|
|
1155
|
+
return "🥑🔪"
|
|
1156
1156
|
elif failures == 2:
|
|
1157
|
-
return "🥑🥣"
|
|
1157
|
+
return "🥑🥣"
|
|
1158
1158
|
elif failures == max_fail:
|
|
1159
|
-
return "🥑🤢"
|
|
1160
|
-
elif failures > max_fail + 20:
|
|
1161
|
-
return "🥑💀"
|
|
1159
|
+
return "🥑🤢"
|
|
1160
|
+
elif failures > max_fail + 20:
|
|
1161
|
+
return "🥑💀"
|
|
1162
1162
|
elif failures > max_fail:
|
|
1163
|
-
return "🥑🟤"
|
|
1163
|
+
return "🥑🟤"
|
|
1164
1164
|
else:
|
|
1165
|
-
return "🥑❓"
|
|
1165
|
+
return "🥑❓"
|
|
1166
|
+
|
|
1166
1167
|
|
|
1167
|
-
# --- New Helper for Persisting Global Choice ---
|
|
1168
1168
|
GUAC_GLOBAL_FLAG_FILE = Path.home() / ".npcsh" / ".guac_use_global"
|
|
1169
1169
|
|
|
1170
1170
|
|
|
@@ -1173,14 +1173,14 @@ def _run_agentic_mode(command: str,
|
|
|
1173
1173
|
locals_dict: Dict[str, Any],
|
|
1174
1174
|
npc_team_dir: Path) -> Tuple[ShellState, Any]:
|
|
1175
1175
|
"""Run agentic mode with continuous iteration based on progress"""
|
|
1176
|
-
max_iterations = 5
|
|
1176
|
+
max_iterations = 5
|
|
1177
1177
|
iteration = 0
|
|
1178
1178
|
full_output = []
|
|
1179
1179
|
current_command = command
|
|
1180
1180
|
consecutive_failures = 0
|
|
1181
|
-
max_consecutive_failures = 3
|
|
1181
|
+
max_consecutive_failures = 3
|
|
1182
1182
|
|
|
1183
|
-
|
|
1183
|
+
|
|
1184
1184
|
existing_vars_context = "EXISTING VARIABLES IN ENVIRONMENT:\n"
|
|
1185
1185
|
for var_name, var_value in locals_dict.items():
|
|
1186
1186
|
if not var_name.startswith('_') and var_name not in ['In', 'Out', 'exit', 'quit', 'get_ipython']:
|
|
@@ -1259,7 +1259,8 @@ def _run_agentic_mode(command: str,
|
|
|
1259
1259
|
llm_response = get_llm_response(prompt,
|
|
1260
1260
|
npc=state.npc,
|
|
1261
1261
|
stream=True,
|
|
1262
|
-
messages=state.messages
|
|
1262
|
+
messages=state.messages,
|
|
1263
|
+
thinking=False)
|
|
1263
1264
|
|
|
1264
1265
|
generated_code = print_and_process_stream(llm_response.get('response'),
|
|
1265
1266
|
npc_model,
|
|
@@ -1270,12 +1271,14 @@ def _run_agentic_mode(command: str,
|
|
|
1270
1271
|
state.messages.append({'role':'assistant', 'content': generated_code})
|
|
1271
1272
|
|
|
1272
1273
|
if '<request_for_input>' in generated_code:
|
|
1274
|
+
|
|
1273
1275
|
generated_code = generated_code.split('>')[1].split('<')[0]
|
|
1274
1276
|
user_feedback = input("\n🤔 Agent requests feedback (press Enter to continue or type your input): ").strip()
|
|
1275
1277
|
current_command = f"{current_command} - User feedback: {user_feedback}"
|
|
1276
1278
|
max_iterations += int(max_iterations/2)
|
|
1277
1279
|
continue
|
|
1278
|
-
|
|
1280
|
+
if '<think>' in generated_code and '</think>' in generated_code:
|
|
1281
|
+
generated_code = generated_code.split('</think>')[1]
|
|
1279
1282
|
if generated_code.startswith('```python'):
|
|
1280
1283
|
generated_code = generated_code[len('```python'):].strip()
|
|
1281
1284
|
if generated_code.endswith('```'):
|
|
@@ -1417,16 +1420,16 @@ def execute_guac_command(command: str, state: ShellState, locals_dict: Dict[str,
|
|
|
1417
1420
|
|
|
1418
1421
|
|
|
1419
1422
|
|
|
1420
|
-
|
|
1423
|
+
|
|
1421
1424
|
if _detect_file_drop(stripped_command):
|
|
1422
1425
|
if stripped_command.startswith('run'):
|
|
1423
1426
|
pass
|
|
1424
1427
|
else:
|
|
1425
|
-
|
|
1428
|
+
|
|
1426
1429
|
file_path = stripped_command.strip("'\"")
|
|
1427
1430
|
expanded_path = Path(file_path).resolve()
|
|
1428
1431
|
|
|
1429
|
-
|
|
1432
|
+
|
|
1430
1433
|
workspace_dirs = _get_workspace_dirs(npc_team_dir)
|
|
1431
1434
|
_ensure_workspace_dirs(workspace_dirs)
|
|
1432
1435
|
|
|
@@ -1442,7 +1445,7 @@ def execute_guac_command(command: str, state: ShellState, locals_dict: Dict[str,
|
|
|
1442
1445
|
shutil.copy2(expanded_path, target_path)
|
|
1443
1446
|
print(f"📁 Copied {expanded_path.name} to workspace: {target_path}")
|
|
1444
1447
|
|
|
1445
|
-
|
|
1448
|
+
|
|
1446
1449
|
loading_code = _generate_file_analysis_code(str(expanded_path), str(target_path))
|
|
1447
1450
|
print(f"\n# Auto-generated file loading code:\n---\n{loading_code}\n---\n")
|
|
1448
1451
|
|
|
@@ -1452,25 +1455,25 @@ def execute_guac_command(command: str, state: ShellState, locals_dict: Dict[str,
|
|
|
1452
1455
|
print(f"[ERROR] Failed to copy or load file: {e}")
|
|
1453
1456
|
return state, f"Error loading file: {e}"
|
|
1454
1457
|
|
|
1455
|
-
|
|
1458
|
+
|
|
1456
1459
|
processed_command, processed_files, file_paths = _handle_file_drop(stripped_command, npc_team_dir)
|
|
1457
1460
|
if processed_files:
|
|
1458
1461
|
print(f"📁 Processed {len(processed_files)} files")
|
|
1459
1462
|
stripped_command = processed_command + 'Here are the files associated with the request'
|
|
1460
1463
|
|
|
1461
|
-
|
|
1464
|
+
|
|
1462
1465
|
if stripped_command == "/refresh":
|
|
1463
1466
|
_handle_guac_refresh(state, project_name, src_dir)
|
|
1464
1467
|
return state, "Refresh process initiated."
|
|
1465
1468
|
|
|
1466
|
-
|
|
1469
|
+
|
|
1467
1470
|
if stripped_command in ["/agent", "/chat", "/cmd"]:
|
|
1468
1471
|
state.current_mode = stripped_command[1:]
|
|
1469
1472
|
return state, f"Switched to {state.current_mode.upper()} mode."
|
|
1470
1473
|
|
|
1471
1474
|
|
|
1472
1475
|
|
|
1473
|
-
|
|
1476
|
+
|
|
1474
1477
|
if stripped_command.startswith('/') and stripped_command not in ["/refresh", "/agent", "/chat", "/cmd"]:
|
|
1475
1478
|
return execute_command(stripped_command, state, review=True, router=router)
|
|
1476
1479
|
if is_python_code(stripped_command):
|
|
@@ -1484,7 +1487,7 @@ def execute_guac_command(command: str, state: ShellState, locals_dict: Dict[str,
|
|
|
1484
1487
|
return _run_agentic_mode(stripped_command, state, locals_dict, npc_team_dir)
|
|
1485
1488
|
if state.current_mode == "cmd":
|
|
1486
1489
|
|
|
1487
|
-
|
|
1490
|
+
|
|
1488
1491
|
locals_context_string = "Current Python environment variables and functions:\n"
|
|
1489
1492
|
if locals_dict:
|
|
1490
1493
|
for k, v in locals_dict.items():
|
|
@@ -1493,14 +1496,14 @@ def execute_guac_command(command: str, state: ShellState, locals_dict: Dict[str,
|
|
|
1493
1496
|
value_repr = repr(v)
|
|
1494
1497
|
if len(value_repr) > 200:
|
|
1495
1498
|
value_repr = value_repr[:197] + "..."
|
|
1496
|
-
|
|
1499
|
+
loaals_context_string += f"- {k} (type: {type(v).__name__}) = {value_repr}\n"
|
|
1497
1500
|
except Exception:
|
|
1498
1501
|
locals_context_string += f"- {k} (type: {type(v).__name__}) = <unrepresentable>\n"
|
|
1499
1502
|
locals_context_string += "\n--- End of Environment Context ---\n"
|
|
1500
1503
|
else:
|
|
1501
1504
|
locals_context_string += "(Environment is empty)\n"
|
|
1502
1505
|
|
|
1503
|
-
|
|
1506
|
+
|
|
1504
1507
|
enhanced_prompt = stripped_command
|
|
1505
1508
|
if any(word in stripped_command.lower() for word in ['plot', 'graph', 'chart', 'figure', 'visualiz']):
|
|
1506
1509
|
plot_context = _get_plot_context(state.conversation_id, state.command_history.db_path)
|
|
@@ -1516,7 +1519,7 @@ def execute_guac_command(command: str, state: ShellState, locals_dict: Dict[str,
|
|
|
1516
1519
|
{locals_context_string}
|
|
1517
1520
|
Begin directly with the code
|
|
1518
1521
|
"""
|
|
1519
|
-
|
|
1522
|
+
|
|
1520
1523
|
npc_model = state.npc.model if state.npc and state.npc.model else state.chat_model
|
|
1521
1524
|
npc_provider = state.npc.provider if state.npc and state.npc.provider else state.chat_provider
|
|
1522
1525
|
|
|
@@ -1558,7 +1561,7 @@ def run_guac_repl(state: ShellState, project_name: str, package_root: Path, pack
|
|
|
1558
1561
|
from npcsh.routes import router
|
|
1559
1562
|
|
|
1560
1563
|
|
|
1561
|
-
|
|
1564
|
+
|
|
1562
1565
|
npc_team_dir = Path.cwd() / "npc_team"
|
|
1563
1566
|
workspace_dirs = _get_workspace_dirs(npc_team_dir)
|
|
1564
1567
|
_ensure_workspace_dirs(workspace_dirs)
|
|
@@ -1610,10 +1613,10 @@ def run_guac_repl(state: ShellState, project_name: str, package_root: Path, pack
|
|
|
1610
1613
|
return None
|
|
1611
1614
|
|
|
1612
1615
|
try:
|
|
1613
|
-
|
|
1616
|
+
|
|
1614
1617
|
file_ext = path.suffix.upper().lstrip('.')
|
|
1615
1618
|
if file_ext in ['PDF', 'DOCX', 'PPTX', 'HTML', 'HTM', 'CSV', 'XLS', 'XLSX', 'JSON']:
|
|
1616
|
-
chunks = load_file_contents(str(path), chunk_size=10000)
|
|
1619
|
+
chunks = load_file_contents(str(path), chunk_size=10000)
|
|
1617
1620
|
if chunks and not chunks[0].startswith("Error") and not chunks[0].startswith("Unsupported"):
|
|
1618
1621
|
content = '\n'.join(chunks)
|
|
1619
1622
|
lines = content.split('\n')
|
|
@@ -1632,7 +1635,7 @@ def run_guac_repl(state: ShellState, project_name: str, package_root: Path, pack
|
|
|
1632
1635
|
print(f"End of {path.name}")
|
|
1633
1636
|
return content
|
|
1634
1637
|
|
|
1635
|
-
|
|
1638
|
+
|
|
1636
1639
|
with open(path, 'r', encoding=encoding) as f:
|
|
1637
1640
|
lines = []
|
|
1638
1641
|
for i, line in enumerate(f, 1):
|
|
@@ -1680,10 +1683,10 @@ def run_guac_repl(state: ShellState, project_name: str, package_root: Path, pack
|
|
|
1680
1683
|
"""
|
|
1681
1684
|
path = Path(file_path).expanduser().resolve()
|
|
1682
1685
|
|
|
1683
|
-
|
|
1686
|
+
|
|
1684
1687
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1685
1688
|
|
|
1686
|
-
|
|
1689
|
+
|
|
1687
1690
|
if backup and path.exists():
|
|
1688
1691
|
backup_path = path.with_suffix(path.suffix + '.backup')
|
|
1689
1692
|
import shutil
|
|
@@ -1691,7 +1694,7 @@ def run_guac_repl(state: ShellState, project_name: str, package_root: Path, pack
|
|
|
1691
1694
|
print(f"Backup saved: {backup_path.name}")
|
|
1692
1695
|
|
|
1693
1696
|
try:
|
|
1694
|
-
|
|
1697
|
+
|
|
1695
1698
|
existing_lines = []
|
|
1696
1699
|
if path.exists():
|
|
1697
1700
|
with open(path, 'r', encoding='utf-8') as f:
|
|
@@ -1801,7 +1804,7 @@ def run_guac_repl(state: ShellState, project_name: str, package_root: Path, pack
|
|
|
1801
1804
|
state.current_path = os.getcwd()
|
|
1802
1805
|
|
|
1803
1806
|
display_model = state.chat_model
|
|
1804
|
-
|
|
1807
|
+
|
|
1805
1808
|
if isinstance(state.npc, NPC) and state.npc.model:
|
|
1806
1809
|
display_model = state.npc.model
|
|
1807
1810
|
|
|
@@ -1891,26 +1894,26 @@ def enter_guac_mode(npc=None,
|
|
|
1891
1894
|
package_name = setup_result.get("package_name", "project")
|
|
1892
1895
|
npc_team_dir = setup_result.get("npc_team_dir")
|
|
1893
1896
|
|
|
1894
|
-
|
|
1897
|
+
|
|
1895
1898
|
command_history, default_team, default_npc = setup_shell()
|
|
1896
1899
|
|
|
1897
|
-
|
|
1900
|
+
|
|
1898
1901
|
if npc is None and default_npc is None:
|
|
1899
|
-
|
|
1902
|
+
|
|
1900
1903
|
guac_npc_path = Path(npc_team_dir) / "guac.npc"
|
|
1901
1904
|
if guac_npc_path.exists():
|
|
1902
1905
|
npc = NPC(file=str(guac_npc_path), db_conn=command_history.engine)
|
|
1903
|
-
|
|
1906
|
+
|
|
1904
1907
|
team_ctx_path = Path(npc_team_dir) / "team.ctx"
|
|
1905
1908
|
if team_ctx_path.exists():
|
|
1906
1909
|
with open(team_ctx_path, "r") as f:
|
|
1907
1910
|
team_ctx = yaml.safe_load(f) or {}
|
|
1908
|
-
team = Team(team_path=str(npc_team_dir), forenpc=npc, jinxs={})
|
|
1911
|
+
team = Team(team_path=str(npc_team_dir), forenpc=npc, jinxs={})
|
|
1909
1912
|
team.name = team_ctx.get("team_name", "guac_global_team")
|
|
1910
1913
|
else:
|
|
1911
1914
|
raise RuntimeError(f"No NPC loaded and {guac_npc_path} not found!")
|
|
1912
1915
|
elif default_npc and npc is None:
|
|
1913
|
-
|
|
1916
|
+
|
|
1914
1917
|
npc = default_npc
|
|
1915
1918
|
|
|
1916
1919
|
|
|
@@ -1921,8 +1924,8 @@ def enter_guac_mode(npc=None,
|
|
|
1921
1924
|
chat_model=os.environ.get("NPCSH_CHAT_MODEL", "gemma3:4b"),
|
|
1922
1925
|
chat_provider=os.environ.get("NPCSH_CHAT_PROVIDER", "ollama"),
|
|
1923
1926
|
current_path=os.getcwd(),
|
|
1924
|
-
npc=npc,
|
|
1925
|
-
team=team or default_team
|
|
1927
|
+
npc=npc,
|
|
1928
|
+
team=team or default_team
|
|
1926
1929
|
)
|
|
1927
1930
|
|
|
1928
1931
|
state.command_history = command_history
|