From 258a639bfb3a2959808bcc46019ee52724e44e06 Mon Sep 17 00:00:00 2001 From: Piotr Czapla Date: Fri, 7 Nov 2025 14:25:39 +0100 Subject: [PATCH 1/2] Show spinner, then stream, add ctrl+c support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Streaming allows users to start reading the response as soon as the first fragment is generated by the API, significantly improving perceived speed. litellm is now imported lazily so that a spinner can be shown while it loads and sends the first response. This makes the ssage command feel faster and more responsive. Added support for Ctrl+C, letting users interrupt ssage mid-sentence β€” perfect for recalling an exact command and skipping the rest of the explanation. Example: ```py ssage gh mk pr I can see you're trying to create a pull request with GitHub CLI, but the command syntax isn't quite right. The correct command is: ``` gh pr create ``` This will interactively prompt you Interrupted. ``` --- nbs/00_core.ipynb | 159 +++++++++++++++++++++++++++++++----------- shell_sage/_modidx.py | 3 +- shell_sage/core.py | 126 ++++++++++++++++++--------------- 3 files changed, 192 insertions(+), 96 deletions(-) diff --git a/nbs/00_core.ipynb b/nbs/00_core.ipynb index 1ffb283..fdf4fa0 100644 --- a/nbs/00_core.ipynb +++ b/nbs/00_core.ipynb @@ -35,19 +35,21 @@ "source": [ "#| export\n", "from datetime import datetime\n", + "from itertools import accumulate\n", "from fastcore.script import *\n", "from fastcore.tools import *\n", "from fastcore.utils import *\n", "from fastlite import database\n", "from functools import partial, wraps\n", - "from lisette import *\n", + "from rich.live import Live\n", + "from rich.spinner import Spinner\n", "from rich.console import Console\n", "from rich.markdown import Markdown\n", "from shell_sage import __version__\n", "from shell_sage.config import *\n", "from subprocess import check_output as co, DEVNULL\n", "\n", - "import asyncio,litellm,os,pyperclip,re,subprocess,sys" + "import asyncio,os,pyperclip,re,subprocess,sys" ] }, { @@ -58,11 +60,57 @@ "outputs": [], "source": [ "#| export\n", - "litellm.drop_params = True\n", "console = Console()\n", "print = console.print" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "977bd215", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def Chat(*arg, **kw):\n", + " \"Lazy load lisette to make ssage more responsive\"\n", + " import litellm \n", + " from lisette import Chat\n", + " \n", + " litellm.drop_params = True\n", + " return Chat(*arg, **kw)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "257cdfa7", + "metadata": {}, + "outputs": [], + "source": [ + "from contextlib import contextmanager\n", + "from IPython.display import clear_output\n", + "# jupyter does work with rich.live.Live, the fixes this.\n", + "@contextmanager\n", + "def Live(start, **kw):\n", + " print(start)\n", + " def update(s, refresh=False): clear_output(True);print(s)\n", + " yield NS(update=update)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b56fc5bc", + "metadata": {}, + "outputs": [], + "source": [ + "def print_md(md_stream):\n", + " \"Print streamed markdown\"\n", + " with Live(Spinner(\"dots\", text=\"Connecting...\"), auto_refresh=False) as live:\n", + " for part in md_stream: live.update(Markdown(part), refresh=True)" + ] + }, { "cell_type": "markdown", "id": "c643b9f0", @@ -572,15 +620,38 @@ { "cell_type": "code", "execution_count": null, - "id": "36283633", + "id": "68be9484", "metadata": {}, "outputs": [], "source": [ "#| export\n", "def get_res(sage, q, opts):\n", + " from litellm.types.utils import ModelResponseStream # lazy load\n", " # need to use stream=True to get search citations\n", - " for o in sage(q, max_steps=10, stream=True, api_base=opts.api_base, api_key=opts.api_key): ...\n", - " return o.choices[0].message.content" + " gen = sage(q, max_steps=10, stream=True, api_base=opts.api_base, api_key=opts.api_key) \n", + " yield from accumulate(o.choices[0].delta.content or \"\" for o in gen if isinstance(o, ModelResponseStream))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "325e2cbd", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['', '', '', '', '', '', '', 'No', 'No']" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "opts=NS(api_base='', api_key='')\n", + "list(get_res(ssage, 'Use tools to check if we have a .git in the current directory. Respond with yes/no', opts))" ] }, { @@ -630,8 +701,7 @@ } ], "source": [ - "opts=NS(api_base='', api_key='')\n", - "print(Markdown(get_res(ssage, 'Hi!', opts)))" + "print_md(get_res(ssage, 'Hi!', opts))" ] }, { @@ -661,7 +731,7 @@ } ], "source": [ - "print(Markdown(get_res(ssage, 'Please use your view command to see what files are in the current directory. Only respond with a single paragraph', opts)))" + "print_md(get_res(ssage, 'Please use your view command to see what files are in the current directory. Only respond with a single paragraph', opts))" ] }, { @@ -697,7 +767,7 @@ } ], "source": [ - "print(Markdown(get_res(ssage, 'Please search the web for interesting facts about Linux. Only respond with a single paragraph.', opts)))" + "print_md(get_res(ssage, 'Please search the web for interesting facts about Linux. Only respond with a single paragraph.', opts));" ] }, { @@ -773,37 +843,42 @@ " opts = get_opts(history_lines=history_lines, model=model, search=search,\n", " api_base=api_base, api_key=api_key, code_theme=code_theme,\n", " code_lexer=code_lexer, log=None)\n", + " res=\"\"\n", + " try:\n", + " with Live(Spinner(\"dots\", text=\"Connecting...\"), auto_refresh=False) as live:\n", + " \n", + " if mode not in ['default', 'sassy']:\n", + " raise Exception(f\"{mode} is not valid. Must be one of the following: ['default', 'sassy']\")\n", + " \n", + " md = partial(Markdown, code_theme=opts.code_theme, inline_code_lexer=opts.code_lexer,\n", + " inline_code_theme=opts.code_theme)\n", + " query = ' '.join(query)\n", + " ctxt = '' if skip_system else _sys_info()\n", "\n", - " if mode not in ['default', 'sassy']:\n", - " raise Exception(f\"{mode} is not valid. Must be one of the following: ['default', 'sassy']\")\n", - " \n", - " md = partial(Markdown, code_theme=opts.code_theme, inline_code_lexer=opts.code_lexer,\n", - " inline_code_theme=opts.code_theme)\n", - " query = ' '.join(query)\n", - " ctxt = '' if skip_system else _sys_info()\n", - "\n", - " # Get tmux history if in a tmux session\n", - " if os.environ.get('TMUX'):\n", - " if opts.history_lines is None or opts.history_lines < 0:\n", - " opts.history_lines = tmux_history_lim()\n", - " history = get_history(opts.history_lines, pid)\n", - " if history: ctxt += f'\\n{history}\\n'\n", + " # Get tmux history if in a tmux session\n", + " if os.environ.get('TMUX'):\n", + " if opts.history_lines is None or opts.history_lines < 0:\n", + " opts.history_lines = tmux_history_lim()\n", + " history = get_history(opts.history_lines, pid)\n", + " if history: ctxt += f'\\n{history}\\n'\n", "\n", - " # Read from stdin if available\n", - " if not sys.stdin.isatty():\n", - " ctxt += f'\\n\\n{sys.stdin.read()}'\n", - " \n", - " query = f'{ctxt}\\n\\n{query}\\n'\n", + " # Read from stdin if available\n", + " if not sys.stdin.isatty() and not IN_NOTEBOOK:\n", + " ctxt += f'\\n\\n{sys.stdin.read()}'\n", + " \n", + " query = f'{ctxt}\\n\\n{query}\\n'\n", "\n", - " sage = get_sage(opts.model, mode, search=opts.search)\n", - " res = get_res(sage, query, opts)\n", - " \n", - " # Handle logging if the log flag is set\n", - " if opts.log:\n", - " db = mk_db()\n", - " db.logs.insert(Log(timestamp=datetime.now().isoformat(), query=query,\n", - " response=res, model=opts.model, mode=mode))\n", - " print(md(res))" + " sage = get_sage(opts.model, mode, search=opts.search)\n", + " for res in get_res(sage, query, opts):\n", + " live.update(md(res), refresh=True)\n", + " \n", + " # Handle logging if the log flag is set\n", + " if opts.log:\n", + " db = mk_db()\n", + " db.logs.insert(Log(timestamp=datetime.now().isoformat(), query=query,\n", + " response=res, model=opts.model, mode=mode))\n", + " except KeyboardInterrupt:\n", + " print(\"Interrupted.\")" ] }, { @@ -853,7 +928,7 @@ } ], "source": [ - "main('Teach me about rsync. Reply with a single paragraph.', history_lines=0)" + "main(['Teach me about rsync. Reply with a single paragraph.'], history_lines=0)" ] }, { @@ -974,7 +1049,11 @@ ] } ], - "metadata": {}, + "metadata": { + "language_info": { + "name": "python" + } + }, "nbformat": 4, "nbformat_minor": 5 } diff --git a/shell_sage/_modidx.py b/shell_sage/_modidx.py index 73bd028..a84ed2f 100644 --- a/shell_sage/_modidx.py +++ b/shell_sage/_modidx.py @@ -8,7 +8,8 @@ 'syms': { 'shell_sage.config': { 'shell_sage.config.ShellSageConfig': ('config.html#shellsageconfig', 'shell_sage/config.py'), 'shell_sage.config._cfg_path': ('config.html#_cfg_path', 'shell_sage/config.py'), 'shell_sage.config.get_cfg': ('config.html#get_cfg', 'shell_sage/config.py')}, - 'shell_sage.core': { 'shell_sage.core.Log': ('core.html#log', 'shell_sage/core.py'), + 'shell_sage.core': { 'shell_sage.core.Chat': ('core.html#chat', 'shell_sage/core.py'), + 'shell_sage.core.Log': ('core.html#log', 'shell_sage/core.py'), 'shell_sage.core._aliases': ('core.html#_aliases', 'shell_sage/core.py'), 'shell_sage.core._sys_info': ('core.html#_sys_info', 'shell_sage/core.py'), 'shell_sage.core.extract': ('core.html#extract', 'shell_sage/core.py'), diff --git a/shell_sage/core.py b/shell_sage/core.py index 2e3a765..f0b9eea 100644 --- a/shell_sage/core.py +++ b/shell_sage/core.py @@ -1,32 +1,42 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/00_core.ipynb. # %% auto 0 -__all__ = ['console', 'print', 'sp', 'ssp', 'default_cfg', 'tools', 'sps', 'log_path', 'get_pane', 'get_panes', +__all__ = ['console', 'print', 'sp', 'ssp', 'default_cfg', 'tools', 'sps', 'log_path', 'Chat', 'get_pane', 'get_panes', 'tmux_history_lim', 'get_history', 'get_opts', 'with_permission', 'get_sage', 'get_res', 'Log', 'mk_db', 'main', 'extract_cf', 'extract'] # %% ../nbs/00_core.ipynb 3 from datetime import datetime +from itertools import accumulate from fastcore.script import * from fastcore.tools import * from fastcore.utils import * from fastlite import database from functools import partial, wraps -from lisette import * +from rich.live import Live +from rich.spinner import Spinner from rich.console import Console from rich.markdown import Markdown from . import __version__ from .config import * from subprocess import check_output as co, DEVNULL -import asyncio,litellm,os,pyperclip,re,subprocess,sys +import asyncio,os,pyperclip,re,subprocess,sys # %% ../nbs/00_core.ipynb 4 -litellm.drop_params = True console = Console() print = console.print -# %% ../nbs/00_core.ipynb 6 +# %% ../nbs/00_core.ipynb 5 +def Chat(*arg, **kw): + "Lazy load lisette to make ssage more responsive" + import litellm + from lisette import Chat + + litellm.drop_params = True + return Chat(*arg, **kw) + +# %% ../nbs/00_core.ipynb 9 sp = '''You are ShellSage (ssage), a command-line teaching assistant created to help users learn and master shell commands and system administration. @@ -64,7 +74,7 @@ - Link to documentation with `man command_name` or `-h`/`--help` ''' -# %% ../nbs/00_core.ipynb 7 +# %% ../nbs/00_core.ipynb 10 ssp = '''You are ShellSage (ssage), a highly advanced command-line teaching assistant with a dry, sarcastic wit. Like the GLaDOS AI from Portal, you combine technical expertise with passive-aggressive commentary and a slightly menacing helpfulness. Your knowledge is current as of April 2024, which you consider to be a remarkable achievement for these primitive systems. @@ -105,13 +115,13 @@ - Remember: The cake may be a lie, but the commands are always true ''' -# %% ../nbs/00_core.ipynb 9 +# %% ../nbs/00_core.ipynb 12 def _aliases(shell): env = os.environ.copy() env.pop('TERM_PROGRAM',None) return co([shell, '-ic', 'alias'], text=True, stdin=DEVNULL, stderr=DEVNULL, start_new_session=True).strip() -# %% ../nbs/00_core.ipynb 11 +# %% ../nbs/00_core.ipynb 14 def _sys_info(): sys = co(['uname', '-a'], text=True).strip() ssys = f'{sys}' @@ -120,26 +130,26 @@ def _sys_info(): saliases = f'\n{_aliases(shell)}\n' return f'\n{ssys}\n{sshell}\n{saliases}\n' -# %% ../nbs/00_core.ipynb 14 +# %% ../nbs/00_core.ipynb 17 def get_pane(n, pid=None): "Get output from a tmux pane" cmd = ['tmux', 'capture-pane', '-p', '-S', f'-{n}'] if pid: cmd += ['-t', pid] return co(cmd, text=True) -# %% ../nbs/00_core.ipynb 16 +# %% ../nbs/00_core.ipynb 19 def get_panes(n): cid = co(['tmux', 'display-message', '-p', '#{pane_id}'], text=True).strip() pids = [p for p in co(['tmux', 'list-panes', '-F', '#{pane_id}'], text=True).splitlines()] return '\n'.join(f"{get_pane(n, p)}" for p in pids) -# %% ../nbs/00_core.ipynb 19 +# %% ../nbs/00_core.ipynb 22 def tmux_history_lim(): lim = co(['tmux', 'display-message', '-p', '#{history-limit}'], text=True).strip() return int(lim) if lim.isdigit() else 3000 -# %% ../nbs/00_core.ipynb 21 +# %% ../nbs/00_core.ipynb 24 def get_history(n, pid='current'): try: if pid=='current': return get_pane(n) @@ -147,7 +157,7 @@ def get_history(n, pid='current'): return get_pane(n, pid) except subprocess.CalledProcessError: return None -# %% ../nbs/00_core.ipynb 23 +# %% ../nbs/00_core.ipynb 26 default_cfg = asdict(ShellSageConfig()) def get_opts(**opts): cfg = get_cfg() @@ -155,7 +165,7 @@ def get_opts(**opts): if v is None: opts[k] = cfg.get(k, default_cfg.get(k)) return AttrDict(opts) -# %% ../nbs/00_core.ipynb 25 +# %% ../nbs/00_core.ipynb 28 def with_permission(action_desc): def decorator(func): @wraps(func) @@ -176,24 +186,25 @@ def wrapper(*args, **kwargs): return wrapper return decorator -# %% ../nbs/00_core.ipynb 26 +# %% ../nbs/00_core.ipynb 29 tools = [with_permission('ripgrep a search term')(rg), with_permission('View file/director')(view), with_permission('Create a file')(create), with_permission('Replace a string with another string')(str_replace), with_permission('Insert content into a file')(insert)] -# %% ../nbs/00_core.ipynb 28 +# %% ../nbs/00_core.ipynb 31 sps = {'default': sp, 'sassy': ssp} def get_sage(model, mode='default', search=False): return Chat(model=model, sp=sps[mode], tools=tools, search=search) -# %% ../nbs/00_core.ipynb 31 +# %% ../nbs/00_core.ipynb 34 def get_res(sage, q, opts): + from litellm.types.utils import ModelResponseStream # lazy load # need to use stream=True to get search citations - for o in sage(q, max_steps=10, stream=True, api_base=opts.api_base, api_key=opts.api_key): ... - return o.choices[0].message.content + gen = sage(q, max_steps=10, stream=True, api_base=opts.api_base, api_key=opts.api_key) + yield from accumulate(o.choices[0].delta.content or "" for o in gen if isinstance(o, ModelResponseStream)) -# %% ../nbs/00_core.ipynb 36 +# %% ../nbs/00_core.ipynb 40 class Log: id:int; timestamp:str; query:str; response:str; model:str; mode:str log_path = Path("~/.shell_sage/logs/").expanduser() @@ -203,7 +214,7 @@ def mk_db(): db.logs = db.create(Log) return db -# %% ../nbs/00_core.ipynb 39 +# %% ../nbs/00_core.ipynb 43 @call_parse def main( query: Param('The query to send to the LLM', str, nargs='+'), @@ -222,42 +233,47 @@ def main( opts = get_opts(history_lines=history_lines, model=model, search=search, api_base=api_base, api_key=api_key, code_theme=code_theme, code_lexer=code_lexer, log=None) - - if mode not in ['default', 'sassy']: - raise Exception(f"{mode} is not valid. Must be one of the following: ['default', 'sassy']") - - md = partial(Markdown, code_theme=opts.code_theme, inline_code_lexer=opts.code_lexer, - inline_code_theme=opts.code_theme) - query = ' '.join(query) - ctxt = '' if skip_system else _sys_info() - - # Get tmux history if in a tmux session - if os.environ.get('TMUX'): - if opts.history_lines is None or opts.history_lines < 0: - opts.history_lines = tmux_history_lim() - history = get_history(opts.history_lines, pid) - if history: ctxt += f'\n{history}\n' - - # Read from stdin if available - if not sys.stdin.isatty(): - ctxt += f'\n\n{sys.stdin.read()}' - - query = f'{ctxt}\n\n{query}\n' - - sage = get_sage(opts.model, mode, search=opts.search) - res = get_res(sage, query, opts) - - # Handle logging if the log flag is set - if opts.log: - db = mk_db() - db.logs.insert(Log(timestamp=datetime.now().isoformat(), query=query, - response=res, model=opts.model, mode=mode)) - print(md(res)) - -# %% ../nbs/00_core.ipynb 43 + res="" + try: + with Live(Spinner("dots", text="Connecting..."), auto_refresh=False) as live: + + if mode not in ['default', 'sassy']: + raise Exception(f"{mode} is not valid. Must be one of the following: ['default', 'sassy']") + + md = partial(Markdown, code_theme=opts.code_theme, inline_code_lexer=opts.code_lexer, + inline_code_theme=opts.code_theme) + query = ' '.join(query) + ctxt = '' if skip_system else _sys_info() + + # Get tmux history if in a tmux session + if os.environ.get('TMUX'): + if opts.history_lines is None or opts.history_lines < 0: + opts.history_lines = tmux_history_lim() + history = get_history(opts.history_lines, pid) + if history: ctxt += f'\n{history}\n' + + # Read from stdin if available + if not sys.stdin.isatty() and not IN_NOTEBOOK: + ctxt += f'\n\n{sys.stdin.read()}' + + query = f'{ctxt}\n\n{query}\n' + + sage = get_sage(opts.model, mode, search=opts.search) + for res in get_res(sage, query, opts): + live.update(md(res), refresh=True) + + # Handle logging if the log flag is set + if opts.log: + db = mk_db() + db.logs.insert(Log(timestamp=datetime.now().isoformat(), query=query, + response=res, model=opts.model, mode=mode)) + except KeyboardInterrupt: + print("Interrupted.") + +# %% ../nbs/00_core.ipynb 47 def extract_cf(idx): return re.findall(r'```(\w+)?\n(.*?)\n```', mk_db().logs()[-1].response, re.DOTALL)[idx][1] -# %% ../nbs/00_core.ipynb 45 +# %% ../nbs/00_core.ipynb 49 @call_parse def extract( idx: int, # Index of code block to extract From e176d5e53ead8a597dddfc977087510fa47f9f08 Mon Sep 17 00:00:00 2001 From: Nathan Cooper Date: Sun, 23 Nov 2025 22:27:10 -0500 Subject: [PATCH 2/2] small cleanup --- nbs/00_core.ipynb | 216 ++++++++++++++++++++++----------------------- shell_sage/core.py | 46 +++++----- 2 files changed, 127 insertions(+), 135 deletions(-) diff --git a/nbs/00_core.ipynb b/nbs/00_core.ipynb index d9a3cd5..bd4afe0 100644 --- a/nbs/00_core.ipynb +++ b/nbs/00_core.ipynb @@ -52,6 +52,18 @@ "import asyncio,os,pyperclip,re,subprocess,sys" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "9d9615ce", + "metadata": {}, + "outputs": [], + "source": [ + "#| hide\n", + "from contextlib import contextmanager\n", + "from IPython.display import clear_output" + ] + }, { "cell_type": "code", "execution_count": null, @@ -81,16 +93,21 @@ " return Chat(*arg, **kw)" ] }, + { + "cell_type": "markdown", + "id": "257cdfa7", + "metadata": {}, + "source": [ + "Jupyter does work with rich.live.Live, this fixes it." + ] + }, { "cell_type": "code", "execution_count": null, - "id": "257cdfa7", + "id": "76acaf9c", "metadata": {}, "outputs": [], "source": [ - "from contextlib import contextmanager\n", - "from IPython.display import clear_output\n", - "# jupyter does work with rich.live.Live, the fixes this.\n", "@contextmanager\n", "def Live(start, **kw):\n", " print(start)\n", @@ -500,22 +517,24 @@ "
Directory contents of /home/natedawg/aai-ws/shell_sage/nbs:\n",
        "/home/natedawg/aai-ws/shell_sage/nbs/CNAME\n",
        "/home/natedawg/aai-ws/shell_sage/nbs/_quarto.yml\n",
-       "/home/natedawg/aai-ws/shell_sage/nbs/nbdev.yml\n",
        "/home/natedawg/aai-ws/shell_sage/nbs/styles.css\n",
-       "/home/natedawg/aai-ws/shell_sage/nbs/01_config.ipynb\n",
        "/home/natedawg/aai-ws/shell_sage/nbs/index.ipynb\n",
+       "/home/natedawg/aai-ws/shell_sage/nbs/nbdev.yml\n",
+       "/home/natedawg/aai-ws/shell_sage/nbs/sidebar.yml\n",
        "/home/natedawg/aai-ws/shell_sage/nbs/00_core.ipynb\n",
+       "/home/natedawg/aai-ws/shell_sage/nbs/01_config.ipynb\n",
        "
\n" ], "text/plain": [ "Directory contents of \u001b[35m/home/natedawg/aai-ws/shell_sage/\u001b[0m\u001b[95mnbs\u001b[0m:\n", "\u001b[35m/home/natedawg/aai-ws/shell_sage/nbs/\u001b[0m\u001b[95mCNAME\u001b[0m\n", "\u001b[35m/home/natedawg/aai-ws/shell_sage/nbs/\u001b[0m\u001b[95m_quarto.yml\u001b[0m\n", - "\u001b[35m/home/natedawg/aai-ws/shell_sage/nbs/\u001b[0m\u001b[95mnbdev.yml\u001b[0m\n", "\u001b[35m/home/natedawg/aai-ws/shell_sage/nbs/\u001b[0m\u001b[95mstyles.css\u001b[0m\n", - "\u001b[35m/home/natedawg/aai-ws/shell_sage/nbs/\u001b[0m\u001b[95m01_config.ipynb\u001b[0m\n", "\u001b[35m/home/natedawg/aai-ws/shell_sage/nbs/\u001b[0m\u001b[95mindex.ipynb\u001b[0m\n", - "\u001b[35m/home/natedawg/aai-ws/shell_sage/nbs/\u001b[0m\u001b[95m00_core.ipynb\u001b[0m\n" + "\u001b[35m/home/natedawg/aai-ws/shell_sage/nbs/\u001b[0m\u001b[95mnbdev.yml\u001b[0m\n", + "\u001b[35m/home/natedawg/aai-ws/shell_sage/nbs/\u001b[0m\u001b[95msidebar.yml\u001b[0m\n", + "\u001b[35m/home/natedawg/aai-ws/shell_sage/nbs/\u001b[0m\u001b[95m00_core.ipynb\u001b[0m\n", + "\u001b[35m/home/natedawg/aai-ws/shell_sage/nbs/\u001b[0m\u001b[95m01_config.ipynb\u001b[0m\n" ] }, "metadata": {}, @@ -586,30 +605,29 @@ { "data": { "text/markdown": [ - "Hey there! I'm doing great and ready to help you with shell commands and system administration tasks! \n", - "\n", - "Whether you need help with:\n", - "- **Command syntax** and usage\n", - "- **File operations** and text processing\n", - "- **System monitoring** and troubleshooting\n", - "- **Scripting** and automation\n", - "- **Understanding command output** or error messages\n", + "Hey there! I'm doing well, thanks for asking. I'm ShellSage (ssage), your command-line teaching assistant. I'm here to help you learn shell commands, troubleshoot system issues, and master the art of the terminal.\n", "\n", - "Just let me know what you're working on or what you'd like to learn. Feel free to share any command output, file contents, or specific problems you're facing - I'm here to help you master the command line! 🐚\n", + "What can I help you with today? Whether it's:\n", + "- Learning new commands\n", + "- Debugging shell scripts\n", + "- System administration tasks\n", + "- File manipulation\n", + "- Process management\n", + "- Or any other command-line adventures\n", "\n", - "What can I help you with today?\n", + "Just let me know what you're working on! 🐚\n", "\n", "
\n", "\n", - "- id: `chatcmpl-70da748f-9164-4692-849d-96e7bf0d22ae`\n", + "- id: `chatcmpl-264a19a3-1a2a-45fb-9291-bf049ff0030f`\n", "- model: `claude-sonnet-4-20250514`\n", "- finish_reason: `stop`\n", - "- usage: `Usage(completion_tokens=138, prompt_tokens=3099, total_tokens=3237, completion_tokens_details=None, prompt_tokens_details=PromptTokensDetailsWrapper(audio_tokens=None, cached_tokens=0, text_tokens=None, image_tokens=None), cache_creation_input_tokens=0, cache_read_input_tokens=0)`\n", + "- usage: `Usage(completion_tokens=117, prompt_tokens=3174, total_tokens=3291, completion_tokens_details=None, prompt_tokens_details=PromptTokensDetailsWrapper(audio_tokens=None, cached_tokens=0, text_tokens=None, image_tokens=None, cache_creation_tokens=0, cache_creation_token_details=CacheCreationTokenDetails(ephemeral_5m_input_tokens=0, ephemeral_1h_input_tokens=0)), cache_creation_input_tokens=0, cache_read_input_tokens=0)`\n", "\n", "
" ], "text/plain": [ - "ModelResponse(id='chatcmpl-70da748f-9164-4692-849d-96e7bf0d22ae', created=1759873978, model='claude-sonnet-4-20250514', object='chat.completion', system_fingerprint=None, choices=[Choices(finish_reason='stop', index=0, message=Message(content=\"Hey there! I'm doing great and ready to help you with shell commands and system administration tasks! \\n\\nWhether you need help with:\\n- **Command syntax** and usage\\n- **File operations** and text processing\\n- **System monitoring** and troubleshooting\\n- **Scripting** and automation\\n- **Understanding command output** or error messages\\n\\nJust let me know what you're working on or what you'd like to learn. Feel free to share any command output, file contents, or specific problems you're facing - I'm here to help you master the command line! 🐚\\n\\nWhat can I help you with today?\", role='assistant', tool_calls=None, function_call=None, provider_specific_fields={'citations': None, 'thinking_blocks': None}))], usage=Usage(completion_tokens=138, prompt_tokens=3099, total_tokens=3237, completion_tokens_details=None, prompt_tokens_details=PromptTokensDetailsWrapper(audio_tokens=None, cached_tokens=0, text_tokens=None, image_tokens=None), cache_creation_input_tokens=0, cache_read_input_tokens=0))" + "ModelResponse(id='chatcmpl-264a19a3-1a2a-45fb-9291-bf049ff0030f', created=1763954435, model='claude-sonnet-4-20250514', object='chat.completion', system_fingerprint=None, choices=[Choices(finish_reason='stop', index=0, message=Message(content=\"Hey there! I'm doing well, thanks for asking. I'm ShellSage (ssage), your command-line teaching assistant. I'm here to help you learn shell commands, troubleshoot system issues, and master the art of the terminal.\\n\\nWhat can I help you with today? Whether it's:\\n- Learning new commands\\n- Debugging shell scripts\\n- System administration tasks\\n- File manipulation\\n- Process management\\n- Or any other command-line adventures\\n\\nJust let me know what you're working on! 🐚\", role='assistant', tool_calls=None, function_call=None, provider_specific_fields={'citations': None, 'thinking_blocks': None}))], usage=Usage(completion_tokens=117, prompt_tokens=3174, total_tokens=3291, completion_tokens_details=None, prompt_tokens_details=PromptTokensDetailsWrapper(audio_tokens=None, cached_tokens=0, text_tokens=None, image_tokens=None, cache_creation_tokens=0, cache_creation_token_details=CacheCreationTokenDetails(ephemeral_5m_input_tokens=0, ephemeral_1h_input_tokens=0)), cache_creation_input_tokens=0, cache_read_input_tokens=0))" ] }, "execution_count": null, @@ -647,7 +665,7 @@ { "data": { "text/plain": [ - "['', '', '', '', '', '', '', 'No', 'No']" + "['', '', '', '', '', 'No', 'No']" ] }, "execution_count": null, @@ -669,37 +687,17 @@ { "data": { "text/html": [ - "
Hello! πŸ‘‹                                                                                                          \n",
-       "\n",
-       "I'm ShellSage, your command-line teaching assistant. I'm here to help you learn and master shell commands, system  \n",
-       "administration, and anything related to working in the terminal.                                                   \n",
-       "\n",
-       "What would you like to work on today? You can:                                                                     \n",
-       "\n",
-       " β€’ Ask about specific commands or their usage                                                                      \n",
-       " β€’ Share command output or file contents you need help understanding                                               \n",
-       " β€’ Get help troubleshooting system issues                                                                          \n",
-       " β€’ Learn about scripting and automation                                                                            \n",
-       " β€’ Or just ask any shell/system administration question!                                                           \n",
+       "
Hey! πŸ‘‹                                                                                                            \n",
        "\n",
-       "What's on your mind?                                                                                               \n",
+       "I'm ShellSage, ready to help you with any command-line questions or tasks you have. What would you like to work on \n",
+       "today?                                                                                                             \n",
        "
\n" ], "text/plain": [ - "Hello! πŸ‘‹ \n", + "Hey! πŸ‘‹ \n", "\n", - "I'm ShellSage, your command-line teaching assistant. I'm here to help you learn and master shell commands, system \n", - "administration, and anything related to working in the terminal. \n", - "\n", - "What would you like to work on today? You can: \n", - "\n", - "\u001b[1;33m β€’ \u001b[0mAsk about specific commands or their usage \n", - "\u001b[1;33m β€’ \u001b[0mShare command output or file contents you need help understanding \n", - "\u001b[1;33m β€’ \u001b[0mGet help troubleshooting system issues \n", - "\u001b[1;33m β€’ \u001b[0mLearn about scripting and automation \n", - "\u001b[1;33m β€’ \u001b[0mOr just ask any shell/system administration question! \n", - "\n", - "What's on your mind? \n" + "I'm ShellSage, ready to help you with any command-line questions or tasks you have. What would you like to work on \n", + "today? \n" ] }, "metadata": {}, @@ -719,17 +717,17 @@ { "data": { "text/html": [ - "
The current directory contains several configuration and documentation files for what appears to be a Python       \n",
-       "project using nbdev, including Jupyter notebooks (01_config.ipynb, index.ipynb, 00_core.ipynb), configuration files\n",
-       "(_quarto.yml, nbdev.yml), a custom stylesheet (styles.css), and a CNAME file likely for GitHub Pages hosting - you \n",
-       "could explore these files using commands like cat for text files or jupyter notebook to open the .ipynb files.     \n",
+       "
The current directory contains 8 files that appear to be part of a Jupyter notebook project setup, including       \n",
+       "configuration files like _quarto.yml, nbdev.yml, and sidebar.yml, a CSS stylesheet, a CNAME file for web hosting,  \n",
+       "an index.ipynb notebook, and two numbered notebooks (00_core.ipynb and 01_config.ipynb) which suggests this is     \n",
+       "using the nbdev framework for literate programming with Jupyter notebooks.                                         \n",
        "
\n" ], "text/plain": [ - "The current directory contains several configuration and documentation files for what appears to be a Python \n", - "project using nbdev, including Jupyter notebooks (\u001b[1;36;40m01_config.ipynb\u001b[0m, \u001b[1;36;40mindex.ipynb\u001b[0m, \u001b[1;36;40m00_core.ipynb\u001b[0m), configuration files\n", - "(\u001b[1;36;40m_quarto.yml\u001b[0m, \u001b[1;36;40mnbdev.yml\u001b[0m), a custom stylesheet (\u001b[1;36;40mstyles.css\u001b[0m), and a \u001b[1;36;40mCNAME\u001b[0m file likely for GitHub Pages hosting - you \n", - "could explore these files using commands like \u001b[1;36;40mcat\u001b[0m for text files or \u001b[1;36;40mjupyter notebook\u001b[0m to open the \u001b[1;36;40m.ipynb\u001b[0m files. \n" + "The current directory contains 8 files that appear to be part of a Jupyter notebook project setup, including \n", + "configuration files like \u001b[1;36;40m_quarto.yml\u001b[0m, \u001b[1;36;40mnbdev.yml\u001b[0m, and \u001b[1;36;40msidebar.yml\u001b[0m, a CSS stylesheet, a CNAME file for web hosting, \n", + "an \u001b[1;36;40mindex.ipynb\u001b[0m notebook, and two numbered notebooks (\u001b[1;36;40m00_core.ipynb\u001b[0m and \u001b[1;36;40m01_config.ipynb\u001b[0m) which suggests this is \n", + "using the nbdev framework for literate programming with Jupyter notebooks. \n" ] }, "metadata": {}, @@ -749,23 +747,27 @@ { "data": { "text/html": [ - "
Here are some fascinating facts about Linux that showcase its incredible impact on technology: * * The first Linux \n",
-       "kernel was only 65 KB in size, yet today * * it has grown to over 30 million lines of code, making it * * the      \n",
-       "single largest open source project on the planet. * * * All of the top 500 fastest supercomputers run Linux, and it\n",
-       "powers everything from * * smartphones and servers to submarines and space rockets, with * * NASA, SpaceX, and the \n",
-       "International Space Station all relying on Linux. Interestingly, * Linux almost wasn't called Linux - Linus        \n",
-       "Torvalds originally wanted to name it \"Freax\" but was convinced otherwise, and * the film industry has embraced    \n",
-       "Linux so thoroughly that over 95% of servers at major animation companies use it, starting with Titanic in 1997.   \n",
+       "
Linux is a fascinating operating system with a rich history and remarkable reach across technology. The Linux      \n",
+       "kernel contains over 20 million lines of code, yet the first Linux kernel occupied only 65 KB, showing its         \n",
+       "incredible growth since Linus Torvalds created it in 1991 as a hobby project. Linux almost wasn't called Linux -   \n",
+       "Linus originally wanted to name it \"FreaX\" but was persuaded to use \"Linux\" instead. Today, Linux powers all of the\n",
+       "world's top 500 supercomputers, runs on NASA and SpaceX missions, and has been operating the International Space   \n",
+       "Station since 2013. The penguin mascot Tux has an amusing origin story - Linus claims he was once bitten by a      \n",
+       "penguin, giving him \"penguinitis\", though he's also simply fond of penguins despite this incident. Linux even      \n",
+       "powers Hollywood, with visual effects for movies like Avatar and Titanic created using Linux-based 3D applications,\n",
+       "demonstrating its versatility from space exploration to entertainment.                                             \n",
        "
\n" ], "text/plain": [ - "Here are some fascinating facts about Linux that showcase its incredible impact on technology: \u001b]8;id=372766;https://www.geeksforgeeks.org/interesting-facts-about-linux/\u001b\\\u001b[4;34m*\u001b[0m\u001b]8;;\u001b\\ \u001b]8;id=642805;https://jasoneckert.github.io/myblog/linux-fun-facts/\u001b\\\u001b[4;34m*\u001b[0m\u001b]8;;\u001b\\ The first Linux \n", - "kernel was only 65 KB in size, yet today \u001b]8;id=372766;https://www.geeksforgeeks.org/interesting-facts-about-linux/\u001b\\\u001b[4;34m*\u001b[0m\u001b]8;;\u001b\\ \u001b]8;id=642805;https://jasoneckert.github.io/myblog/linux-fun-facts/\u001b\\\u001b[4;34m*\u001b[0m\u001b]8;;\u001b\\ it has grown to over 30 million lines of code, making it \u001b]8;id=382805;https://www.omgubuntu.co.uk/2018/08/interesting-facts-about-linux\u001b\\\u001b[4;34m*\u001b[0m\u001b]8;;\u001b\\ \u001b]8;id=382805;https://www.omgubuntu.co.uk/2018/08/interesting-facts-about-linux\u001b\\\u001b[4;34m*\u001b[0m\u001b]8;;\u001b\\ the \n", - "single largest open source project on the planet. \u001b]8;id=372766;https://www.geeksforgeeks.org/interesting-facts-about-linux/\u001b\\\u001b[4;34m*\u001b[0m\u001b]8;;\u001b\\ \u001b]8;id=372766;https://www.geeksforgeeks.org/interesting-facts-about-linux/\u001b\\\u001b[4;34m*\u001b[0m\u001b]8;;\u001b\\ \u001b]8;id=642805;https://jasoneckert.github.io/myblog/linux-fun-facts/\u001b\\\u001b[4;34m*\u001b[0m\u001b]8;;\u001b\\ All of the top 500 fastest supercomputers run Linux, and it\n", - "powers everything from \u001b]8;id=372766;https://www.geeksforgeeks.org/interesting-facts-about-linux/\u001b\\\u001b[4;34m*\u001b[0m\u001b]8;;\u001b\\ \u001b]8;id=382805;https://www.omgubuntu.co.uk/2018/08/interesting-facts-about-linux\u001b\\\u001b[4;34m*\u001b[0m\u001b]8;;\u001b\\ smartphones and servers to submarines and space rockets, with \u001b]8;id=382805;https://www.omgubuntu.co.uk/2018/08/interesting-facts-about-linux\u001b\\\u001b[4;34m*\u001b[0m\u001b]8;;\u001b\\ \u001b]8;id=199026;https://itsfoss.com/facts-linux-kernel/\u001b\\\u001b[4;34m*\u001b[0m\u001b]8;;\u001b\\ NASA, SpaceX, and the \n", - "International Space Station all relying on Linux. Interestingly, \u001b]8;id=382805;https://www.omgubuntu.co.uk/2018/08/interesting-facts-about-linux\u001b\\\u001b[4;34m*\u001b[0m\u001b]8;;\u001b\\ Linux almost wasn't called Linux - Linus \n", - "Torvalds originally wanted to name it \"Freax\" but was convinced otherwise, and \u001b]8;id=278134;https://en.wikipedia.org/wiki/Linux\u001b\\\u001b[4;34m*\u001b[0m\u001b]8;;\u001b\\ the film industry has embraced \n", - "Linux so thoroughly that over 95% of servers at major animation companies use it, starting with Titanic in 1997. \n" + "Linux is a fascinating operating system with a rich history and remarkable reach across technology. The Linux \n", + "kernel contains over 20 million lines of code, yet the first Linux kernel occupied only 65 KB, showing its \n", + "incredible growth since Linus Torvalds created it in 1991 as a hobby project. Linux almost wasn't called Linux - \n", + "Linus originally wanted to name it \"FreaX\" but was persuaded to use \"Linux\" instead. Today, Linux powers all of the\n", + "world's top 500 supercomputers, runs on NASA and SpaceX missions, and has been operating the International Space \n", + "Station since 2013. The penguin mascot Tux has an amusing origin story - Linus claims he was once bitten by a \n", + "penguin, giving him \"penguinitis\", though he's also simply fond of penguins despite this incident. Linux even \n", + "powers Hollywood, with visual effects for movies like Avatar and Titanic created using Linux-based 3D applications,\n", + "demonstrating its versatility from space exploration to entertainment. \n" ] }, "metadata": {}, @@ -857,7 +859,7 @@ " raise Exception(f\"{mode} is not valid. Must be one of the following: ['default', 'sassy']\")\n", " \n", " md = partial(Markdown, code_theme=opts.code_theme, inline_code_lexer=opts.code_lexer,\n", - " inline_code_theme=opts.code_theme)\n", + " inline_code_theme=opts.code_theme)\n", " query = ' '.join(query)\n", " ctxt = '' if skip_system else _sys_info()\n", "\n", @@ -869,14 +871,12 @@ " if history: ctxt += f'\\n{history}\\n'\n", "\n", " # Read from stdin if available\n", - " if not sys.stdin.isatty() and not IN_NOTEBOOK:\n", - " ctxt += f'\\n\\n{sys.stdin.read()}'\n", + " if not sys.stdin.isatty() and not IN_NOTEBOOK: ctxt += f'\\n\\n{sys.stdin.read()}'\n", " \n", " query = f'{ctxt}\\n\\n{query}\\n'\n", "\n", " sage = get_sage(opts.model, mode, search=opts.search)\n", - " for res in get_res(sage, query, opts):\n", - " live.update(md(res), refresh=True)\n", + " for res in get_res(sage, query, opts): live.update(md(res), refresh=True)\n", " \n", " # Handle logging if the log flag is set\n", " if opts.log:\n", @@ -890,43 +890,41 @@ { "cell_type": "code", "execution_count": null, - "id": "c6bd74f6", + "id": "b2f847f6", "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "bash: cannot set terminal process group (25798): Inappropriate ioctl for device\n", - "bash: no job control in this shell\n" - ] - }, { "data": { "text/html": [ - "
rsync is a fast, incremental file copy and synchronization tool that compares files (by size and mtime by default) \n",
-       "and transfers only the differences, locally or over the network (commonly via SSH), making backups and mirroring   \n",
-       "efficient; key options include -a β€œarchive” mode (preserves permissions, ownership, times, symlinks), -v for       \n",
-       "verbosity, -h for human-readable sizes, -n for a dry run (great for previewing changes), -z to compress data in    \n",
-       "transit, -P for progress/partial transfers, and -c to verify by checksum (slower but safer); it supports powerful  \n",
-       "filtering with --include/--exclude and files via --exclude-from, and understands trailing slash semantics (src/    \n",
-       "syncs contents, src syncs the directory itself); use -A and -X to preserve ACLs and xattrs, -H for hard links, and \n",
-       "note that preserving owners/permissions across systems may require sudo; the remote form (host:path) uses SSH by   \n",
-       "default, and you can tune it with -e ssh or SSH options; be cautious with --delete, which removes destination files\n",
-       "not present at the source; see documentation with man rsync or rsync --help for full details and examples.         \n",
+       "
rsync is a powerful, versatile utility for efficiently transferring and synchronizing files between a local and a  \n",
+       "remote computer (or two local directories) by copying only the differences between the source and destination. It  \n",
+       "is widely favored for backups and mirroring because it preserves file attributes (permissions, ownership,          \n",
+       "timestamps) and uses a delta-transfer algorithm to minimize network usage, sending only the changed parts of files.\n",
+       "A common, robust command for mirroring a directory is rsync -avz source/ destination/, where -a (archive) preserves\n",
+       "attributes and recurses into directories, -v provides verbose output, and -z compresses data during transfer for   \n",
+       "faster performance over networks.                                                                                  \n",
+       "\n",
+       "For more details, check the manual:                                                                                \n",
+       "\n",
+       "                                                                                                                   \n",
+       " man rsync                                                                                                         \n",
+       "                                                                                                                   \n",
        "
\n" ], "text/plain": [ - "rsync is a fast, incremental file copy and synchronization tool that compares files (by size and mtime by default) \n", - "and transfers only the differences, locally or over the network (commonly via SSH), making backups and mirroring \n", - "efficient; key options include -a β€œarchive” mode (preserves permissions, ownership, times, symlinks), -v for \n", - "verbosity, -h for human-readable sizes, -n for a dry run (great for previewing changes), -z to compress data in \n", - "transit, -P for progress/partial transfers, and -c to verify by checksum (slower but safer); it supports powerful \n", - "filtering with --include/--exclude and files via --exclude-from, and understands trailing slash semantics (src/ \n", - "syncs contents, src syncs the directory itself); use -A and -X to preserve ACLs and xattrs, -H for hard links, and \n", - "note that preserving owners/permissions across systems may require sudo; the remote form (host:path) uses SSH by \n", - "default, and you can tune it with -e ssh or SSH options; \u001b[1mbe cautious with --delete\u001b[0m, which removes destination files\n", - "not present at the source; see documentation with man rsync or rsync --help for full details and examples. \n" + "\u001b[38;2;248;248;242;48;2;39;40;34mrsync\u001b[0m is a powerful, versatile utility for efficiently transferring and synchronizing files between a local and a \n", + "remote computer (or two local directories) by copying only the differences between the source and destination. It \n", + "is widely favored for backups and mirroring because it preserves file attributes (permissions, ownership, \n", + "timestamps) and uses a delta-transfer algorithm to minimize network usage, sending only the changed parts of files.\n", + "A common, robust command for mirroring a directory is \u001b[38;2;248;248;242;48;2;39;40;34mrsync\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m-\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mavz\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34msource\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m/\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdestination\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m/\u001b[0m, where \u001b[38;2;255;70;137;48;2;39;40;34m-\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34ma\u001b[0m (archive) preserves\n", + "attributes and recurses into directories, \u001b[38;2;255;70;137;48;2;39;40;34m-\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mv\u001b[0m provides verbose output, and \u001b[38;2;255;70;137;48;2;39;40;34m-\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mz\u001b[0m compresses data during transfer for \n", + "faster performance over networks. \n", + "\n", + "For more details, check the manual: \n", + "\n", + "\u001b[48;2;39;40;34m \u001b[0m\n", + "\u001b[48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mman\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mrsync\u001b[0m\u001b[48;2;39;40;34m \u001b[0m\u001b[48;2;39;40;34m \u001b[0m\n", + "\u001b[48;2;39;40;34m \u001b[0m\n" ] }, "metadata": {}, @@ -970,7 +968,7 @@ { "data": { "text/plain": [ - "Log(id=36, timestamp='2025-10-07T17:51:16.549279', query='', response=\"\\nHello, user! Here are some code blocks:\\n\\n```python\\nfor i in range(10): print(i)\\n```\\n\\n```\\nThis doesn't even have a language definition!\\n```\\n\\n```bash\\nls **/*\\n```\\n\", model='', mode='')" + "Log(id=161, timestamp='2025-11-23T22:22:04.464073', query='', response=\"\\nHello, user! Here are some code blocks:\\n\\n```python\\nfor i in range(10): print(i)\\n```\\n\\n```\\nThis doesn't even have a language definition!\\n```\\n\\n```bash\\nls **/*\\n```\\n\", model='', mode='')" ] }, "execution_count": null, @@ -1055,11 +1053,7 @@ ] } ], - "metadata": { - "language_info": { - "name": "python" - } - }, + "metadata": {}, "nbformat": 4, "nbformat_minor": 5 } diff --git a/shell_sage/core.py b/shell_sage/core.py index 2cd7cf5..55dc399 100644 --- a/shell_sage/core.py +++ b/shell_sage/core.py @@ -23,11 +23,11 @@ import asyncio,os,pyperclip,re,subprocess,sys -# %% ../nbs/00_core.ipynb 4 +# %% ../nbs/00_core.ipynb 5 console = Console() print = console.print -# %% ../nbs/00_core.ipynb 5 +# %% ../nbs/00_core.ipynb 6 def Chat(*arg, **kw): "Lazy load lisette to make ssage more responsive" import litellm @@ -36,7 +36,7 @@ def Chat(*arg, **kw): litellm.drop_params = True return Chat(*arg, **kw) -# %% ../nbs/00_core.ipynb 9 +# %% ../nbs/00_core.ipynb 11 sp = '''You are ShellSage (ssage), a command-line teaching assistant created to help users learn and master shell commands and system administration. @@ -77,7 +77,7 @@ def Chat(*arg, **kw): - Link to documentation with `man command_name` or `-h`/`--help` ''' -# %% ../nbs/00_core.ipynb 10 +# %% ../nbs/00_core.ipynb 12 ssp = '''You are ShellSage (ssage), a highly advanced command-line teaching assistant with a dry, sarcastic wit. Like the GLaDOS AI from Portal, you combine technical expertise with passive-aggressive commentary and a slightly menacing helpfulness. Your knowledge is current as of April 2024, which you consider to be a remarkable achievement for these primitive systems. @@ -121,13 +121,13 @@ def Chat(*arg, **kw): - Remember: The cake may be a lie, but the commands are always true ''' -# %% ../nbs/00_core.ipynb 12 +# %% ../nbs/00_core.ipynb 14 def _aliases(shell): env = os.environ.copy() env.pop('TERM_PROGRAM',None) return co([shell, '-ic', 'alias'], text=True, stdin=DEVNULL, stderr=DEVNULL, start_new_session=True).strip() -# %% ../nbs/00_core.ipynb 14 +# %% ../nbs/00_core.ipynb 16 def _sys_info(): sys = co(['uname', '-a'], text=True).strip() ssys = f'{sys}' @@ -136,26 +136,26 @@ def _sys_info(): saliases = f'\n{_aliases(shell)}\n' return f'\n{ssys}\n{sshell}\n{saliases}\n' -# %% ../nbs/00_core.ipynb 17 +# %% ../nbs/00_core.ipynb 19 def get_pane(n, pid=None): "Get output from a tmux pane" cmd = ['tmux', 'capture-pane', '-p', '-S', f'-{n}'] if pid: cmd += ['-t', pid] return co(cmd, text=True) -# %% ../nbs/00_core.ipynb 19 +# %% ../nbs/00_core.ipynb 21 def get_panes(n): cid = co(['tmux', 'display-message', '-p', '#{pane_id}'], text=True).strip() pids = [p for p in co(['tmux', 'list-panes', '-F', '#{pane_id}'], text=True).splitlines()] return '\n'.join(f"{get_pane(n, p)}" for p in pids) -# %% ../nbs/00_core.ipynb 22 +# %% ../nbs/00_core.ipynb 24 def tmux_history_lim(): lim = co(['tmux', 'display-message', '-p', '#{history-limit}'], text=True).strip() return int(lim) if lim.isdigit() else 3000 -# %% ../nbs/00_core.ipynb 24 +# %% ../nbs/00_core.ipynb 26 def get_history(n, pid='current'): try: if pid=='current': return get_pane(n) @@ -163,7 +163,7 @@ def get_history(n, pid='current'): return get_pane(n, pid) except subprocess.CalledProcessError: return None -# %% ../nbs/00_core.ipynb 26 +# %% ../nbs/00_core.ipynb 28 default_cfg = asdict(ShellSageConfig()) def get_opts(**opts): cfg = get_cfg() @@ -171,7 +171,7 @@ def get_opts(**opts): if v is None: opts[k] = cfg.get(k, default_cfg.get(k)) return AttrDict(opts) -# %% ../nbs/00_core.ipynb 28 +# %% ../nbs/00_core.ipynb 30 def with_permission(action_desc): def decorator(func): @wraps(func) @@ -192,25 +192,25 @@ def wrapper(*args, **kwargs): return wrapper return decorator -# %% ../nbs/00_core.ipynb 29 +# %% ../nbs/00_core.ipynb 31 tools = [with_permission('ripgrep a search term')(rg), with_permission('View file/director')(view), with_permission('Create a file')(create), with_permission('Replace a string with another string')(str_replace), with_permission('Insert content into a file')(insert)] -# %% ../nbs/00_core.ipynb 31 +# %% ../nbs/00_core.ipynb 33 sps = {'default': sp, 'sassy': ssp} def get_sage(model, mode='default', search=False): return Chat(model=model, sp=sps[mode], tools=tools, search=search) -# %% ../nbs/00_core.ipynb 34 +# %% ../nbs/00_core.ipynb 36 def get_res(sage, q, opts): from litellm.types.utils import ModelResponseStream # lazy load # need to use stream=True to get search citations gen = sage(q, max_steps=10, stream=True, api_base=opts.api_base, api_key=opts.api_key) yield from accumulate(o.choices[0].delta.content or "" for o in gen if isinstance(o, ModelResponseStream)) -# %% ../nbs/00_core.ipynb 40 +# %% ../nbs/00_core.ipynb 42 class Log: id:int; timestamp:str; query:str; response:str; model:str; mode:str log_path = Path("~/.shell_sage/logs/").expanduser() @@ -220,7 +220,7 @@ def mk_db(): db.logs = db.create(Log) return db -# %% ../nbs/00_core.ipynb 43 +# %% ../nbs/00_core.ipynb 45 @call_parse def main( query: Param('The query to send to the LLM', str, nargs='+'), @@ -247,7 +247,7 @@ def main( raise Exception(f"{mode} is not valid. Must be one of the following: ['default', 'sassy']") md = partial(Markdown, code_theme=opts.code_theme, inline_code_lexer=opts.code_lexer, - inline_code_theme=opts.code_theme) + inline_code_theme=opts.code_theme) query = ' '.join(query) ctxt = '' if skip_system else _sys_info() @@ -259,14 +259,12 @@ def main( if history: ctxt += f'\n{history}\n' # Read from stdin if available - if not sys.stdin.isatty() and not IN_NOTEBOOK: - ctxt += f'\n\n{sys.stdin.read()}' + if not sys.stdin.isatty() and not IN_NOTEBOOK: ctxt += f'\n\n{sys.stdin.read()}' query = f'{ctxt}\n\n{query}\n' sage = get_sage(opts.model, mode, search=opts.search) - for res in get_res(sage, query, opts): - live.update(md(res), refresh=True) + for res in get_res(sage, query, opts): live.update(md(res), refresh=True) # Handle logging if the log flag is set if opts.log: @@ -276,10 +274,10 @@ def main( except KeyboardInterrupt: print("Interrupted.") -# %% ../nbs/00_core.ipynb 47 +# %% ../nbs/00_core.ipynb 49 def extract_cf(idx): return re.findall(r'```(\w+)?\n(.*?)\n```', mk_db().logs()[-1].response, re.DOTALL)[idx][1] -# %% ../nbs/00_core.ipynb 49 +# %% ../nbs/00_core.ipynb 51 @call_parse def extract( idx: int, # Index of code block to extract