npcsh 1.0.14__py3-none-any.whl → 1.0.17__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 +1536 -78
- npcsh/corca.py +709 -0
- npcsh/guac.py +1433 -596
- npcsh/mcp_server.py +64 -60
- npcsh/npc.py +125 -98
- npcsh/npcsh.py +41 -1318
- npcsh/pti.py +195 -215
- npcsh/routes.py +106 -36
- npcsh/spool.py +138 -144
- {npcsh-1.0.14.dist-info → npcsh-1.0.17.dist-info}/METADATA +37 -367
- npcsh-1.0.17.dist-info/RECORD +21 -0
- {npcsh-1.0.14.dist-info → npcsh-1.0.17.dist-info}/entry_points.txt +1 -1
- npcsh/mcp_npcsh.py +0 -822
- npcsh-1.0.14.dist-info/RECORD +0 -21
- {npcsh-1.0.14.dist-info → npcsh-1.0.17.dist-info}/WHEEL +0 -0
- {npcsh-1.0.14.dist-info → npcsh-1.0.17.dist-info}/licenses/LICENSE +0 -0
- {npcsh-1.0.14.dist-info → npcsh-1.0.17.dist-info}/top_level.txt +0 -0
npcsh/pti.py
CHANGED
|
@@ -1,234 +1,214 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import shlex
|
|
4
|
+
import argparse
|
|
5
|
+
from typing import Dict, List, Any, Optional
|
|
6
|
+
|
|
7
|
+
from termcolor import colored
|
|
8
|
+
|
|
9
|
+
from npcpy.memory.command_history import CommandHistory, save_conversation_message
|
|
10
|
+
from npcpy.npc_sysenv import (
|
|
11
|
+
render_markdown
|
|
12
|
+
)
|
|
13
|
+
from npcpy.llm_funcs import get_llm_response
|
|
14
|
+
from npcpy.npc_compiler import NPC
|
|
15
|
+
from npcpy.data.load import load_file_contents
|
|
16
|
+
|
|
17
|
+
from npcsh._state import (
|
|
18
|
+
ShellState,
|
|
19
|
+
setup_shell,
|
|
20
|
+
get_multiline_input,
|
|
21
|
+
readline_safe_prompt,
|
|
22
|
+
get_npc_path
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
ice = "🧊"
|
|
26
|
+
bear = "🐻❄️"
|
|
27
|
+
def print_pti_welcome_message():
|
|
28
|
+
|
|
29
|
+
print(f"""
|
|
30
|
+
Welcome to PTI Mode!
|
|
1
31
|
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
from npcpy.npc_compiler import NPC
|
|
16
|
-
from npcpy.data.load import load_csv, load_pdf
|
|
17
|
-
from npcpy.data.text import rag_search
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def enter_reasoning_human_in_the_loop(
|
|
25
|
-
user_input=None,
|
|
26
|
-
messages: List[Dict[str, str]] = None,
|
|
27
|
-
reasoning_model: str = NPCSH_REASONING_MODEL,
|
|
28
|
-
reasoning_provider: str = NPCSH_REASONING_PROVIDER,
|
|
29
|
-
files : List = None,
|
|
30
|
-
npc: Any = None,
|
|
31
|
-
conversation_id : str= False,
|
|
32
|
-
answer_only: bool = False,
|
|
33
|
-
context=None,
|
|
34
|
-
) :
|
|
35
|
-
"""
|
|
36
|
-
Stream responses while checking for think tokens and handling human input when needed.
|
|
37
|
-
|
|
38
|
-
Args:
|
|
39
|
-
messages: List of conversation messages
|
|
40
|
-
model: LLM model to use
|
|
41
|
-
provider: Model provider
|
|
42
|
-
npc: NPC instance if applicable
|
|
43
|
-
|
|
44
|
-
"""
|
|
45
|
-
# Get the initial stream
|
|
46
|
-
loaded_content = {} # New dictionary to hold loaded content
|
|
47
|
-
|
|
48
|
-
# Create conversation ID if not provided
|
|
49
|
-
if not conversation_id:
|
|
50
|
-
conversation_id = start_new_conversation()
|
|
51
|
-
|
|
52
|
-
command_history = CommandHistory()
|
|
53
|
-
# Load specified files if any
|
|
54
|
-
if files:
|
|
55
|
-
for file in files:
|
|
56
|
-
extension = os.path.splitext(file)[1].lower()
|
|
57
|
-
try:
|
|
58
|
-
if extension == ".pdf":
|
|
59
|
-
content = load_pdf(file)["texts"].iloc[0]
|
|
60
|
-
elif extension == ".csv":
|
|
61
|
-
content = load_csv(file)
|
|
62
|
-
else:
|
|
63
|
-
print(f"Unsupported file type: {file}")
|
|
64
|
-
continue
|
|
65
|
-
loaded_content[file] = content
|
|
66
|
-
print(f"Loaded content from: {file}")
|
|
67
|
-
except Exception as e:
|
|
68
|
-
print(f"Error loading {file}: {str(e)}")
|
|
32
|
+
{ice}{ice}{ice} {ice}{ice}{ice} {bear}
|
|
33
|
+
{ice} {ice} {ice} {bear}
|
|
34
|
+
{ice}{ice}{ice} {ice} {bear}
|
|
35
|
+
{ice} {ice} {bear}
|
|
36
|
+
{ice} {ice} {bear}
|
|
37
|
+
|
|
38
|
+
Pardon-The-Interruption for human-in-the-loop reasoning.
|
|
39
|
+
Type 'exit' or 'quit' to return to the main shell.
|
|
40
|
+
""")
|
|
41
|
+
|
|
42
|
+
def enter_pti_mode(command: str, **kwargs):
|
|
43
|
+
state: ShellState = kwargs.get('shell_state')
|
|
44
|
+
command_history: CommandHistory = kwargs.get('command_history')
|
|
69
45
|
|
|
46
|
+
if not state or not command_history:
|
|
47
|
+
return {"output": "Error: PTI mode requires shell state and history.", "messages": kwargs.get('messages', [])}
|
|
70
48
|
|
|
49
|
+
all_command_parts = shlex.split(command)
|
|
50
|
+
parsed_args_list = all_command_parts[1:]
|
|
51
|
+
|
|
52
|
+
parser = argparse.ArgumentParser(prog="/pti", description="Enter PTI mode for human-in-the-loop reasoning.")
|
|
53
|
+
parser.add_argument('initial_prompt', nargs='*', help="Initial prompt to start the session.")
|
|
54
|
+
parser.add_argument("-f", "--files", nargs="*", default=[], help="Files to load into context.")
|
|
55
|
+
|
|
71
56
|
try:
|
|
72
|
-
|
|
57
|
+
args = parser.parse_args(parsed_args_list)
|
|
58
|
+
except SystemExit:
|
|
59
|
+
return {"output": "Invalid arguments for /pti. Usage: /pti [initial prompt] [-f file1 file2 ...]", "messages": state.messages}
|
|
73
60
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
assistant_reply = print_and_process_stream_with_markdown(assistant_reply, reasoning_model, reasoning_provider)
|
|
101
|
-
messages.append({'role':'assistant', 'content':assistant_reply})
|
|
102
|
-
return enter_reasoning_human_in_the_loop(user_input = None,
|
|
103
|
-
messages=messages,
|
|
104
|
-
reasoning_model=reasoning_model,
|
|
105
|
-
reasoning_provider=reasoning_provider, answer_only=False)
|
|
106
|
-
else:
|
|
107
|
-
message= "Think first though and use <think> tags in your chain of thought. Once finished, either answer plainly or write a request for input by beginning with the <request_for_input> tag. and close it with a </request_for_input>"
|
|
108
|
-
if user_input is None:
|
|
109
|
-
user_input = input('🐻❄️>')
|
|
61
|
+
print_pti_welcome_message()
|
|
62
|
+
|
|
63
|
+
frederic_path = get_npc_path("frederic", command_history.db_path)
|
|
64
|
+
state.npc = NPC(file=frederic_path)
|
|
65
|
+
print(colored("Defaulting to NPC: frederic", "cyan"))
|
|
66
|
+
state.npc = NPC(name="frederic")
|
|
67
|
+
|
|
68
|
+
pti_messages = list(state.messages)
|
|
69
|
+
loaded_content = {}
|
|
70
|
+
|
|
71
|
+
if args.files:
|
|
72
|
+
for file_path in args.files:
|
|
73
|
+
try:
|
|
74
|
+
content_chunks = load_file_contents(file_path)
|
|
75
|
+
loaded_content[file_path] = "\n".join(content_chunks)
|
|
76
|
+
print(colored(f"Successfully loaded content from: {file_path}", "green"))
|
|
77
|
+
except Exception as e:
|
|
78
|
+
print(colored(f"Error loading {file_path}: {e}", "red"))
|
|
79
|
+
|
|
80
|
+
user_input = " ".join(args.initial_prompt)
|
|
81
|
+
|
|
82
|
+
while True:
|
|
83
|
+
try:
|
|
84
|
+
if not user_input:
|
|
85
|
+
npc_name = state.npc.name if state.npc and isinstance(state.npc, NPC) else "frederic"
|
|
86
|
+
model_name = state.reasoning_model
|
|
110
87
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
88
|
+
prompt_str = f"{colored(os.path.basename(state.current_path), 'blue')}:{npc_name}:{model_name}{bear}> "
|
|
89
|
+
prompt = readline_safe_prompt(prompt_str)
|
|
90
|
+
user_input = get_multiline_input(prompt).strip()
|
|
91
|
+
|
|
92
|
+
if user_input.lower() in ["exit", "quit", "done"]:
|
|
93
|
+
break
|
|
94
|
+
|
|
95
|
+
if not user_input:
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
prompt_for_llm = user_input
|
|
99
|
+
if loaded_content:
|
|
100
|
+
context_str = "\n".join([f"--- Content from {fname} ---\n{content}" for fname, content in loaded_content.items()])
|
|
101
|
+
prompt_for_llm += f"\n\nUse the following context to inform your answer:\n{context_str}"
|
|
102
|
+
|
|
103
|
+
prompt_for_llm += "\n\nThink step-by-step using <think> tags. When you need more information from me, enclose your question in <request_for_input> tags."
|
|
104
|
+
|
|
105
|
+
save_conversation_message(
|
|
106
|
+
command_history,
|
|
107
|
+
state.conversation_id,
|
|
108
|
+
"user",
|
|
109
|
+
user_input,
|
|
110
|
+
wd=state.current_path,
|
|
111
|
+
model=state.reasoning_model,
|
|
112
|
+
provider=state.reasoning_provider,
|
|
113
|
+
npc=state.npc.name if isinstance(state.npc, NPC) else None,
|
|
114
|
+
)
|
|
115
|
+
pti_messages.append({"role": "user", "content": user_input})
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
response_dict = get_llm_response(
|
|
119
|
+
prompt=prompt_for_llm,
|
|
120
|
+
model=state.reasoning_model,
|
|
121
|
+
provider=state.reasoning_provider,
|
|
122
|
+
messages=pti_messages,
|
|
127
123
|
stream=True,
|
|
124
|
+
npc=state.npc
|
|
128
125
|
)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
thoughts = []
|
|
126
|
+
stream = response_dict.get('response')
|
|
127
|
+
|
|
132
128
|
response_chunks = []
|
|
133
|
-
|
|
129
|
+
request_found = False
|
|
134
130
|
|
|
135
|
-
|
|
131
|
+
for chunk in stream:
|
|
132
|
+
chunk_content = ""
|
|
133
|
+
if state.reasoning_provider == "ollama":
|
|
134
|
+
chunk_content = chunk.get("message", {}).get("content", "")
|
|
135
|
+
else:
|
|
136
|
+
chunk_content = "".join(
|
|
137
|
+
choice.delta.content
|
|
138
|
+
for choice in chunk.choices
|
|
139
|
+
if choice.delta.content is not None
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
print(chunk_content, end='')
|
|
143
|
+
sys.stdout.flush()
|
|
144
|
+
response_chunks.append(chunk_content)
|
|
145
|
+
|
|
146
|
+
combined_text = "".join(response_chunks)
|
|
147
|
+
if "</request_for_input>" in combined_text:
|
|
148
|
+
request_found = True
|
|
149
|
+
break
|
|
150
|
+
|
|
151
|
+
full_response_text = "".join(response_chunks)
|
|
152
|
+
|
|
153
|
+
save_conversation_message(
|
|
154
|
+
command_history,
|
|
155
|
+
state.conversation_id,
|
|
156
|
+
"assistant",
|
|
157
|
+
full_response_text,
|
|
158
|
+
wd=state.current_path,
|
|
159
|
+
model=state.reasoning_model,
|
|
160
|
+
provider=state.reasoning_provider,
|
|
161
|
+
npc=state.npc.name if isinstance(state.npc, NPC) else None,
|
|
162
|
+
)
|
|
163
|
+
pti_messages.append({"role": "assistant", "content": full_response_text})
|
|
136
164
|
|
|
165
|
+
print()
|
|
166
|
+
user_input = None
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
except KeyboardInterrupt:
|
|
170
|
+
print(colored("\n\n--- Stream Interrupted ---", "yellow"))
|
|
171
|
+
interrupt_text = input('🐻❄️> ').strip()
|
|
172
|
+
if interrupt_text:
|
|
173
|
+
user_input = interrupt_text
|
|
174
|
+
else:
|
|
175
|
+
user_input = None
|
|
176
|
+
continue
|
|
137
177
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if reasoning_provider == "ollama":
|
|
145
|
-
chunk_content = chunk.get("message", {}).get("content", "")
|
|
146
|
-
else:
|
|
147
|
-
chunk_content = ''
|
|
148
|
-
reasoning_content = ''
|
|
149
|
-
for c in chunk.choices:
|
|
150
|
-
if hasattr(c.delta, "reasoning_content"):
|
|
151
|
-
|
|
152
|
-
reasoning_content += c.delta.reasoning_content
|
|
153
|
-
|
|
154
|
-
if reasoning_content:
|
|
155
|
-
thinking = True
|
|
156
|
-
chunk_content = reasoning_content
|
|
157
|
-
chunk_content += "".join(
|
|
158
|
-
choice.delta.content
|
|
159
|
-
for choice in chunk.choices
|
|
160
|
-
if choice.delta.content is not None
|
|
161
|
-
)
|
|
162
|
-
response_chunks.append(chunk_content)
|
|
163
|
-
print(chunk_content, end='')
|
|
164
|
-
combined_text = "".join(response_chunks)
|
|
165
|
-
|
|
166
|
-
if in_think_block:
|
|
167
|
-
if '</thinking>' in combined_text:
|
|
168
|
-
in_think_block = False
|
|
169
|
-
thoughts.append(chunk_content)
|
|
170
|
-
|
|
171
|
-
if "</request_for_input>" in combined_text:
|
|
172
|
-
# Process the LLM's input request
|
|
173
|
-
request_text = "".join(thoughts)
|
|
174
|
-
|
|
175
|
-
print("\nPlease provide the requested information: ")
|
|
176
|
-
|
|
177
|
-
user_input = input('🐻❄️>')
|
|
178
|
-
|
|
179
|
-
messages.append({"role": "assistant", "content": request_text})
|
|
180
|
-
|
|
181
|
-
print("\n[Continuing with provided information...]\n")
|
|
182
|
-
return enter_reasoning_human_in_the_loop( user_input = user_input,
|
|
183
|
-
messages=messages,
|
|
184
|
-
reasoning_model=reasoning_model,
|
|
185
|
-
reasoning_provider=reasoning_provider,
|
|
186
|
-
npc=npc,
|
|
187
|
-
answer_only=True)
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
except KeyboardInterrupt:
|
|
191
|
-
user_interrupt = input("\n[Stream interrupted by user]\n Enter your additional input: ")
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
# Add the interruption to messages and restart stream
|
|
195
|
-
messages.append(
|
|
196
|
-
{"role": "user", "content": f"[INTERRUPT] {user_interrupt}"}
|
|
197
|
-
)
|
|
198
|
-
print(f"\n[Continuing with added context...]\n")
|
|
199
|
-
|
|
200
|
-
except KeyboardInterrupt:
|
|
201
|
-
user_interrupt = input("\n[Stream interrupted by user]\n 🔴🔴🔴🔴\nEnter your additional input: ")
|
|
202
|
-
|
|
178
|
+
except KeyboardInterrupt:
|
|
179
|
+
print()
|
|
180
|
+
continue
|
|
181
|
+
except EOFError:
|
|
182
|
+
print("\nExiting PTI Mode.")
|
|
183
|
+
break
|
|
203
184
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
{"role": "user", "content": f"[INTERRUPT] {user_interrupt}"}
|
|
207
|
-
)
|
|
208
|
-
print(f"\n[Continuing with added context...]\n")
|
|
209
|
-
|
|
210
|
-
return {'messages':messages, }
|
|
211
|
-
|
|
185
|
+
render_markdown("\n# Exiting PTI Mode")
|
|
186
|
+
return {"output": "", "messages": pti_messages}
|
|
212
187
|
|
|
213
188
|
def main():
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
parser
|
|
217
|
-
parser.add_argument("--npc", default='~/.npcsh/npc_team/frederic.npc', help="Path to NPC File")
|
|
218
|
-
parser.add_argument("--model", default=NPCSH_REASONING_MODEL, help="Model to use")
|
|
219
|
-
parser.add_argument("--provider", default=NPCSH_REASONING_PROVIDER, help="Provider to use")
|
|
220
|
-
parser.add_argument("--files", nargs="*", help="Files to load into context")
|
|
189
|
+
parser = argparse.ArgumentParser(description="PTI - Pardon-The-Interruption human-in-the-loop shell.")
|
|
190
|
+
parser.add_argument('initial_prompt', nargs='*', help="Initial prompt to start the session.")
|
|
191
|
+
parser.add_argument("-f", "--files", nargs="*", default=[], help="Files to load into context.")
|
|
221
192
|
args = parser.parse_args()
|
|
193
|
+
|
|
194
|
+
command_history, team, default_npc = setup_shell()
|
|
195
|
+
|
|
196
|
+
from npcsh._state import initial_state
|
|
197
|
+
initial_shell_state = initial_state
|
|
198
|
+
initial_shell_state.team = team
|
|
199
|
+
initial_shell_state.npc = default_npc
|
|
222
200
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
201
|
+
fake_command_str = "/pti " + " ".join(args.initial_prompt)
|
|
202
|
+
if args.files:
|
|
203
|
+
fake_command_str += " --files " + " ".join(args.files)
|
|
204
|
+
|
|
205
|
+
kwargs = {
|
|
206
|
+
'command': fake_command_str,
|
|
207
|
+
'shell_state': initial_shell_state,
|
|
208
|
+
'command_history': command_history
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
enter_pti_mode(**kwargs)
|
|
231
212
|
|
|
232
213
|
if __name__ == "__main__":
|
|
233
|
-
main()
|
|
234
|
-
|
|
214
|
+
main()
|