symbolicai 1.0.0__py3-none-any.whl → 1.1.0__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.
- symai/__init__.py +198 -134
- symai/backend/base.py +51 -51
- symai/backend/engines/drawing/engine_bfl.py +33 -33
- symai/backend/engines/drawing/engine_gpt_image.py +4 -10
- symai/backend/engines/embedding/engine_llama_cpp.py +50 -35
- symai/backend/engines/embedding/engine_openai.py +22 -16
- symai/backend/engines/execute/engine_python.py +16 -16
- symai/backend/engines/files/engine_io.py +51 -49
- symai/backend/engines/imagecaptioning/engine_blip2.py +27 -23
- symai/backend/engines/imagecaptioning/engine_llavacpp_client.py +53 -46
- symai/backend/engines/index/engine_pinecone.py +116 -88
- symai/backend/engines/index/engine_qdrant.py +1011 -0
- symai/backend/engines/index/engine_vectordb.py +78 -52
- symai/backend/engines/lean/engine_lean4.py +65 -25
- symai/backend/engines/neurosymbolic/__init__.py +28 -28
- symai/backend/engines/neurosymbolic/engine_anthropic_claudeX_chat.py +137 -135
- symai/backend/engines/neurosymbolic/engine_anthropic_claudeX_reasoning.py +145 -152
- symai/backend/engines/neurosymbolic/engine_cerebras.py +328 -0
- symai/backend/engines/neurosymbolic/engine_deepseekX_reasoning.py +75 -49
- symai/backend/engines/neurosymbolic/engine_google_geminiX_reasoning.py +199 -155
- symai/backend/engines/neurosymbolic/engine_groq.py +106 -72
- symai/backend/engines/neurosymbolic/engine_huggingface.py +100 -67
- symai/backend/engines/neurosymbolic/engine_llama_cpp.py +121 -93
- symai/backend/engines/neurosymbolic/engine_openai_gptX_chat.py +213 -132
- symai/backend/engines/neurosymbolic/engine_openai_gptX_reasoning.py +180 -137
- symai/backend/engines/ocr/engine_apilayer.py +18 -20
- symai/backend/engines/output/engine_stdout.py +9 -9
- symai/backend/engines/{webscraping → scrape}/engine_requests.py +25 -11
- symai/backend/engines/search/engine_openai.py +95 -83
- symai/backend/engines/search/engine_parallel.py +665 -0
- symai/backend/engines/search/engine_perplexity.py +40 -41
- symai/backend/engines/search/engine_serpapi.py +33 -28
- symai/backend/engines/speech_to_text/engine_local_whisper.py +37 -27
- symai/backend/engines/symbolic/engine_wolframalpha.py +14 -8
- symai/backend/engines/text_to_speech/engine_openai.py +15 -19
- symai/backend/engines/text_vision/engine_clip.py +34 -28
- symai/backend/engines/userinput/engine_console.py +3 -4
- symai/backend/mixin/anthropic.py +48 -40
- symai/backend/mixin/deepseek.py +4 -5
- symai/backend/mixin/google.py +5 -4
- symai/backend/mixin/groq.py +2 -4
- symai/backend/mixin/openai.py +132 -110
- symai/backend/settings.py +14 -14
- symai/chat.py +164 -94
- symai/collect/dynamic.py +13 -11
- symai/collect/pipeline.py +39 -31
- symai/collect/stats.py +109 -69
- symai/components.py +556 -238
- symai/constraints.py +14 -5
- symai/core.py +1495 -1210
- symai/core_ext.py +55 -50
- symai/endpoints/api.py +113 -58
- symai/extended/api_builder.py +22 -17
- symai/extended/arxiv_pdf_parser.py +13 -5
- symai/extended/bibtex_parser.py +8 -4
- symai/extended/conversation.py +88 -69
- symai/extended/document.py +40 -27
- symai/extended/file_merger.py +45 -7
- symai/extended/graph.py +38 -24
- symai/extended/html_style_template.py +17 -11
- symai/extended/interfaces/blip_2.py +1 -1
- symai/extended/interfaces/clip.py +4 -2
- symai/extended/interfaces/console.py +5 -3
- symai/extended/interfaces/dall_e.py +3 -1
- symai/extended/interfaces/file.py +2 -0
- symai/extended/interfaces/flux.py +3 -1
- symai/extended/interfaces/gpt_image.py +15 -6
- symai/extended/interfaces/input.py +2 -1
- symai/extended/interfaces/llava.py +1 -1
- symai/extended/interfaces/{naive_webscraping.py → naive_scrape.py} +3 -2
- symai/extended/interfaces/naive_vectordb.py +2 -2
- symai/extended/interfaces/ocr.py +4 -2
- symai/extended/interfaces/openai_search.py +2 -0
- symai/extended/interfaces/parallel.py +30 -0
- symai/extended/interfaces/perplexity.py +2 -0
- symai/extended/interfaces/pinecone.py +6 -4
- symai/extended/interfaces/python.py +2 -0
- symai/extended/interfaces/serpapi.py +2 -0
- symai/extended/interfaces/terminal.py +0 -1
- symai/extended/interfaces/tts.py +2 -1
- symai/extended/interfaces/whisper.py +2 -1
- symai/extended/interfaces/wolframalpha.py +1 -0
- symai/extended/metrics/__init__.py +1 -1
- symai/extended/metrics/similarity.py +5 -2
- symai/extended/os_command.py +31 -22
- symai/extended/packages/symdev.py +39 -34
- symai/extended/packages/sympkg.py +30 -27
- symai/extended/packages/symrun.py +46 -35
- symai/extended/repo_cloner.py +10 -9
- symai/extended/seo_query_optimizer.py +15 -12
- symai/extended/solver.py +104 -76
- symai/extended/summarizer.py +8 -7
- symai/extended/taypan_interpreter.py +10 -9
- symai/extended/vectordb.py +28 -15
- symai/formatter/formatter.py +39 -31
- symai/formatter/regex.py +46 -44
- symai/functional.py +184 -86
- symai/imports.py +85 -51
- symai/interfaces.py +1 -1
- symai/memory.py +33 -24
- symai/menu/screen.py +28 -19
- symai/misc/console.py +27 -27
- symai/misc/loader.py +4 -3
- symai/models/base.py +147 -76
- symai/models/errors.py +1 -1
- symai/ops/__init__.py +1 -1
- symai/ops/measures.py +17 -14
- symai/ops/primitives.py +933 -635
- symai/post_processors.py +28 -24
- symai/pre_processors.py +58 -52
- symai/processor.py +15 -9
- symai/prompts.py +714 -649
- symai/server/huggingface_server.py +115 -32
- symai/server/llama_cpp_server.py +14 -6
- symai/server/qdrant_server.py +206 -0
- symai/shell.py +98 -39
- symai/shellsv.py +307 -223
- symai/strategy.py +135 -81
- symai/symbol.py +276 -225
- symai/utils.py +62 -46
- {symbolicai-1.0.0.dist-info → symbolicai-1.1.0.dist-info}/METADATA +19 -9
- symbolicai-1.1.0.dist-info/RECORD +168 -0
- symbolicai-1.0.0.dist-info/RECORD +0 -163
- {symbolicai-1.0.0.dist-info → symbolicai-1.1.0.dist-info}/WHEEL +0 -0
- {symbolicai-1.0.0.dist-info → symbolicai-1.1.0.dist-info}/entry_points.txt +0 -0
- {symbolicai-1.0.0.dist-info → symbolicai-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {symbolicai-1.0.0.dist-info → symbolicai-1.1.0.dist-info}/top_level.txt +0 -0
symai/shellsv.py
CHANGED
|
@@ -13,7 +13,7 @@ from collections.abc import Iterable
|
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
from types import SimpleNamespace
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
# @TODO: refactor to use rich instead of prompt_toolkit
|
|
17
17
|
from prompt_toolkit import HTML, PromptSession, print_formatted_text
|
|
18
18
|
from prompt_toolkit.completion import Completer, Completion, WordCompleter
|
|
19
19
|
from prompt_toolkit.history import History
|
|
@@ -47,26 +47,26 @@ logging.getLogger("subprocess").setLevel(logging.ERROR)
|
|
|
47
47
|
|
|
48
48
|
# load json config from home directory root
|
|
49
49
|
home_path = HOME_PATH
|
|
50
|
-
config_path = home_path /
|
|
50
|
+
config_path = home_path / "symsh.config.json"
|
|
51
51
|
# migrate config from old path
|
|
52
|
-
if
|
|
52
|
+
if "colors" not in SYMSH_CONFIG:
|
|
53
53
|
__new_config__ = {"colors": SYMSH_CONFIG}
|
|
54
54
|
# add command in config
|
|
55
55
|
SYMSH_CONFIG = __new_config__
|
|
56
56
|
# save config
|
|
57
|
-
with config_path.open(
|
|
57
|
+
with config_path.open("w") as f:
|
|
58
58
|
json.dump(__new_config__, f, indent=4)
|
|
59
59
|
|
|
60
60
|
# make sure map-nt-cmd is in config
|
|
61
|
-
if
|
|
61
|
+
if "map-nt-cmd" not in SYMSH_CONFIG:
|
|
62
62
|
# add command in config
|
|
63
|
-
SYMSH_CONFIG[
|
|
63
|
+
SYMSH_CONFIG["map-nt-cmd"] = True
|
|
64
64
|
# save config
|
|
65
|
-
with config_path.open(
|
|
65
|
+
with config_path.open("w") as f:
|
|
66
66
|
json.dump(SYMSH_CONFIG, f, indent=4)
|
|
67
67
|
|
|
68
|
-
print = print_formatted_text
|
|
69
|
-
map_nt_cmd_enabled = SYMSH_CONFIG[
|
|
68
|
+
print = print_formatted_text # noqa
|
|
69
|
+
map_nt_cmd_enabled = SYMSH_CONFIG["map-nt-cmd"]
|
|
70
70
|
|
|
71
71
|
_shell_state = SimpleNamespace(
|
|
72
72
|
function_type=Function,
|
|
@@ -91,6 +91,7 @@ If additional instructions are provided the follow the user query to produce the
|
|
|
91
91
|
A well related and helpful answer with suggested improvements is preferred over "I don't know" or "I don't understand" answers or stating the obvious.
|
|
92
92
|
"""
|
|
93
93
|
|
|
94
|
+
|
|
94
95
|
def supports_ansi_escape():
|
|
95
96
|
try:
|
|
96
97
|
os.get_terminal_size(0)
|
|
@@ -98,22 +99,23 @@ def supports_ansi_escape():
|
|
|
98
99
|
except OSError:
|
|
99
100
|
return False
|
|
100
101
|
|
|
102
|
+
|
|
101
103
|
class PathCompleter(Completer):
|
|
102
104
|
def get_completions(self, document, _complete_event):
|
|
103
105
|
complete_word = document.get_word_before_cursor(WORD=True)
|
|
104
106
|
sep = os.path.sep
|
|
105
|
-
if complete_word.startswith(f
|
|
107
|
+
if complete_word.startswith(f"~{sep}"):
|
|
106
108
|
complete_word = FileReader.expand_user_path(complete_word)
|
|
107
109
|
|
|
108
110
|
# list all files and directories in current directory
|
|
109
111
|
complete_path = Path(complete_word)
|
|
110
112
|
if complete_word.endswith(sep):
|
|
111
113
|
parent = complete_path
|
|
112
|
-
pattern =
|
|
114
|
+
pattern = "*"
|
|
113
115
|
else:
|
|
114
116
|
baseline = Path()
|
|
115
117
|
parent = complete_path.parent if complete_path.parent != baseline else baseline
|
|
116
|
-
pattern = f"{complete_path.name}*" if complete_path.name else
|
|
118
|
+
pattern = f"{complete_path.name}*" if complete_path.name else "*"
|
|
117
119
|
files = [str(path) for path in parent.glob(pattern)]
|
|
118
120
|
if len(files) == 0:
|
|
119
121
|
return None
|
|
@@ -124,16 +126,18 @@ class PathCompleter(Completer):
|
|
|
124
126
|
for file in files:
|
|
125
127
|
path_obj = Path(file)
|
|
126
128
|
# split the command into words by space (ignore escaped spaces)
|
|
127
|
-
command_words = document.text.split(
|
|
129
|
+
command_words = document.text.split(" ")
|
|
128
130
|
if len(command_words) > 1:
|
|
129
131
|
# Calculate start position of the completion
|
|
130
|
-
start_position = len(document.text) - len(
|
|
132
|
+
start_position = len(document.text) - len(" ".join(command_words[:-1])) - 1
|
|
131
133
|
start_position = max(0, start_position)
|
|
132
134
|
else:
|
|
133
135
|
start_position = len(document.text)
|
|
134
136
|
# if there is a space in the file name, then escape it
|
|
135
|
-
display_name = file.replace(
|
|
136
|
-
if (
|
|
137
|
+
display_name = file.replace(" ", "\\ ") if " " in file else file
|
|
138
|
+
if (
|
|
139
|
+
document.text.startswith("cd") or document.text.startswith("mkdir")
|
|
140
|
+
) and path_obj.is_file():
|
|
137
141
|
continue
|
|
138
142
|
if path_obj.is_dir():
|
|
139
143
|
dirs_.append(display_name)
|
|
@@ -143,25 +147,33 @@ class PathCompleter(Completer):
|
|
|
143
147
|
for d in dirs_:
|
|
144
148
|
# if starts with home directory, then replace it with ~
|
|
145
149
|
directory_completion = FileReader.expand_user_path(d)
|
|
146
|
-
yield Completion(
|
|
147
|
-
|
|
148
|
-
|
|
150
|
+
yield Completion(
|
|
151
|
+
directory_completion,
|
|
152
|
+
start_position=-start_position,
|
|
153
|
+
style="class:path-completion",
|
|
154
|
+
selected_style="class:path-completion-selected",
|
|
155
|
+
)
|
|
149
156
|
|
|
150
157
|
for f in files_:
|
|
151
158
|
# if starts with home directory, then replace it with ~
|
|
152
159
|
file_completion = FileReader.expand_user_path(f)
|
|
153
|
-
yield Completion(
|
|
154
|
-
|
|
155
|
-
|
|
160
|
+
yield Completion(
|
|
161
|
+
file_completion,
|
|
162
|
+
start_position=-start_position,
|
|
163
|
+
style="class:file-completion",
|
|
164
|
+
selected_style="class:file-completion-selected",
|
|
165
|
+
)
|
|
166
|
+
|
|
156
167
|
|
|
157
168
|
class HistoryCompleter(WordCompleter):
|
|
158
169
|
def get_completions(self, document, complete_event):
|
|
159
170
|
completions = super().get_completions(document, complete_event)
|
|
160
171
|
for completion in completions:
|
|
161
|
-
completion.style =
|
|
162
|
-
completion.selected_style =
|
|
172
|
+
completion.style = "class:history-completion"
|
|
173
|
+
completion.selected_style = "class:history-completion-selected"
|
|
163
174
|
yield completion
|
|
164
175
|
|
|
176
|
+
|
|
165
177
|
class MergedCompleter(Completer):
|
|
166
178
|
def __init__(self, path_completer, history_completer):
|
|
167
179
|
self.path_completer = path_completer
|
|
@@ -170,46 +182,52 @@ class MergedCompleter(Completer):
|
|
|
170
182
|
def get_completions(self, document, complete_event):
|
|
171
183
|
text = document.text
|
|
172
184
|
|
|
173
|
-
if
|
|
174
|
-
text.startswith(
|
|
175
|
-
text.startswith(
|
|
176
|
-
text.startswith(
|
|
177
|
-
text.startswith(
|
|
178
|
-
text.startswith(
|
|
179
|
-
text.startswith(
|
|
180
|
-
text.startswith(
|
|
181
|
-
text.startswith(
|
|
182
|
-
text.startswith(
|
|
183
|
-
text.startswith(
|
|
184
|
-
text.startswith(
|
|
185
|
-
text.startswith(r
|
|
186
|
-
text.startswith(r
|
|
187
|
-
text.startswith(
|
|
188
|
-
text.startswith(
|
|
189
|
-
text.startswith(
|
|
190
|
-
text.startswith(
|
|
191
|
-
text.startswith(
|
|
192
|
-
text.startswith(
|
|
185
|
+
if (
|
|
186
|
+
text.startswith("cd ")
|
|
187
|
+
or text.startswith("ls ")
|
|
188
|
+
or text.startswith("touch ")
|
|
189
|
+
or text.startswith("cat ")
|
|
190
|
+
or text.startswith("mkdir ")
|
|
191
|
+
or text.startswith("open ")
|
|
192
|
+
or text.startswith("rm ")
|
|
193
|
+
or text.startswith("git ")
|
|
194
|
+
or text.startswith("vi ")
|
|
195
|
+
or text.startswith("nano ")
|
|
196
|
+
or text.startswith("*")
|
|
197
|
+
or text.startswith(r".\\")
|
|
198
|
+
or text.startswith(r"~\\")
|
|
199
|
+
or text.startswith(r"\\")
|
|
200
|
+
or text.startswith(".\\")
|
|
201
|
+
or text.startswith("~\\")
|
|
202
|
+
or text.startswith("\\")
|
|
203
|
+
or text.startswith("./")
|
|
204
|
+
or text.startswith("~/")
|
|
205
|
+
or text.startswith("/")
|
|
206
|
+
):
|
|
193
207
|
yield from self.path_completer.get_completions(document, complete_event)
|
|
194
208
|
yield from self.history_completer.get_completions(document, complete_event)
|
|
195
209
|
else:
|
|
196
210
|
yield from self.history_completer.get_completions(document, complete_event)
|
|
197
211
|
yield from self.path_completer.get_completions(document, complete_event)
|
|
198
212
|
|
|
213
|
+
|
|
199
214
|
# Create custom keybindings
|
|
200
215
|
bindings = KeyBindings()
|
|
201
216
|
# Get a copy of the current environment
|
|
202
217
|
default_env = os.environ.copy()
|
|
203
218
|
|
|
219
|
+
|
|
204
220
|
def get_exec_prefix():
|
|
205
221
|
exec_prefix = _shell_state.exec_prefix
|
|
206
|
-
return sys.exec_prefix if exec_prefix ==
|
|
222
|
+
return sys.exec_prefix if exec_prefix == "default" else exec_prefix
|
|
223
|
+
|
|
207
224
|
|
|
208
225
|
def get_conda_env():
|
|
209
226
|
# what conda env am I in (e.g., where is my Python process from)?
|
|
210
227
|
ENVBIN = get_exec_prefix()
|
|
211
228
|
return Path(ENVBIN).name
|
|
212
229
|
|
|
230
|
+
|
|
213
231
|
# bind to 'Ctrl' + 'Space'
|
|
214
232
|
@bindings.add(Keys.ControlSpace)
|
|
215
233
|
def _(event):
|
|
@@ -222,13 +240,14 @@ def _(event):
|
|
|
222
240
|
kb = KeyBindings()
|
|
223
241
|
|
|
224
242
|
cancel = [False]
|
|
225
|
-
|
|
243
|
+
|
|
244
|
+
@kb.add("f")
|
|
226
245
|
def _(_event):
|
|
227
|
-
UserMessage(
|
|
246
|
+
UserMessage("You pressed `f`.", style="alert")
|
|
228
247
|
|
|
229
|
-
@kb.add(
|
|
248
|
+
@kb.add("x")
|
|
230
249
|
def _(_event):
|
|
231
|
-
"
|
|
250
|
+
"Send Abort (control-c) signal."
|
|
232
251
|
cancel[0] = True
|
|
233
252
|
os.kill(os.getpid(), signal.SIGINT)
|
|
234
253
|
|
|
@@ -238,34 +257,37 @@ def _(event):
|
|
|
238
257
|
# TODO: hack to simulate progress bar of indeterminate length of an synchronous function
|
|
239
258
|
for i in pb(range(100)):
|
|
240
259
|
if i > 50 and i < 70:
|
|
241
|
-
time.sleep(.01)
|
|
260
|
+
time.sleep(0.01)
|
|
242
261
|
|
|
243
262
|
if i == 60:
|
|
244
|
-
res = func(current_user_input)
|
|
263
|
+
res = func(current_user_input) # hack to see progress bar
|
|
245
264
|
|
|
246
265
|
# Stop when the cancel flag has been set.
|
|
247
266
|
if cancel[0]:
|
|
248
267
|
break
|
|
249
268
|
|
|
250
|
-
with ConsoleStyle(
|
|
269
|
+
with ConsoleStyle("code") as console:
|
|
251
270
|
console.print(res)
|
|
252
271
|
|
|
272
|
+
|
|
253
273
|
@bindings.add(Keys.PageUp)
|
|
254
274
|
def _(event):
|
|
255
275
|
# Moving up for 5 lines
|
|
256
276
|
for _ in range(5):
|
|
257
277
|
event.current_buffer.auto_up()
|
|
258
278
|
|
|
279
|
+
|
|
259
280
|
@bindings.add(Keys.PageDown)
|
|
260
281
|
def _(event):
|
|
261
282
|
# Moving down for 5 lines
|
|
262
283
|
for _ in range(5):
|
|
263
284
|
event.current_buffer.auto_down()
|
|
264
285
|
|
|
286
|
+
|
|
265
287
|
class FileHistory(History):
|
|
266
|
-
|
|
288
|
+
"""
|
|
267
289
|
:class:`.History` class that stores all strings in a file.
|
|
268
|
-
|
|
290
|
+
"""
|
|
269
291
|
|
|
270
292
|
def __init__(self, filename: str) -> None:
|
|
271
293
|
self.filename = Path(filename)
|
|
@@ -297,25 +319,32 @@ class FileHistory(History):
|
|
|
297
319
|
for line in string.split("\n"):
|
|
298
320
|
write(f"{line}\n")
|
|
299
321
|
|
|
322
|
+
|
|
300
323
|
# Defining commands history
|
|
301
|
-
def load_history(home_path=HOME_PATH, history_file=
|
|
324
|
+
def load_history(home_path=HOME_PATH, history_file=".bash_history"):
|
|
302
325
|
history_file_path = home_path / history_file
|
|
303
326
|
history = FileHistory(history_file_path)
|
|
304
327
|
return history, list(history.load_history_strings())
|
|
305
328
|
|
|
329
|
+
|
|
306
330
|
# Function to check if current directory is a git directory
|
|
307
331
|
def get_git_branch():
|
|
308
332
|
try:
|
|
309
|
-
git_process = subprocess.Popen(
|
|
333
|
+
git_process = subprocess.Popen(
|
|
334
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
335
|
+
stdout=subprocess.PIPE,
|
|
336
|
+
stderr=subprocess.PIPE,
|
|
337
|
+
)
|
|
310
338
|
stdout, _stderr = git_process.communicate()
|
|
311
339
|
if git_process.returncode == 0:
|
|
312
|
-
return stdout.strip().decode(
|
|
340
|
+
return stdout.strip().decode("utf-8")
|
|
313
341
|
except FileNotFoundError:
|
|
314
342
|
pass
|
|
315
343
|
return None
|
|
316
344
|
|
|
345
|
+
|
|
317
346
|
def disambiguate(cmds: str) -> tuple[str, int]:
|
|
318
|
-
|
|
347
|
+
"""
|
|
319
348
|
Ok, so, possible options for now:
|
|
320
349
|
1. query | cmd
|
|
321
350
|
2. query | file [file ...]
|
|
@@ -323,14 +352,14 @@ def disambiguate(cmds: str) -> tuple[str, int]:
|
|
|
323
352
|
3. query | cmd | file
|
|
324
353
|
4. query | cmd cmd ...
|
|
325
354
|
5. query | file | cmd
|
|
326
|
-
|
|
327
|
-
has_at_least_one_cmd = any(shutil.which(cmd) is not None for cmd in cmds.split(
|
|
328
|
-
maybe_cmd
|
|
355
|
+
"""
|
|
356
|
+
has_at_least_one_cmd = any(shutil.which(cmd) is not None for cmd in cmds.split(" "))
|
|
357
|
+
maybe_cmd = cmds.split(" ")[0].strip() # get first command
|
|
329
358
|
maybe_files = FileReader.extract_files(cmds)
|
|
330
359
|
# if cmd follows file(s) or file(s) follows cmd throw error as not supported
|
|
331
360
|
if maybe_files is not None and has_at_least_one_cmd:
|
|
332
361
|
msg = (
|
|
333
|
-
|
|
362
|
+
"Cannot disambiguate commands that have both files and commands or multiple commands. Please provide "
|
|
334
363
|
'correct order of commands. Supported are: query | file [file ...] (e.g. "what do these files have in '
|
|
335
364
|
'common?" | file1 [file2 ...]) and query | cmd (e.g. "what flags can I use with rg?" | rg --help)'
|
|
336
365
|
)
|
|
@@ -339,50 +368,51 @@ def disambiguate(cmds: str) -> tuple[str, int]:
|
|
|
339
368
|
if shutil.which(maybe_cmd) is not None:
|
|
340
369
|
cmd_out = subprocess.run(cmds, check=False, capture_output=True, text=True, shell=True)
|
|
341
370
|
if not cmd_out.stdout:
|
|
342
|
-
msg = f
|
|
371
|
+
msg = f"Command not found or failed. Error: {cmd_out.stderr}"
|
|
343
372
|
UserMessage(msg, raise_with=ValueError)
|
|
344
373
|
return cmd_out.stdout, 1
|
|
345
374
|
if maybe_files is not None:
|
|
346
375
|
return maybe_files, 2
|
|
347
376
|
return None
|
|
348
377
|
|
|
378
|
+
|
|
349
379
|
# query language model
|
|
350
380
|
def _starts_with_prefix(query: str, prefix: str) -> bool:
|
|
351
381
|
return (
|
|
352
382
|
query.startswith(f'{prefix}"')
|
|
353
383
|
or query.startswith(f"{prefix}'")
|
|
354
|
-
or query.startswith(f
|
|
384
|
+
or query.startswith(f"{prefix}`")
|
|
355
385
|
)
|
|
356
386
|
|
|
357
387
|
|
|
358
388
|
def _is_new_conversation_query(query: str) -> bool:
|
|
359
|
-
return _starts_with_prefix(query,
|
|
389
|
+
return _starts_with_prefix(query, "!")
|
|
360
390
|
|
|
361
391
|
|
|
362
392
|
def _is_followup_conversation_query(query: str) -> bool:
|
|
363
|
-
return _starts_with_prefix(query,
|
|
393
|
+
return _starts_with_prefix(query, ".")
|
|
364
394
|
|
|
365
395
|
|
|
366
396
|
def _is_stateful_query(query: str) -> bool:
|
|
367
|
-
return any(_starts_with_prefix(query, prefix) for prefix in [
|
|
397
|
+
return any(_starts_with_prefix(query, prefix) for prefix in [".", "!"])
|
|
368
398
|
|
|
369
399
|
|
|
370
400
|
def _extract_query_kwargs(query: str, previous_kwargs, existing_kwargs):
|
|
371
|
-
if
|
|
401
|
+
if "--kwargs" not in query and "-kw" not in query:
|
|
372
402
|
return query, existing_kwargs, previous_kwargs
|
|
373
403
|
|
|
374
|
-
splitter =
|
|
404
|
+
splitter = "--kwargs" if "--kwargs" in query else "-kw"
|
|
375
405
|
splits = query.split(splitter)
|
|
376
406
|
suffix = splits[-1]
|
|
377
|
-
if previous_kwargs is None and
|
|
378
|
-
msg =
|
|
407
|
+
if previous_kwargs is None and "=" not in suffix and "," not in suffix:
|
|
408
|
+
msg = "Kwargs format must be last in query."
|
|
379
409
|
UserMessage(msg, raise_with=ValueError)
|
|
380
|
-
if previous_kwargs is not None and
|
|
410
|
+
if previous_kwargs is not None and "=" not in suffix and "," not in suffix:
|
|
381
411
|
cmd_kwargs = previous_kwargs
|
|
382
412
|
else:
|
|
383
413
|
query = splits[0].strip()
|
|
384
414
|
kwargs_str = suffix.strip()
|
|
385
|
-
cmd_kwargs = dict([kw.split(
|
|
415
|
+
cmd_kwargs = dict([kw.split("=") for kw in kwargs_str.split(",")])
|
|
386
416
|
cmd_kwargs = {k.strip(): Symbol(v.strip()).ast() for k, v in cmd_kwargs.items()}
|
|
387
417
|
|
|
388
418
|
previous_kwargs = cmd_kwargs
|
|
@@ -398,7 +428,7 @@ def _process_new_conversation(query, conversation_cls, symai_path, plugin, previ
|
|
|
398
428
|
if plugin is None:
|
|
399
429
|
return conversation, previous_kwargs, None, False
|
|
400
430
|
with Loader(desc="Inference ...", end=""):
|
|
401
|
-
cmd = query[1:].strip('\
|
|
431
|
+
cmd = query[1:].strip("'\"")
|
|
402
432
|
cmd = f"symrun {plugin} '{cmd}' --disable-pbar"
|
|
403
433
|
cmd_out = run_shell_command(cmd, auto_query_on_error=True)
|
|
404
434
|
conversation.store(cmd_out)
|
|
@@ -408,18 +438,20 @@ def _process_new_conversation(query, conversation_cls, symai_path, plugin, previ
|
|
|
408
438
|
return conversation, previous_kwargs, cmd_out, True
|
|
409
439
|
|
|
410
440
|
|
|
411
|
-
def _process_followup_conversation(
|
|
441
|
+
def _process_followup_conversation(
|
|
442
|
+
query, conversation, conversation_cls, symai_path, plugin, previous_kwargs, state
|
|
443
|
+
):
|
|
412
444
|
try:
|
|
413
445
|
conversation = conversation.load_conversation_state(symai_path)
|
|
414
446
|
state.stateful_conversation = conversation
|
|
415
447
|
except Exception:
|
|
416
|
-
with ConsoleStyle(
|
|
417
|
-
console.print(
|
|
448
|
+
with ConsoleStyle("error") as console:
|
|
449
|
+
console.print("No conversation state found. Please start a new conversation.")
|
|
418
450
|
return conversation, previous_kwargs, None, True
|
|
419
451
|
if plugin is None:
|
|
420
452
|
return conversation, previous_kwargs, None, False
|
|
421
453
|
with Loader(desc="Inference ...", end=""):
|
|
422
|
-
trimmed_query = query[1:].strip('\
|
|
454
|
+
trimmed_query = query[1:].strip("'\"")
|
|
423
455
|
answer = conversation(trimmed_query).value
|
|
424
456
|
cmd = f"symrun {plugin} '{answer}' --disable-pbar"
|
|
425
457
|
cmd_out = run_shell_command(cmd, auto_query_on_error=True)
|
|
@@ -431,10 +463,10 @@ def _process_followup_conversation(query, conversation, conversation_cls, symai_
|
|
|
431
463
|
|
|
432
464
|
|
|
433
465
|
def _handle_piped_query(query, conversation, state):
|
|
434
|
-
cmds = query.split(
|
|
466
|
+
cmds = query.split("|")
|
|
435
467
|
if len(cmds) > 2:
|
|
436
468
|
msg = (
|
|
437
|
-
|
|
469
|
+
"Cannot disambiguate commands that have more than 1 pipes. Please provide correct order of commands. "
|
|
438
470
|
'Supported are: query | file [file ...] (e.g. "what do these files have in common?" | file1 [file2 ...]) '
|
|
439
471
|
'and query | cmd (e.g. "what flags can I use with rg?" | rg --help)'
|
|
440
472
|
)
|
|
@@ -460,7 +492,7 @@ def _handle_piped_query(query, conversation, state):
|
|
|
460
492
|
|
|
461
493
|
|
|
462
494
|
def _select_function_for_query(query, conversation, state):
|
|
463
|
-
if
|
|
495
|
+
if "|" in query:
|
|
464
496
|
return _handle_piped_query(query, conversation, state)
|
|
465
497
|
if _is_stateful_query(query):
|
|
466
498
|
return conversation, query
|
|
@@ -479,8 +511,8 @@ def query_language_model(query: str, res=None, *args, **kwargs):
|
|
|
479
511
|
previous_kwargs = state.previous_kwargs
|
|
480
512
|
conversation_cls = state.conversation_type
|
|
481
513
|
home_path = HOME_PATH
|
|
482
|
-
symai_path = home_path /
|
|
483
|
-
plugin = SYMSH_CONFIG.get(
|
|
514
|
+
symai_path = home_path / ".conversation_state"
|
|
515
|
+
plugin = SYMSH_CONFIG.get("plugin_prefix")
|
|
484
516
|
|
|
485
517
|
query, kwargs, previous_kwargs = _extract_query_kwargs(query, previous_kwargs, kwargs)
|
|
486
518
|
|
|
@@ -512,7 +544,8 @@ def query_language_model(query: str, res=None, *args, **kwargs):
|
|
|
512
544
|
state.previous_kwargs = previous_kwargs
|
|
513
545
|
return msg
|
|
514
546
|
|
|
515
|
-
|
|
547
|
+
|
|
548
|
+
def retrieval_augmented_indexing(query: str, index_name=None, *_args, **_kwargs):
|
|
516
549
|
state = _shell_state
|
|
517
550
|
sep = os.path.sep
|
|
518
551
|
path = query
|
|
@@ -520,35 +553,35 @@ def retrieval_augmented_indexing(query: str, index_name = None, *_args, **_kwarg
|
|
|
520
553
|
|
|
521
554
|
# check if path contains overwrite flag
|
|
522
555
|
overwrite = False
|
|
523
|
-
if path.startswith(
|
|
556
|
+
if path.startswith("!"):
|
|
524
557
|
overwrite = True
|
|
525
558
|
path = path[1:]
|
|
526
559
|
|
|
527
560
|
# check if request use of specific index
|
|
528
|
-
use_index_name
|
|
529
|
-
if path.startswith(
|
|
561
|
+
use_index_name = False
|
|
562
|
+
if path.startswith("index:"):
|
|
530
563
|
use_index_name = True
|
|
531
564
|
# continue conversation with specific index
|
|
532
|
-
index_name
|
|
565
|
+
index_name = path.split("index:")[-1].strip()
|
|
533
566
|
else:
|
|
534
567
|
parse_arxiv = False
|
|
535
568
|
|
|
536
569
|
# check if path contains arxiv flag
|
|
537
|
-
if path.startswith(
|
|
570
|
+
if path.startswith("arxiv:"):
|
|
538
571
|
parse_arxiv = True
|
|
539
572
|
|
|
540
573
|
# check if path contains git flag
|
|
541
|
-
if path.startswith(
|
|
574
|
+
if path.startswith("git@"):
|
|
542
575
|
overwrite = True
|
|
543
|
-
repo_path = home_path /
|
|
576
|
+
repo_path = home_path / "temp"
|
|
544
577
|
with Loader(desc="Cloning repo ...", end=""):
|
|
545
578
|
cloner = RepositoryCloner(repo_path=str(repo_path))
|
|
546
579
|
url = path[4:]
|
|
547
|
-
if
|
|
548
|
-
url =
|
|
549
|
-
url = url.replace(
|
|
580
|
+
if "http" not in url:
|
|
581
|
+
url = "https://" + url
|
|
582
|
+
url = url.replace(".com:", ".com/")
|
|
550
583
|
# if ends with '.git' then remove it
|
|
551
|
-
if url.endswith(
|
|
584
|
+
if url.endswith(".git"):
|
|
552
585
|
url = url[:-4]
|
|
553
586
|
path = cloner(url)
|
|
554
587
|
|
|
@@ -560,79 +593,90 @@ def retrieval_augmented_indexing(query: str, index_name = None, *_args, **_kwarg
|
|
|
560
593
|
arxiv = ArxivPdfParser()
|
|
561
594
|
pdf_file = arxiv(file)
|
|
562
595
|
if pdf_file is not None:
|
|
563
|
-
file = file |
|
|
596
|
+
file = file | "\n" | pdf_file
|
|
564
597
|
|
|
565
598
|
index_name = path.split(sep)[-1] if index_name is None else index_name
|
|
566
599
|
index_name = Indexer.replace_special_chars(index_name)
|
|
567
|
-
UserMessage(f
|
|
600
|
+
UserMessage(f"Indexing {index_name} ...", style="extensity")
|
|
568
601
|
|
|
569
602
|
# creates index if not exists
|
|
570
603
|
DocumentRetriever(index_name=index_name, file=file, overwrite=overwrite)
|
|
571
604
|
|
|
572
|
-
symai_path = home_path /
|
|
605
|
+
symai_path = home_path / ".conversation_state"
|
|
573
606
|
symai_path.parent.mkdir(parents=True, exist_ok=True)
|
|
574
|
-
stateful_conversation = state.retrieval_conversation_type(
|
|
607
|
+
stateful_conversation = state.retrieval_conversation_type(
|
|
608
|
+
auto_print=False, index_name=index_name
|
|
609
|
+
)
|
|
575
610
|
state.stateful_conversation = stateful_conversation
|
|
576
611
|
Conversation.save_conversation_state(stateful_conversation, symai_path)
|
|
577
612
|
if use_index_name:
|
|
578
|
-
message =
|
|
613
|
+
message = "New session "
|
|
579
614
|
else:
|
|
580
|
-
message =
|
|
581
|
-
|
|
615
|
+
message = (
|
|
616
|
+
f"Repository {url} cloned and "
|
|
617
|
+
if query.startswith("git@") or query.startswith("git:")
|
|
618
|
+
else f"Directory {path} "
|
|
619
|
+
)
|
|
620
|
+
return f"{message}successfully indexed: {index_name}"
|
|
621
|
+
|
|
582
622
|
|
|
583
623
|
def search_engine(query: str, res=None, *_args, **_kwargs):
|
|
584
|
-
search = Interface(
|
|
624
|
+
search = Interface("serpapi")
|
|
585
625
|
with Loader(desc="Searching ...", end=""):
|
|
586
|
-
search_query = Symbol(query).extract(
|
|
626
|
+
search_query = Symbol(query).extract("search engine optimized query")
|
|
587
627
|
res = search(search_query)
|
|
588
628
|
with Loader(desc="Inference ...", end=""):
|
|
589
629
|
func = _shell_state.function_type(query)
|
|
590
630
|
msg = func(res, payload=res)
|
|
591
631
|
# write a temp dump file with the query and results
|
|
592
632
|
home_path = HOME_PATH
|
|
593
|
-
symai_path = home_path /
|
|
633
|
+
symai_path = home_path / ".search_dump"
|
|
594
634
|
symai_path.parent.mkdir(parents=True, exist_ok=True)
|
|
595
|
-
with symai_path.open(
|
|
596
|
-
f.write(f
|
|
635
|
+
with symai_path.open("w") as f:
|
|
636
|
+
f.write(f"[SEARCH_QUERY]:\n{search_query}\n[RESULTS]\n{res}\n[MESSAGE]\n{msg}")
|
|
597
637
|
return msg
|
|
598
638
|
|
|
639
|
+
|
|
599
640
|
def set_default_module(cmd: str):
|
|
600
|
-
if cmd.startswith(
|
|
601
|
-
module = cmd.split(
|
|
602
|
-
SYMSH_CONFIG[
|
|
603
|
-
with config_path.open(
|
|
641
|
+
if cmd.startswith("set-plugin"):
|
|
642
|
+
module = cmd.split("set-plugin")[-1].strip()
|
|
643
|
+
SYMSH_CONFIG["plugin_prefix"] = module
|
|
644
|
+
with config_path.open("w") as f:
|
|
604
645
|
json.dump(SYMSH_CONFIG, f, indent=4)
|
|
605
646
|
msg = f"Default plugin set to '{module}'"
|
|
606
|
-
elif cmd ==
|
|
607
|
-
SYMSH_CONFIG[
|
|
608
|
-
with config_path.open(
|
|
647
|
+
elif cmd == "unset-plugin":
|
|
648
|
+
SYMSH_CONFIG["plugin_prefix"] = None
|
|
649
|
+
with config_path.open("w") as f:
|
|
609
650
|
json.dump(SYMSH_CONFIG, f, indent=4)
|
|
610
651
|
msg = "Default plugin unset"
|
|
611
|
-
elif cmd ==
|
|
652
|
+
elif cmd == "get-plugin":
|
|
612
653
|
msg = f"Default plugin is '{SYMSH_CONFIG['plugin_prefix']}'"
|
|
613
654
|
|
|
614
|
-
with ConsoleStyle(
|
|
655
|
+
with ConsoleStyle("success") as console:
|
|
615
656
|
console.print(msg)
|
|
616
657
|
|
|
658
|
+
|
|
617
659
|
def handle_error(cmd, res, message, auto_query_on_error):
|
|
618
|
-
msg = Symbol(cmd) | f
|
|
619
|
-
if
|
|
620
|
-
|
|
660
|
+
msg = Symbol(cmd) | f"\n{res!s}"
|
|
661
|
+
if "command not found" in str(
|
|
662
|
+
res
|
|
663
|
+
) or "not recognized as an internal or external command" in str(res):
|
|
664
|
+
return res.stderr.decode("utf-8")
|
|
621
665
|
stderr = res.stderr
|
|
622
666
|
if stderr and auto_query_on_error:
|
|
623
|
-
rsp = stderr.decode(
|
|
667
|
+
rsp = stderr.decode("utf-8")
|
|
624
668
|
UserMessage(rsp, style="alert")
|
|
625
669
|
msg = msg | f"\n{rsp}"
|
|
626
|
-
if
|
|
670
|
+
if "usage:" in rsp:
|
|
627
671
|
try:
|
|
628
|
-
cmd = cmd.split(
|
|
672
|
+
cmd = cmd.split("usage: ")[-1].split(" ")[0]
|
|
629
673
|
# get man page result for command
|
|
630
|
-
res = subprocess.run(
|
|
631
|
-
|
|
632
|
-
|
|
674
|
+
res = subprocess.run(
|
|
675
|
+
f"man -P cat {cmd}", check=False, shell=True, stdout=subprocess.PIPE
|
|
676
|
+
)
|
|
633
677
|
stdout = res.stdout
|
|
634
678
|
if stdout:
|
|
635
|
-
rsp = stdout.decode(
|
|
679
|
+
rsp = stdout.decode("utf-8")[:500]
|
|
636
680
|
msg = msg | f"\n{rsp}"
|
|
637
681
|
except Exception:
|
|
638
682
|
pass
|
|
@@ -640,29 +684,34 @@ def handle_error(cmd, res, message, auto_query_on_error):
|
|
|
640
684
|
return query_language_model(msg)
|
|
641
685
|
stdout = res.stdout
|
|
642
686
|
if stdout:
|
|
643
|
-
message = stderr.decode(
|
|
687
|
+
message = stderr.decode("utf-8")
|
|
644
688
|
return message
|
|
645
689
|
|
|
690
|
+
|
|
646
691
|
# run shell command
|
|
647
|
-
def run_shell_command(
|
|
692
|
+
def run_shell_command(
|
|
693
|
+
cmd: str, prev=None, auto_query_on_error: bool = False, stdout=None, stderr=None
|
|
694
|
+
):
|
|
648
695
|
if prev is not None:
|
|
649
|
-
cmd = prev +
|
|
696
|
+
cmd = prev + " && " + cmd
|
|
650
697
|
message = None
|
|
651
698
|
conda_env = get_exec_prefix()
|
|
652
699
|
# copy default_env
|
|
653
700
|
new_env = default_env.copy()
|
|
654
|
-
if _shell_state.exec_prefix !=
|
|
701
|
+
if _shell_state.exec_prefix != "default":
|
|
655
702
|
# remove current env from PATH
|
|
656
703
|
new_env["PATH"] = new_env["PATH"].replace(sys.exec_prefix, conda_env)
|
|
657
704
|
# Execute the command
|
|
658
705
|
try:
|
|
659
706
|
stdout = subprocess.PIPE if auto_query_on_error else stdout
|
|
660
707
|
stderr = subprocess.PIPE if auto_query_on_error else stderr
|
|
661
|
-
res = subprocess.run(
|
|
708
|
+
res = subprocess.run(
|
|
709
|
+
cmd, check=False, shell=True, stdout=stdout, stderr=stderr, env=new_env
|
|
710
|
+
)
|
|
662
711
|
if res and stdout and res.stdout:
|
|
663
|
-
message = res.stdout.decode(
|
|
712
|
+
message = res.stdout.decode("utf-8")
|
|
664
713
|
elif res and stderr and res.stderr:
|
|
665
|
-
message = res.stderr.decode(
|
|
714
|
+
message = res.stderr.decode("utf-8")
|
|
666
715
|
except FileNotFoundError as e:
|
|
667
716
|
return e
|
|
668
717
|
except PermissionError as e:
|
|
@@ -674,46 +723,59 @@ def run_shell_command(cmd: str, prev=None, auto_query_on_error: bool=False, stdo
|
|
|
674
723
|
# If command not found, then try to query language model
|
|
675
724
|
return handle_error(cmd, res, message, auto_query_on_error)
|
|
676
725
|
|
|
726
|
+
|
|
677
727
|
def is_llm_request(cmd: str):
|
|
678
|
-
return
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
728
|
+
return (
|
|
729
|
+
cmd.startswith('"')
|
|
730
|
+
or cmd.startswith('."')
|
|
731
|
+
or cmd.startswith('!"')
|
|
732
|
+
or cmd.startswith('?"')
|
|
733
|
+
or cmd.startswith("'")
|
|
734
|
+
or cmd.startswith(".'")
|
|
735
|
+
or cmd.startswith("!'")
|
|
736
|
+
or cmd.startswith("?'")
|
|
737
|
+
or cmd.startswith("`")
|
|
738
|
+
or cmd.startswith(".`")
|
|
739
|
+
or cmd.startswith("!`")
|
|
740
|
+
or cmd.startswith("?`")
|
|
741
|
+
or cmd.startswith("!(")
|
|
742
|
+
)
|
|
743
|
+
|
|
682
744
|
|
|
683
745
|
def map_nt_cmd(cmd: str, map_nt_cmd_enabled: bool = True):
|
|
684
|
-
if os.name.lower() ==
|
|
746
|
+
if os.name.lower() == "nt" and map_nt_cmd_enabled and not is_llm_request(cmd):
|
|
685
747
|
# Mapping command replacements with regex for commands with variants
|
|
686
748
|
cmd_mappings = {
|
|
687
|
-
r
|
|
688
|
-
r
|
|
689
|
-
r
|
|
690
|
-
r
|
|
691
|
-
r
|
|
692
|
-
r
|
|
693
|
-
r
|
|
694
|
-
r
|
|
695
|
-
r
|
|
696
|
-
r
|
|
697
|
-
r
|
|
698
|
-
r
|
|
699
|
-
r
|
|
700
|
-
r
|
|
701
|
-
r
|
|
702
|
-
r
|
|
703
|
-
r
|
|
704
|
-
r
|
|
705
|
-
r
|
|
706
|
-
r
|
|
707
|
-
r
|
|
708
|
-
r
|
|
709
|
-
r
|
|
749
|
+
r"\bls\b(-[a-zA-Z]*)?": r"dir \1", # Maps 'ls' with or without arguments
|
|
750
|
+
r"\bmv\b\s+(.*)": r"move \1", # Maps 'mv' with any arguments
|
|
751
|
+
r"\bcp\b\s+(.*)": r"copy \1", # Maps 'cp' with any arguments
|
|
752
|
+
r"\btouch\b\s+(.*)": r"type nul > \1", # Maps 'touch filename' to 'type nul > filename'
|
|
753
|
+
r"\brm\b\s+(-rf)?": r"del \1", # Maps 'rm' and 'rm -rf'
|
|
754
|
+
r"\bdiff\b\s+(.*)": r"fc \1", # Maps 'diff' with any arguments
|
|
755
|
+
r"\bgrep\b\s+(.*)": r"find \1", # Maps 'grep' with any arguments
|
|
756
|
+
r"\bpwd\b": "chdir", # pwd has no arguments
|
|
757
|
+
r"\bdate\b": "time", # date has no arguments
|
|
758
|
+
r"\bmkdir\b\s+(.*)": r"md \1", # Maps 'mkdir' with any arguments
|
|
759
|
+
r"\bwhich\b\s+(.*)": r"where \1", # Maps 'which' with any arguments
|
|
760
|
+
r"\b(vim|nano)\b\s+(.*)": r"notepad \2", # Maps 'vim' or 'nano' with any arguments
|
|
761
|
+
r"\b(mke2fs|mformat)\b\s+(.*)": r"format \2", # Maps 'mke2fs' or 'mformat' with any arguments
|
|
762
|
+
r"\b(rm\s+-rf|rmdir)\b": "rmdir /s /q", # Matches 'rm -rf' or 'rmdir'
|
|
763
|
+
r"\bkill\b\s+(.*)": r"taskkill \1", # Maps 'kill' with any arguments
|
|
764
|
+
r"\bps\b\s*(.*)?": r"tasklist \1", # Maps 'ps' with any or no arguments
|
|
765
|
+
r"\bexport\b\s+(.*)": r"set \1", # Maps 'export' with any arguments
|
|
766
|
+
r"\b(chown|chmod)\b\s+(.*)": r"attrib +r \2", # Maps 'chown' or 'chmod' with any arguments
|
|
767
|
+
r"\btraceroute\b\s+(.*)": r"tracert \1", # Maps 'traceroute' with any arguments
|
|
768
|
+
r"\bcron\b\s+(.*)": r"at \1", # Maps 'cron' with any arguments
|
|
769
|
+
r"\bcat\b\s+(.*)": r"type \1", # Maps 'cat' with any arguments
|
|
770
|
+
r"\bdu\s+-s\b": "chkdsk", # du -s has no arguments, chkdsk is closest in functionality
|
|
771
|
+
r"\bls\s+-R\b": "tree", # ls -R has no arguments
|
|
710
772
|
}
|
|
711
773
|
|
|
712
774
|
# Remove 1:1 mappings
|
|
713
775
|
direct_mappings = {
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
776
|
+
"clear": "cls",
|
|
777
|
+
"man": "help",
|
|
778
|
+
"mem": "free",
|
|
717
779
|
}
|
|
718
780
|
|
|
719
781
|
cmd_mappings.update(direct_mappings)
|
|
@@ -724,93 +786,95 @@ def map_nt_cmd(cmd: str, map_nt_cmd_enabled: bool = True):
|
|
|
724
786
|
original_cmd = cmd
|
|
725
787
|
cmd = re.sub(linux_cmd, windows_cmd, cmd)
|
|
726
788
|
if cmd != original_cmd:
|
|
727
|
-
UserMessage(
|
|
789
|
+
UserMessage(
|
|
790
|
+
f'symsh >> command "{original_cmd}" mapped to "{cmd}"\n', style="extensity"
|
|
791
|
+
)
|
|
728
792
|
|
|
729
793
|
return cmd
|
|
730
794
|
|
|
731
795
|
|
|
732
796
|
def _handle_plugin_commands(cmd: str):
|
|
733
|
-
if cmd.startswith(
|
|
797
|
+
if cmd.startswith("set-plugin") or cmd == "unset-plugin" or cmd == "get-plugin":
|
|
734
798
|
return set_default_module(cmd)
|
|
735
799
|
return None
|
|
736
800
|
|
|
737
801
|
|
|
738
802
|
def _handle_chained_llm_commands(cmd: str, res, auto_query_on_error: bool):
|
|
739
|
-
if '" && ' not in cmd and "' && " not in cmd and
|
|
803
|
+
if '" && ' not in cmd and "' && " not in cmd and "` && " not in cmd:
|
|
740
804
|
return None
|
|
741
805
|
if not is_llm_request(cmd):
|
|
742
806
|
return run_shell_command(cmd, prev=res, auto_query_on_error=auto_query_on_error)
|
|
743
|
-
cmds = cmd.split(
|
|
807
|
+
cmds = cmd.split(" && ")
|
|
744
808
|
if not is_llm_request(cmds[0]):
|
|
745
|
-
return ValueError(
|
|
809
|
+
return ValueError("The first command must be a LLM request.")
|
|
746
810
|
first_res = query_language_model(cmds[0], res=res)
|
|
747
|
-
rest =
|
|
748
|
-
if len(cmds) > 1 and
|
|
749
|
-
first_res_str = str(first_res).replace(
|
|
750
|
-
rest = rest.replace(
|
|
811
|
+
rest = " && ".join(cmds[1:])
|
|
812
|
+
if len(cmds) > 1 and "$1" in cmds[1]:
|
|
813
|
+
first_res_str = str(first_res).replace("\n", r"\\n")
|
|
814
|
+
rest = rest.replace("$1", f'"{first_res_str}"')
|
|
751
815
|
first_res = None
|
|
752
816
|
return run_shell_command(rest, prev=first_res, auto_query_on_error=auto_query_on_error)
|
|
753
817
|
|
|
754
818
|
|
|
755
819
|
def _handle_llm_or_search(cmd: str, res):
|
|
756
|
-
if cmd.startswith('?"') or cmd.startswith("?'") or cmd.startswith(
|
|
820
|
+
if cmd.startswith('?"') or cmd.startswith("?'") or cmd.startswith("?`"):
|
|
757
821
|
query = cmd[1:]
|
|
758
822
|
return search_engine(query, res=res)
|
|
759
|
-
if is_llm_request(cmd) or
|
|
823
|
+
if is_llm_request(cmd) or "..." in cmd:
|
|
760
824
|
return query_language_model(cmd, res=res)
|
|
761
825
|
return None
|
|
762
826
|
|
|
763
827
|
|
|
764
828
|
def _handle_retrieval_commands(cmd: str):
|
|
765
|
-
if cmd.startswith(
|
|
829
|
+
if cmd.startswith("*"):
|
|
766
830
|
return retrieval_augmented_indexing(cmd[1:])
|
|
767
831
|
return None
|
|
768
832
|
|
|
769
833
|
|
|
770
834
|
def _handle_man_command(cmd: str):
|
|
771
|
-
if cmd.startswith(
|
|
835
|
+
if cmd.startswith("man symsh"):
|
|
772
836
|
pkg_path = Path(__file__).resolve().parent
|
|
773
|
-
symsh_path = pkg_path /
|
|
837
|
+
symsh_path = pkg_path / "symsh.md"
|
|
774
838
|
with symsh_path.open(encoding="utf8") as file_ptr:
|
|
775
839
|
return file_ptr.read()
|
|
776
840
|
return None
|
|
777
841
|
|
|
778
842
|
|
|
779
843
|
def _handle_conda_commands(cmd: str, state, res, auto_query_on_error: bool):
|
|
780
|
-
if cmd.startswith(
|
|
844
|
+
if cmd.startswith("conda activate"):
|
|
781
845
|
env = Path(sys.exec_prefix)
|
|
782
846
|
env_base = env.parent
|
|
783
|
-
req_env = cmd.split(
|
|
847
|
+
req_env = cmd.split(" ")[2]
|
|
784
848
|
env_path = env_base / req_env
|
|
785
849
|
if not env_path.exists():
|
|
786
|
-
return f
|
|
850
|
+
return f"Environment {req_env} does not exist!"
|
|
787
851
|
state.previous_prefix = state.exec_prefix
|
|
788
852
|
state.exec_prefix = str(env_path)
|
|
789
853
|
return state.exec_prefix
|
|
790
|
-
if cmd.startswith(
|
|
854
|
+
if cmd.startswith("conda deactivate"):
|
|
791
855
|
prev_prefix = state.previous_prefix
|
|
792
856
|
if prev_prefix is not None:
|
|
793
857
|
state.exec_prefix = prev_prefix
|
|
794
|
-
if prev_prefix ==
|
|
858
|
+
if prev_prefix == "default":
|
|
795
859
|
state.previous_prefix = None
|
|
796
860
|
return get_exec_prefix()
|
|
797
|
-
if cmd.startswith(
|
|
861
|
+
if cmd.startswith("conda"):
|
|
798
862
|
env = Path(get_exec_prefix())
|
|
799
863
|
try:
|
|
800
864
|
env_base = env.parents[1]
|
|
801
865
|
except IndexError:
|
|
802
866
|
env_base = env.parent
|
|
803
|
-
cmd_rewritten = cmd.replace(
|
|
867
|
+
cmd_rewritten = cmd.replace("conda", str(env_base / "condabin" / "conda"))
|
|
804
868
|
return run_shell_command(cmd_rewritten, prev=res, auto_query_on_error=auto_query_on_error)
|
|
805
869
|
return None
|
|
806
870
|
|
|
807
871
|
|
|
808
872
|
def _handle_directory_navigation(cmd: str):
|
|
809
873
|
sep = os.path.sep
|
|
810
|
-
if cmd.startswith(
|
|
874
|
+
if cmd.startswith("cd"):
|
|
811
875
|
try:
|
|
812
876
|
cmd_expanded = FileReader.expand_user_path(cmd)
|
|
813
|
-
path =
|
|
877
|
+
path = " ".join(cmd_expanded.split(" ")[1:])
|
|
814
878
|
if path.endswith(sep):
|
|
815
879
|
path = path[:-1]
|
|
816
880
|
return os.chdir(path)
|
|
@@ -830,16 +894,16 @@ def _handle_directory_navigation(cmd: str):
|
|
|
830
894
|
|
|
831
895
|
|
|
832
896
|
def _handle_ll_alias(cmd: str, res):
|
|
833
|
-
if not cmd.startswith(
|
|
897
|
+
if not cmd.startswith("ll"):
|
|
834
898
|
return None
|
|
835
|
-
if os.name ==
|
|
836
|
-
rewritten = cmd.replace(
|
|
899
|
+
if os.name == "nt":
|
|
900
|
+
rewritten = cmd.replace("ll", "dir")
|
|
837
901
|
return run_shell_command(rewritten, prev=res)
|
|
838
|
-
rewritten = cmd.replace(
|
|
902
|
+
rewritten = cmd.replace("ll", "ls -la")
|
|
839
903
|
return run_shell_command(rewritten, prev=res)
|
|
840
904
|
|
|
841
905
|
|
|
842
|
-
def process_command(cmd: str, res=None, auto_query_on_error: bool=False):
|
|
906
|
+
def process_command(cmd: str, res=None, auto_query_on_error: bool = False):
|
|
843
907
|
state = _shell_state
|
|
844
908
|
|
|
845
909
|
# map commands to windows if needed
|
|
@@ -878,14 +942,15 @@ def process_command(cmd: str, res=None, auto_query_on_error: bool=False):
|
|
|
878
942
|
|
|
879
943
|
return run_shell_command(cmd, prev=res, auto_query_on_error=auto_query_on_error)
|
|
880
944
|
|
|
945
|
+
|
|
881
946
|
def save_conversation():
|
|
882
947
|
home_path = HOME_PATH
|
|
883
|
-
symai_path = home_path /
|
|
948
|
+
symai_path = home_path / ".conversation_state"
|
|
884
949
|
Conversation.save_conversation_state(_shell_state.stateful_conversation, symai_path)
|
|
885
950
|
|
|
886
951
|
|
|
887
952
|
def _is_exit_command(cmd: str) -> bool:
|
|
888
|
-
return cmd in [
|
|
953
|
+
return cmd in ["quit", "exit", "q"]
|
|
889
954
|
|
|
890
955
|
|
|
891
956
|
def _format_working_directory():
|
|
@@ -898,8 +963,8 @@ def _format_working_directory():
|
|
|
898
963
|
prev_paths = sep.join(paths[:-1])
|
|
899
964
|
last_path = paths[-1]
|
|
900
965
|
if len(paths) > 1:
|
|
901
|
-
return f
|
|
902
|
-
return f
|
|
966
|
+
return f"{prev_paths}{sep}<b>{last_path}</b>"
|
|
967
|
+
return f"<b>{last_path}</b>"
|
|
903
968
|
|
|
904
969
|
|
|
905
970
|
def _build_prompt(git_branch, conda_env, cur_working_dir_str):
|
|
@@ -919,15 +984,20 @@ def _handle_exit(state):
|
|
|
919
984
|
if state.stateful_conversation is not None:
|
|
920
985
|
save_conversation()
|
|
921
986
|
if not state.use_styles:
|
|
922
|
-
UserMessage(
|
|
987
|
+
UserMessage("Goodbye!", style="extensity")
|
|
923
988
|
else:
|
|
924
|
-
func = _shell_state.function_type(
|
|
925
|
-
UserMessage(func(
|
|
989
|
+
func = _shell_state.function_type("Give short goodbye")
|
|
990
|
+
UserMessage(func("bye"), style="extensity")
|
|
926
991
|
os._exit(0)
|
|
927
992
|
|
|
928
993
|
|
|
929
994
|
# Function to listen for user input and execute commands
|
|
930
|
-
def listen(
|
|
995
|
+
def listen(
|
|
996
|
+
session: PromptSession,
|
|
997
|
+
word_comp: WordCompleter,
|
|
998
|
+
auto_query_on_error: bool = False,
|
|
999
|
+
verbose: bool = False,
|
|
1000
|
+
):
|
|
931
1001
|
state = _shell_state
|
|
932
1002
|
with patch_stdout():
|
|
933
1003
|
while True:
|
|
@@ -937,39 +1007,43 @@ def listen(session: PromptSession, word_comp: WordCompleter, auto_query_on_error
|
|
|
937
1007
|
cur_working_dir_str = _format_working_directory()
|
|
938
1008
|
prompt = _build_prompt(git_branch, conda_env, cur_working_dir_str)
|
|
939
1009
|
cmd = session.prompt(prompt)
|
|
940
|
-
if cmd.strip() ==
|
|
1010
|
+
if cmd.strip() == "":
|
|
941
1011
|
continue
|
|
942
1012
|
|
|
943
1013
|
if _is_exit_command(cmd):
|
|
944
1014
|
_handle_exit(state)
|
|
945
1015
|
msg = process_command(cmd, auto_query_on_error=auto_query_on_error)
|
|
946
1016
|
if msg is not None:
|
|
947
|
-
with ConsoleStyle(
|
|
1017
|
+
with ConsoleStyle("code") as console:
|
|
948
1018
|
console.print(msg)
|
|
949
1019
|
|
|
950
1020
|
# Append the command to the word completer list
|
|
951
1021
|
word_comp.words.append(cmd)
|
|
952
1022
|
|
|
953
1023
|
except KeyboardInterrupt:
|
|
954
|
-
UserMessage(
|
|
1024
|
+
UserMessage("", style="alert")
|
|
955
1025
|
except Exception as e:
|
|
956
1026
|
UserMessage(str(e), style="alert")
|
|
957
1027
|
if verbose:
|
|
958
1028
|
traceback.print_exc()
|
|
959
1029
|
|
|
1030
|
+
|
|
960
1031
|
def create_session(history, merged_completer):
|
|
961
|
-
colors = SYMSH_CONFIG[
|
|
1032
|
+
colors = SYMSH_CONFIG["colors"]
|
|
962
1033
|
|
|
963
1034
|
# Load style
|
|
964
1035
|
style = Style.from_dict(colors)
|
|
965
1036
|
|
|
966
1037
|
# Session for the auto-completion
|
|
967
|
-
return PromptSession(
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1038
|
+
return PromptSession(
|
|
1039
|
+
history=history,
|
|
1040
|
+
completer=merged_completer,
|
|
1041
|
+
complete_style=CompleteStyle.MULTI_COLUMN,
|
|
1042
|
+
reserve_space_for_menu=5,
|
|
1043
|
+
style=style,
|
|
1044
|
+
key_bindings=bindings,
|
|
1045
|
+
)
|
|
1046
|
+
|
|
973
1047
|
|
|
974
1048
|
def create_completer():
|
|
975
1049
|
# Load history
|
|
@@ -983,10 +1057,11 @@ def create_completer():
|
|
|
983
1057
|
merged_completer = MergedCompleter(custom_completer, word_comp)
|
|
984
1058
|
return history, word_comp, merged_completer
|
|
985
1059
|
|
|
1060
|
+
|
|
986
1061
|
def run(auto_query_on_error=False, conversation_style=None, verbose=False):
|
|
987
1062
|
state = _shell_state
|
|
988
|
-
if conversation_style is not None and conversation_style !=
|
|
989
|
-
UserMessage(f
|
|
1063
|
+
if conversation_style is not None and conversation_style != "":
|
|
1064
|
+
UserMessage(f"Loading style: {conversation_style}", style="extensity")
|
|
990
1065
|
styles_ = Import.load_module_class(conversation_style)
|
|
991
1066
|
(
|
|
992
1067
|
state.function_type,
|
|
@@ -995,24 +1070,33 @@ def run(auto_query_on_error=False, conversation_style=None, verbose=False):
|
|
|
995
1070
|
) = styles_
|
|
996
1071
|
state.use_styles = True
|
|
997
1072
|
|
|
998
|
-
if SYMSH_CONFIG[
|
|
1073
|
+
if SYMSH_CONFIG["show-splash-screen"]:
|
|
999
1074
|
show_intro_menu()
|
|
1000
1075
|
# set show splash screen to false
|
|
1001
|
-
SYMSH_CONFIG[
|
|
1076
|
+
SYMSH_CONFIG["show-splash-screen"] = False
|
|
1002
1077
|
# save config
|
|
1003
|
-
_config_path = HOME_PATH /
|
|
1004
|
-
with _config_path.open(
|
|
1078
|
+
_config_path = HOME_PATH / "symsh.config.json"
|
|
1079
|
+
with _config_path.open("w") as f:
|
|
1005
1080
|
json.dump(SYMSH_CONFIG, f, indent=4)
|
|
1006
|
-
if
|
|
1007
|
-
SYMSH_CONFIG[
|
|
1081
|
+
if "plugin_prefix" not in SYMSH_CONFIG:
|
|
1082
|
+
SYMSH_CONFIG["plugin_prefix"] = None
|
|
1008
1083
|
|
|
1009
1084
|
history, word_comp, merged_completer = create_completer()
|
|
1010
1085
|
session = create_session(history, merged_completer)
|
|
1011
1086
|
listen(session, word_comp, auto_query_on_error=auto_query_on_error, verbose=verbose)
|
|
1012
1087
|
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
parser
|
|
1016
|
-
parser.add_argument(
|
|
1088
|
+
|
|
1089
|
+
if __name__ == "__main__":
|
|
1090
|
+
parser = argparse.ArgumentParser(description="SymSH: Symbolic Shell")
|
|
1091
|
+
parser.add_argument(
|
|
1092
|
+
"--auto-query-on-error",
|
|
1093
|
+
action="store_true",
|
|
1094
|
+
help="Automatically query the language model on error.",
|
|
1095
|
+
)
|
|
1096
|
+
parser.add_argument("--verbose", action="store_true", help="Print verbose errors.")
|
|
1017
1097
|
args = parser.parse_args()
|
|
1018
|
-
run(
|
|
1098
|
+
run(
|
|
1099
|
+
auto_query_on_error=args.auto_query_on_error,
|
|
1100
|
+
conversation_style=args.conversation_style,
|
|
1101
|
+
verbose=args.verbose,
|
|
1102
|
+
)
|