npcsh 0.1.2__py3-none-any.whl → 1.1.13__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 +3508 -0
- npcsh/alicanto.py +65 -0
- npcsh/build.py +291 -0
- npcsh/completion.py +206 -0
- npcsh/config.py +163 -0
- npcsh/corca.py +50 -0
- npcsh/execution.py +185 -0
- npcsh/guac.py +46 -0
- npcsh/mcp_helpers.py +357 -0
- npcsh/mcp_server.py +299 -0
- npcsh/npc.py +323 -0
- npcsh/npc_team/alicanto.npc +2 -0
- npcsh/npc_team/alicanto.png +0 -0
- npcsh/npc_team/corca.npc +12 -0
- npcsh/npc_team/corca.png +0 -0
- npcsh/npc_team/corca_example.png +0 -0
- npcsh/npc_team/foreman.npc +7 -0
- npcsh/npc_team/frederic.npc +6 -0
- npcsh/npc_team/frederic4.png +0 -0
- npcsh/npc_team/guac.png +0 -0
- npcsh/npc_team/jinxs/code/python.jinx +11 -0
- npcsh/npc_team/jinxs/code/sh.jinx +34 -0
- npcsh/npc_team/jinxs/code/sql.jinx +16 -0
- npcsh/npc_team/jinxs/modes/alicanto.jinx +194 -0
- npcsh/npc_team/jinxs/modes/corca.jinx +249 -0
- npcsh/npc_team/jinxs/modes/guac.jinx +317 -0
- npcsh/npc_team/jinxs/modes/plonk.jinx +214 -0
- npcsh/npc_team/jinxs/modes/pti.jinx +170 -0
- npcsh/npc_team/jinxs/modes/spool.jinx +161 -0
- npcsh/npc_team/jinxs/modes/wander.jinx +186 -0
- npcsh/npc_team/jinxs/modes/yap.jinx +262 -0
- npcsh/npc_team/jinxs/npc_studio/npc-studio.jinx +77 -0
- npcsh/npc_team/jinxs/utils/agent.jinx +17 -0
- npcsh/npc_team/jinxs/utils/chat.jinx +44 -0
- npcsh/npc_team/jinxs/utils/cmd.jinx +44 -0
- npcsh/npc_team/jinxs/utils/compress.jinx +140 -0
- npcsh/npc_team/jinxs/utils/core/build.jinx +65 -0
- npcsh/npc_team/jinxs/utils/core/compile.jinx +50 -0
- npcsh/npc_team/jinxs/utils/core/help.jinx +52 -0
- npcsh/npc_team/jinxs/utils/core/init.jinx +41 -0
- npcsh/npc_team/jinxs/utils/core/jinxs.jinx +32 -0
- npcsh/npc_team/jinxs/utils/core/set.jinx +40 -0
- npcsh/npc_team/jinxs/utils/edit_file.jinx +94 -0
- npcsh/npc_team/jinxs/utils/load_file.jinx +35 -0
- npcsh/npc_team/jinxs/utils/ots.jinx +61 -0
- npcsh/npc_team/jinxs/utils/roll.jinx +68 -0
- npcsh/npc_team/jinxs/utils/sample.jinx +56 -0
- npcsh/npc_team/jinxs/utils/search.jinx +130 -0
- npcsh/npc_team/jinxs/utils/serve.jinx +26 -0
- npcsh/npc_team/jinxs/utils/sleep.jinx +116 -0
- npcsh/npc_team/jinxs/utils/trigger.jinx +61 -0
- npcsh/npc_team/jinxs/utils/usage.jinx +33 -0
- npcsh/npc_team/jinxs/utils/vixynt.jinx +144 -0
- npcsh/npc_team/kadiefa.npc +3 -0
- npcsh/npc_team/kadiefa.png +0 -0
- npcsh/npc_team/npcsh.ctx +18 -0
- npcsh/npc_team/npcsh_sibiji.png +0 -0
- npcsh/npc_team/plonk.npc +2 -0
- npcsh/npc_team/plonk.png +0 -0
- npcsh/npc_team/plonkjr.npc +2 -0
- npcsh/npc_team/plonkjr.png +0 -0
- npcsh/npc_team/sibiji.npc +3 -0
- npcsh/npc_team/sibiji.png +0 -0
- npcsh/npc_team/spool.png +0 -0
- npcsh/npc_team/yap.png +0 -0
- npcsh/npcsh.py +296 -112
- npcsh/parsing.py +118 -0
- npcsh/plonk.py +54 -0
- npcsh/pti.py +54 -0
- npcsh/routes.py +139 -0
- npcsh/spool.py +48 -0
- npcsh/ui.py +199 -0
- npcsh/wander.py +62 -0
- npcsh/yap.py +50 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/agent.jinx +17 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/alicanto.jinx +194 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/alicanto.npc +2 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/alicanto.png +0 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/build.jinx +65 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/chat.jinx +44 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/cmd.jinx +44 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/compile.jinx +50 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/compress.jinx +140 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/corca.jinx +249 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/corca.npc +12 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/corca.png +0 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/corca_example.png +0 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/edit_file.jinx +94 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/foreman.npc +7 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/frederic.npc +6 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/frederic4.png +0 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/guac.jinx +317 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/guac.png +0 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/help.jinx +52 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/init.jinx +41 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/jinxs.jinx +32 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/kadiefa.npc +3 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/kadiefa.png +0 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/load_file.jinx +35 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/npc-studio.jinx +77 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/npcsh.ctx +18 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/ots.jinx +61 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/plonk.jinx +214 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/plonk.npc +2 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/plonk.png +0 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/plonkjr.npc +2 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/plonkjr.png +0 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/pti.jinx +170 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/python.jinx +11 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/roll.jinx +68 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/sample.jinx +56 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/search.jinx +130 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/serve.jinx +26 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/set.jinx +40 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/sh.jinx +34 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/sibiji.npc +3 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/sibiji.png +0 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/sleep.jinx +116 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/spool.jinx +161 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/spool.png +0 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/sql.jinx +16 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/trigger.jinx +61 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/usage.jinx +33 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/vixynt.jinx +144 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/wander.jinx +186 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/yap.jinx +262 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/yap.png +0 -0
- npcsh-1.1.13.dist-info/METADATA +522 -0
- npcsh-1.1.13.dist-info/RECORD +135 -0
- {npcsh-0.1.2.dist-info → npcsh-1.1.13.dist-info}/WHEEL +1 -1
- npcsh-1.1.13.dist-info/entry_points.txt +9 -0
- {npcsh-0.1.2.dist-info → npcsh-1.1.13.dist-info/licenses}/LICENSE +1 -1
- npcsh/command_history.py +0 -81
- npcsh/helpers.py +0 -36
- npcsh/llm_funcs.py +0 -295
- npcsh/main.py +0 -5
- npcsh/modes.py +0 -343
- npcsh/npc_compiler.py +0 -124
- npcsh-0.1.2.dist-info/METADATA +0 -99
- npcsh-0.1.2.dist-info/RECORD +0 -14
- npcsh-0.1.2.dist-info/entry_points.txt +0 -2
- {npcsh-0.1.2.dist-info → npcsh-1.1.13.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
jinx_name: corca
|
|
2
|
+
description: MCP-powered agentic shell - LLM with tool use via MCP servers
|
|
3
|
+
inputs:
|
|
4
|
+
- mcp_server_path: null
|
|
5
|
+
- initial_command: null
|
|
6
|
+
- model: null
|
|
7
|
+
- provider: null
|
|
8
|
+
|
|
9
|
+
steps:
|
|
10
|
+
- name: corca_repl
|
|
11
|
+
engine: python
|
|
12
|
+
code: |
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
import asyncio
|
|
16
|
+
import json
|
|
17
|
+
from contextlib import AsyncExitStack
|
|
18
|
+
from termcolor import colored
|
|
19
|
+
|
|
20
|
+
from npcpy.llm_funcs import get_llm_response
|
|
21
|
+
from npcpy.npc_sysenv import render_markdown, get_system_message
|
|
22
|
+
|
|
23
|
+
# MCP imports
|
|
24
|
+
try:
|
|
25
|
+
from mcp import ClientSession, StdioServerParameters
|
|
26
|
+
from mcp.client.stdio import stdio_client
|
|
27
|
+
MCP_AVAILABLE = True
|
|
28
|
+
except ImportError:
|
|
29
|
+
MCP_AVAILABLE = False
|
|
30
|
+
print(colored("MCP not available. Install with: pip install mcp-client", "yellow"))
|
|
31
|
+
|
|
32
|
+
npc = context.get('npc')
|
|
33
|
+
team = context.get('team')
|
|
34
|
+
messages = context.get('messages', [])
|
|
35
|
+
mcp_server_path = context.get('mcp_server_path')
|
|
36
|
+
initial_command = context.get('initial_command')
|
|
37
|
+
|
|
38
|
+
model = context.get('model') or (npc.model if npc else None)
|
|
39
|
+
provider = context.get('provider') or (npc.provider if npc else None)
|
|
40
|
+
|
|
41
|
+
# Use shared_context for MCP state
|
|
42
|
+
shared_ctx = npc.shared_context if npc and hasattr(npc, 'shared_context') else {}
|
|
43
|
+
|
|
44
|
+
print("""
|
|
45
|
+
██████╗ ██████╗ ██████╗ ██████╗ █████╗
|
|
46
|
+
██╔════╝██╔═══██╗██╔══██╗██╔════╝██╔══██╗
|
|
47
|
+
██║ ██║ ██║██████╔╝██║ ███████║
|
|
48
|
+
██║ ██║ ██║██╔══██╗██║ ██╔══██╗
|
|
49
|
+
╚██████╗╚██████╔╝██║ ██║╚██████╗██║ ██║
|
|
50
|
+
╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝
|
|
51
|
+
""")
|
|
52
|
+
|
|
53
|
+
npc_name = npc.name if npc else "corca"
|
|
54
|
+
print(f"Entering corca mode (NPC: {npc_name}). Type '/cq' to exit.")
|
|
55
|
+
|
|
56
|
+
# ========== MCP Connection Setup ==========
|
|
57
|
+
async def connect_mcp(server_path):
|
|
58
|
+
"""Connect to MCP server and return tools"""
|
|
59
|
+
if not MCP_AVAILABLE:
|
|
60
|
+
return [], {}
|
|
61
|
+
|
|
62
|
+
abs_path = os.path.abspath(os.path.expanduser(server_path))
|
|
63
|
+
if not os.path.exists(abs_path):
|
|
64
|
+
print(colored(f"MCP server not found: {abs_path}", "red"))
|
|
65
|
+
return [], {}
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
loop = asyncio.get_event_loop()
|
|
69
|
+
except RuntimeError:
|
|
70
|
+
loop = asyncio.new_event_loop()
|
|
71
|
+
asyncio.set_event_loop(loop)
|
|
72
|
+
|
|
73
|
+
exit_stack = AsyncExitStack()
|
|
74
|
+
|
|
75
|
+
if abs_path.endswith('.py'):
|
|
76
|
+
cmd_parts = [sys.executable, abs_path]
|
|
77
|
+
else:
|
|
78
|
+
cmd_parts = [abs_path]
|
|
79
|
+
|
|
80
|
+
server_params = StdioServerParameters(
|
|
81
|
+
command=cmd_parts[0],
|
|
82
|
+
args=[abs_path],
|
|
83
|
+
env=os.environ.copy()
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
stdio_transport = await exit_stack.enter_async_context(stdio_client(server_params))
|
|
87
|
+
session = await exit_stack.enter_async_context(ClientSession(*stdio_transport))
|
|
88
|
+
await session.initialize()
|
|
89
|
+
|
|
90
|
+
response = await session.list_tools()
|
|
91
|
+
tools_llm = []
|
|
92
|
+
tool_map = {}
|
|
93
|
+
|
|
94
|
+
if response.tools:
|
|
95
|
+
for mcp_tool in response.tools:
|
|
96
|
+
tool_def = {
|
|
97
|
+
"type": "function",
|
|
98
|
+
"function": {
|
|
99
|
+
"name": mcp_tool.name,
|
|
100
|
+
"description": mcp_tool.description or f"MCP tool: {mcp_tool.name}",
|
|
101
|
+
"parameters": getattr(mcp_tool, "inputSchema", {"type": "object", "properties": {}})
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
tools_llm.append(tool_def)
|
|
105
|
+
|
|
106
|
+
# Create sync wrapper for async tool call
|
|
107
|
+
def make_tool_func(tool_name, sess, lp):
|
|
108
|
+
async def call_tool(**kwargs):
|
|
109
|
+
cleaned = {k: (None if v == 'None' else v) for k, v in kwargs.items()}
|
|
110
|
+
result = await asyncio.wait_for(sess.call_tool(tool_name, cleaned), timeout=30.0)
|
|
111
|
+
return result
|
|
112
|
+
def sync_call(**kwargs):
|
|
113
|
+
return lp.run_until_complete(call_tool(**kwargs))
|
|
114
|
+
return sync_call
|
|
115
|
+
|
|
116
|
+
tool_map[mcp_tool.name] = make_tool_func(mcp_tool.name, session, loop)
|
|
117
|
+
|
|
118
|
+
# Store in shared context
|
|
119
|
+
shared_ctx['mcp_client'] = session
|
|
120
|
+
shared_ctx['mcp_tools'] = tools_llm
|
|
121
|
+
shared_ctx['mcp_tool_map'] = tool_map
|
|
122
|
+
shared_ctx['_mcp_exit_stack'] = exit_stack
|
|
123
|
+
shared_ctx['_mcp_loop'] = loop
|
|
124
|
+
|
|
125
|
+
print(colored(f"Connected to MCP server. Tools: {', '.join(tool_map.keys())}", "green"))
|
|
126
|
+
return tools_llm, tool_map
|
|
127
|
+
|
|
128
|
+
# Try to connect if server path provided
|
|
129
|
+
tools_llm = shared_ctx.get('mcp_tools', [])
|
|
130
|
+
tool_map = shared_ctx.get('mcp_tool_map', {})
|
|
131
|
+
|
|
132
|
+
if mcp_server_path and not tools_llm:
|
|
133
|
+
try:
|
|
134
|
+
loop = asyncio.get_event_loop()
|
|
135
|
+
except RuntimeError:
|
|
136
|
+
loop = asyncio.new_event_loop()
|
|
137
|
+
asyncio.set_event_loop(loop)
|
|
138
|
+
tools_llm, tool_map = loop.run_until_complete(connect_mcp(mcp_server_path))
|
|
139
|
+
|
|
140
|
+
# Find default MCP server if none provided
|
|
141
|
+
if not tools_llm:
|
|
142
|
+
default_paths = [
|
|
143
|
+
os.path.expanduser("~/.npcsh/npc_team/mcp_server.py"),
|
|
144
|
+
os.path.join(team.team_path, "mcp_server.py") if team and hasattr(team, 'team_path') else None,
|
|
145
|
+
]
|
|
146
|
+
for path in default_paths:
|
|
147
|
+
if path and os.path.exists(path):
|
|
148
|
+
try:
|
|
149
|
+
loop = asyncio.get_event_loop()
|
|
150
|
+
except RuntimeError:
|
|
151
|
+
loop = asyncio.new_event_loop()
|
|
152
|
+
asyncio.set_event_loop(loop)
|
|
153
|
+
tools_llm, tool_map = loop.run_until_complete(connect_mcp(path))
|
|
154
|
+
if tools_llm:
|
|
155
|
+
break
|
|
156
|
+
|
|
157
|
+
# Ensure system message
|
|
158
|
+
if not messages or messages[0].get("role") != "system":
|
|
159
|
+
sys_msg = get_system_message(npc) if npc else "You are an AI assistant with access to tools."
|
|
160
|
+
if tools_llm:
|
|
161
|
+
sys_msg += f"\n\nYou have access to these tools: {', '.join(t['function']['name'] for t in tools_llm)}"
|
|
162
|
+
messages.insert(0, {"role": "system", "content": sys_msg})
|
|
163
|
+
|
|
164
|
+
# Handle initial command if provided (one-shot mode)
|
|
165
|
+
if initial_command:
|
|
166
|
+
resp = get_llm_response(
|
|
167
|
+
initial_command,
|
|
168
|
+
model=model,
|
|
169
|
+
provider=provider,
|
|
170
|
+
messages=messages,
|
|
171
|
+
tools=tools_llm if tools_llm else None,
|
|
172
|
+
tool_map=tool_map if tool_map else None,
|
|
173
|
+
auto_process_tool_calls=True,
|
|
174
|
+
npc=npc
|
|
175
|
+
)
|
|
176
|
+
messages = resp.get('messages', messages)
|
|
177
|
+
render_markdown(str(resp.get('response', '')))
|
|
178
|
+
context['output'] = resp.get('response', 'Done.')
|
|
179
|
+
context['messages'] = messages
|
|
180
|
+
# Don't enter REPL for one-shot
|
|
181
|
+
exit()
|
|
182
|
+
|
|
183
|
+
# REPL loop
|
|
184
|
+
while True:
|
|
185
|
+
try:
|
|
186
|
+
prompt_str = f"{npc_name}:corca> "
|
|
187
|
+
user_input = input(prompt_str).strip()
|
|
188
|
+
|
|
189
|
+
if not user_input:
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
if user_input.lower() == "/cq":
|
|
193
|
+
print("Exiting corca mode.")
|
|
194
|
+
break
|
|
195
|
+
|
|
196
|
+
# Handle /tools to list available tools
|
|
197
|
+
if user_input.lower() == "/tools":
|
|
198
|
+
if tools_llm:
|
|
199
|
+
print(colored("Available MCP tools:", "cyan"))
|
|
200
|
+
for t in tools_llm:
|
|
201
|
+
print(f" - {t['function']['name']}: {t['function'].get('description', '')[:60]}")
|
|
202
|
+
else:
|
|
203
|
+
print(colored("No MCP tools connected.", "yellow"))
|
|
204
|
+
continue
|
|
205
|
+
|
|
206
|
+
# Handle /connect to connect to new MCP server
|
|
207
|
+
if user_input.startswith("/connect "):
|
|
208
|
+
new_path = user_input[9:].strip()
|
|
209
|
+
try:
|
|
210
|
+
loop = asyncio.get_event_loop()
|
|
211
|
+
except RuntimeError:
|
|
212
|
+
loop = asyncio.new_event_loop()
|
|
213
|
+
asyncio.set_event_loop(loop)
|
|
214
|
+
tools_llm, tool_map = loop.run_until_complete(connect_mcp(new_path))
|
|
215
|
+
continue
|
|
216
|
+
|
|
217
|
+
# Get LLM response with tools
|
|
218
|
+
resp = get_llm_response(
|
|
219
|
+
user_input,
|
|
220
|
+
model=model,
|
|
221
|
+
provider=provider,
|
|
222
|
+
messages=messages,
|
|
223
|
+
tools=tools_llm if tools_llm else None,
|
|
224
|
+
tool_map=tool_map if tool_map else None,
|
|
225
|
+
auto_process_tool_calls=True,
|
|
226
|
+
stream=False, # Tool calls don't work well with streaming
|
|
227
|
+
npc=npc
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
messages = resp.get('messages', messages)
|
|
231
|
+
response_text = resp.get('response', '')
|
|
232
|
+
render_markdown(str(response_text))
|
|
233
|
+
|
|
234
|
+
# Track usage
|
|
235
|
+
if 'usage' in resp and npc and hasattr(npc, 'shared_context'):
|
|
236
|
+
usage = resp['usage']
|
|
237
|
+
npc.shared_context['session_input_tokens'] += usage.get('input_tokens', 0)
|
|
238
|
+
npc.shared_context['session_output_tokens'] += usage.get('output_tokens', 0)
|
|
239
|
+
npc.shared_context['turn_count'] += 1
|
|
240
|
+
|
|
241
|
+
except KeyboardInterrupt:
|
|
242
|
+
print("\nUse '/cq' to exit or continue.")
|
|
243
|
+
continue
|
|
244
|
+
except EOFError:
|
|
245
|
+
print("\nExiting corca mode.")
|
|
246
|
+
break
|
|
247
|
+
|
|
248
|
+
context['output'] = "Exited corca mode."
|
|
249
|
+
context['messages'] = messages
|
|
@@ -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
|