diff --git a/nbs/00_core.ipynb b/nbs/00_core.ipynb index 547565a..bd4afe0 100644 --- a/nbs/00_core.ipynb +++ b/nbs/00_core.ipynb @@ -35,19 +35,33 @@ "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" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9d9615ce", + "metadata": {}, + "outputs": [], + "source": [ + "#| hide\n", + "from contextlib import contextmanager\n", + "from IPython.display import clear_output" ] }, { @@ -58,11 +72,62 @@ "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": "markdown", + "id": "257cdfa7", + "metadata": {}, + "source": [ + "Jupyter does work with rich.live.Live, this fixes it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "76acaf9c", + "metadata": {}, + "outputs": [], + "source": [ + "@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", @@ -452,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": {}, @@ -538,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, @@ -578,15 +644,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))" ] }, { @@ -598,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",
+       "
Hey! πŸ‘‹                                                                                                            \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",
-       "\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", - "\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", + "Hey! πŸ‘‹ \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": {}, @@ -636,8 +705,7 @@ } ], "source": [ - "opts=NS(api_base='', api_key='')\n", - "print(Markdown(get_res(ssage, 'Hi!', opts)))" + "print_md(get_res(ssage, 'Hi!', opts))" ] }, { @@ -649,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": {}, @@ -667,7 +735,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))" ] }, { @@ -679,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": {}, @@ -703,7 +775,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));" ] }, { @@ -779,79 +851,80 @@ " 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", + " # 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", - " # 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", + " # Read from stdin if available\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", - " # 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", - "\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): 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.\")" ] }, { "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": {}, @@ -859,7 +932,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)" ] }, { @@ -895,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, 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 9a4d648..55dc399 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 +# %% ../nbs/00_core.ipynb 5 console = Console() print = console.print # %% ../nbs/00_core.ipynb 6 +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 11 sp = '''You are ShellSage (ssage), a command-line teaching assistant created to help users learn and master shell commands and system administration. @@ -67,7 +77,7 @@ - Link to documentation with `man command_name` or `-h`/`--help` ''' -# %% ../nbs/00_core.ipynb 7 +# %% ../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. @@ -111,13 +121,13 @@ - Remember: The cake may be a lie, but the commands are always true ''' -# %% ../nbs/00_core.ipynb 9 +# %% ../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 11 +# %% ../nbs/00_core.ipynb 16 def _sys_info(): sys = co(['uname', '-a'], text=True).strip() ssys = f'{sys}' @@ -126,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 14 +# %% ../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 16 +# %% ../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 19 +# %% ../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 21 +# %% ../nbs/00_core.ipynb 26 def get_history(n, pid='current'): try: if pid=='current': return get_pane(n) @@ -153,7 +163,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 28 default_cfg = asdict(ShellSageConfig()) def get_opts(**opts): cfg = get_cfg() @@ -161,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 25 +# %% ../nbs/00_core.ipynb 30 def with_permission(action_desc): def decorator(func): @wraps(func) @@ -182,24 +192,25 @@ def wrapper(*args, **kwargs): return wrapper return decorator -# %% ../nbs/00_core.ipynb 26 +# %% ../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 28 +# %% ../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 31 +# %% ../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 - 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 42 class Log: id:int; timestamp:str; query:str; response:str; model:str; mode:str log_path = Path("~/.shell_sage/logs/").expanduser() @@ -209,7 +220,7 @@ def mk_db(): db.logs = db.create(Log) return db -# %% ../nbs/00_core.ipynb 39 +# %% ../nbs/00_core.ipynb 45 @call_parse def main( query: Param('The query to send to the LLM', str, nargs='+'), @@ -228,42 +239,45 @@ 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 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 45 +# %% ../nbs/00_core.ipynb 51 @call_parse def extract( idx: int, # Index of code block to extract