diff --git a/README.md b/README.md index 2d303e4..4544e93 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,11 @@ ICortex is a [Jupyter kernel](https://jupyter-client.readthedocs.io/en/latest/ke https://user-images.githubusercontent.com/2453968/196814906-1a0de2a1-27a7-4aec-a960-0eb21fbe2879.mp4 +TODO: Prompts are given using the %prompt magic now, update the video accordingly + It is ... -- a drop-in replacement for the IPython kernel. Prompts start with a forward slash `/`—otherwise the line is treated as regular Python code. +- a drop-in replacement for the IPython kernel. Prompts can be executed with the [magic commands](https://ipython.readthedocs.io/en/stable/interactive/magics.html) `%prompt` or `%p` for short. - an interface for [Natural Language Programming](https://en.wikipedia.org/wiki/Natural-language_programming) interface—prompts written in plain English automatically generate Python code which can then be executed globally. - interactive—install missing packages directly, decide whether to execute the generated code or not, and so on, directly in the Jupyter Notebook cell. - open source and fully extensible—if you think we are missing a model or an API, you can request it by creating an issue, or implement it yourself by subclassing `ServiceBase` under [`icortex/services`](icortex/services). @@ -40,7 +42,7 @@ icortex init Alternatively, you can initialize directly in a Jupyter Notebook ([instructions on how to start JupyterLab](https://jupyterlab.readthedocs.io/en/stable/getting_started/starting.html)): ``` -//init +%icortex init ``` The shell will then instruct you step by step and create a configuration file `icortex.toml` in the current directory. @@ -65,10 +67,10 @@ You can also try out different services e.g. OpenAI's Codex API, if you have acc ### Executing prompts -To execute a prompt with ICortex, use the `/` character (forward slash, also used to denote division) as a prefix. Copy and paste the following prompt into a cell and try to run it: +To execute a prompt with ICortex, use the `%prompt` [magic command](https://ipython.readthedocs.io/en/stable/interactive/magics.html) (or `%p` for short) as a prefix. Copy and paste the following prompt into a cell and try to run it: ``` -/print Hello World. Then print the Fibonacci numbers till 100 +%p print Hello World. Then print the Fibonacci numbers till 100 ``` Depending on the response, you should see an output similar to the following: @@ -87,7 +89,7 @@ Hello World. You can also specify variables or options with command line flags, e.g. to auto-install packages, auto-execute the returned code and so on. To see the complete list of variables for your chosen service, run: ``` -/help +%help ``` ### Using ICortex CLI @@ -106,7 +108,7 @@ icortex service help ### Accessing ICortex CLI inside Jupyter -You can still access the `icortex` CLI in a Jupyter Notebook or shell by using the prefix `//`. For example running the following in the terminal switches to a local HuggingFace model: +You can still access the `icortex` CLI in a Jupyter Notebook or shell by using the magic command `%icortex`. For example running the following in the terminal switches to a local HuggingFace model: ``` icortex service set huggingface @@ -115,7 +117,7 @@ icortex service set huggingface To do the same in a Jupyter Notebook, you can run ``` -//service set huggingface +%icortex service set huggingface ``` in a cell, which initializes and switches to the new service directly in your Jupyter session. diff --git a/docs/source/specification.rst b/docs/source/specification.rst new file mode 100644 index 0000000..f1e1d1a --- /dev/null +++ b/docs/source/specification.rst @@ -0,0 +1,5 @@ + +ICortex Specification +===================== + +Some stuff \ No newline at end of file diff --git a/icortex/__init__.py b/icortex/__init__.py index dbe77a8..f10c9cd 100644 --- a/icortex/__init__.py +++ b/icortex/__init__.py @@ -1,4 +1,4 @@ -from icortex.kernel import ICortexKernel, print_help, get_icortex_kernel +from icortex.kernel import ICortexKernel, print_service_help, get_icortex_kernel import icortex.services import importlib.metadata diff --git a/icortex/cli.py b/icortex/cli.py index 98dbcb7..970c73d 100644 --- a/icortex/cli.py +++ b/icortex/cli.py @@ -20,7 +20,7 @@ def get_parser(prog=None): # Initialize ICortex # ###################### - # //init + # icortex init parser_init = subparsers.add_parser( "init", help="Initialize ICortex configuration in the current directory", @@ -42,7 +42,7 @@ def get_parser(prog=None): # Shell related commands # ########################## - # //shell + # icortex shell parser_shell = subparsers.add_parser( "shell", help="Start ICortex shell", @@ -53,7 +53,7 @@ def get_parser(prog=None): # Help # ######## - # //help + # icortex help parser_help = subparsers.add_parser( "help", help="Print help", @@ -64,7 +64,7 @@ def get_parser(prog=None): # Service related commands # ############################ - # //service + # icortex service parser_service = subparsers.add_parser( "service", help="Set and configure code generation services", @@ -75,7 +75,7 @@ def get_parser(prog=None): required=True, ) - # //service set + # icortex service set parser_service_commands_set = parser_service_commands.add_parser( "set", help="Set the service to be used for code generation", @@ -87,21 +87,21 @@ def get_parser(prog=None): help="Name of the service to be used for code generation", ) - # //service show + # icortex service show parser_service_commands_show = parser_service_commands.add_parser( "show", help="Show current service", add_help=False, ) - # //service help + # icortex service help parser_service_commands_help = parser_service_commands.add_parser( "help", - help="Print help for //service", + help="Print help for the current service", add_help=False, ) - # //service set-var + # icortex service set-var parser_service_commands_set_var = parser_service_commands.add_parser( "set-var", help="Set a variable for the current service", @@ -117,7 +117,7 @@ def get_parser(prog=None): help="New value for the variable", ) - # //service init + # icortex service init # Used to re-spawn the config dialog if some config for the service # already exists parser_service_commands_init = parser_service_commands.add_parser( @@ -198,7 +198,7 @@ def main(argv=None, prog=None, kernel=None): def eval_cli(prompt: str): argv = shlex.split(prompt) try: - return main(argv=argv, prog="//") + return main(argv=argv, prog=r"%icortex") except SystemExit: return diff --git a/icortex/context.py b/icortex/context.py index 9fb03b4..213e852 100644 --- a/icortex/context.py +++ b/icortex/context.py @@ -48,8 +48,12 @@ def _check_init(self): self._dict = self.scope[DEFAULT_HISTORY_VAR] - def get_dict(self): - return deepcopy(self._dict) + def get_dict(self, omit_last_cell=False): + ret = deepcopy(self._dict) + if omit_last_cell: + if len(ret["cells"]) > 0: + del ret["cells"][-1] + return ret def add_code(self, code: str, outputs: t.List[t.Any]): self._check_init() diff --git a/icortex/helper.py b/icortex/helper.py index dd258a7..1bc5f0b 100644 --- a/icortex/helper.py +++ b/icortex/helper.py @@ -8,11 +8,16 @@ def unescape(s) -> str: def is_prompt(input: str) -> bool: - return input.strip()[0] == "/" + return input.strip().split()[0] in [ + r"%p", + r"%prompt", + r"%%prompt", + r"%%prompt", + ] def is_cli(input: str) -> bool: - return input.strip()[:2] == "//" + return input.strip().split()[0] == r"%icortex" def escape_quotes(s: str) -> str: @@ -20,11 +25,14 @@ def escape_quotes(s: str) -> str: def extract_prompt(input: str) -> str: - return input.strip()[1:].strip() + tokens = input.strip().split(" ", 1) + if len(tokens) == 1: + return "" + else: + return tokens[1] -def extract_cli(input: str) -> str: - return input.strip()[2:].strip() +extract_cli = extract_prompt def yes_no_input(message: str, default=True) -> bool: diff --git a/icortex/kernel/__init__.py b/icortex/kernel/__init__.py index fbb0aa3..b90ef94 100644 --- a/icortex/kernel/__init__.py +++ b/icortex/kernel/__init__.py @@ -2,6 +2,7 @@ # https://jupyter-client.readthedocs.io/en/latest/wrapperkernels.html # https://github.com/jupyter/jupyter/wiki/Jupyter-kernels +from logging import warning import shlex import typing as t from icortex.config import ICortexConfig @@ -9,24 +10,26 @@ from icortex.context import ICortexHistory from ipykernel.ipkernel import IPythonKernel +from IPython import InteractiveShell from traitlets.config.configurable import SingletonConfigurable from icortex.helper import ( - extract_cli, - is_cli, - is_prompt, - extract_prompt, escape_quotes, yes_no_input, highlight_python, ) -from icortex.services import ServiceBase, ServiceInteraction +from icortex.services import ServiceBase, ServiceInteraction, get_available_services from icortex.pypi import install_missing_packages, get_missing_modules from icortex.defaults import * import importlib.metadata __version__ = importlib.metadata.version("icortex") +INIT_SERVICE_MSG = ( + r"No service selected. Run `%icortex service init ` to initialize a service. Candidates: " + + ", ".join(get_available_services()) +) + class ICortexKernel(IPythonKernel, SingletonConfigurable): implementation = "ICortex" @@ -41,6 +44,7 @@ class ICortexKernel(IPythonKernel, SingletonConfigurable): "codemirror_mode": {"name": "ipython", "version": 3}, } banner = "ICortex: Generate Python code from natural language prompts using large language models" + shell: InteractiveShell def __init__(self, **kwargs): @@ -48,80 +52,14 @@ def __init__(self, **kwargs): self.service = None scope = self.shell.user_ns self.history = ICortexHistory(scope) + from icortex.magics import load_ipython_extension - async def do_execute( - self, - input_, - silent, - store_history=True, - user_expressions=None, - allow_stdin=True, - ): - self._forward_input(allow_stdin) - - try: - if is_cli(input_): - prompt = extract_cli(input_) - prompt = escape_quotes(prompt) - eval_cli(prompt) - code = "" - elif is_prompt(input_): - prompt = extract_prompt(input_) - prompt = escape_quotes(prompt) - - if self.service is None: - conf = ICortexConfig(DEFAULT_ICORTEX_CONFIG_PATH) - conf.set_kernel(self) - success = conf.set_service() - - if success: - service_interaction = self.eval_prompt(prompt) - code = service_interaction.get_code() - - # TODO: Store output once #12 is implemented - self.history.add_prompt( - input_, [], service_interaction.to_dict() - ) - else: - print( - "No service selected. Run `//service init ` to initialize a service." - ) - code = "" - else: - service_interaction = self.eval_prompt(prompt) - code = service_interaction.get_code() - # TODO: Store output once #12 is implemented - self.history.add_prompt(input_, [], service_interaction.to_dict()) - - else: - code = input_ - # TODO: Store output once #12 is implemented - self.history.add_code(code, []) - - finally: - self._restore_input() - - # TODO: KeyboardInterrupt does not kill coroutines, fix - # Until then, try not to use Ctrl+C while a cell is executing - return await IPythonKernel.do_execute( - self, - code, - silent, - store_history=store_history, - user_expressions=user_expressions, - allow_stdin=allow_stdin, - ) + load_ipython_extension(self.shell) def set_service(self, service: t.Type[ServiceBase]): self.service = service return True - # def set_icortex_service(config_path=DEFAULT_ICORTEX_CONFIG_PATH): - # kernel = get_icortex_kernel() - # if kernel is not None: - # return ICortexConfig(DEFAULT_ICORTEX_CONFIG_PATH).set_service() - # return False - def _run_dialog( self, code: str, @@ -192,17 +130,61 @@ def _run_dialog( # still_missing_modules=still_missing_modules, ) + def _check_service(self): + if self.service is None: + conf = ICortexConfig(DEFAULT_ICORTEX_CONFIG_PATH) + conf.set_kernel(self) + success = conf.set_service() + return success + else: + return True + + def print_service_help(self): + if self._check_service(): + self.service.prompt_parser.print_help() + else: + print(INIT_SERVICE_MSG) + + def prompt(self, input_: str): + prompt = escape_quotes(input_) + if self._check_service(): + service_interaction = self.eval_prompt(prompt) + code = service_interaction.get_code() + # Execute generated code + self.shell.run_cell( + code, + store_history=False, + silent=False, + cell_id=self.shell.execution_count, + ) + # Get the output from InteractiveShell.history_manager. + # run_cell should be called with store_history=False in order for + # self.shell.execution_count to match with the respective output + outputs = [] + try: + if self.shell.execution_count in self.shell.history_manager.output_hist_reprs: + output = self.shell.history_manager.output_hist_reprs[self.shell.execution_count] + outputs.append(output) + except: + warning("There was an issue with saving execution output to history") + + # Store history with the input and corresponding output + self.history.add_prompt(input_, outputs, service_interaction.to_dict()) + else: + print(INIT_SERVICE_MSG) + + def cli(self, input_: str): + prompt = escape_quotes(input_) + eval_cli(prompt) + def eval_prompt(self, prompt_with_args: str) -> ServiceInteraction: # Print help if the user has typed `/help` argv = shlex.split(prompt_with_args) args = self.service.prompt_parser.parse_args(argv) - prompt = " ".join(args.prompt) - if prompt == "help": - return "from icortex import print_help\nprint_help()" # Otherwise, generate with the prompt response = self.service.generate( - prompt_with_args, context=self.history.get_dict() + prompt_with_args, context=self.history.get_dict(omit_last_cell=True) ) # TODO: Account for multiple response values code_: str = response[0]["text"] @@ -225,10 +207,10 @@ def get_icortex_kernel() -> ICortexKernel: return ICortexKernel.instance() -def print_help() -> None: +def print_service_help() -> None: icortex_kernel = get_icortex_kernel() if icortex_kernel is not None: - icortex_kernel.service.prompt_parser.print_help() + icortex_kernel.print_service_help() if __name__ == "__main__": diff --git a/icortex/kernel/__main__.py b/icortex/kernel/__main__.py index b04d39a..28b7c07 100644 --- a/icortex/kernel/__main__.py +++ b/icortex/kernel/__main__.py @@ -1,3 +1,4 @@ +import IPython from icortex.kernel import ICortexKernel from ipykernel.kernelapp import IPKernelApp diff --git a/icortex/magics.py b/icortex/magics.py new file mode 100644 index 0000000..44707d7 --- /dev/null +++ b/icortex/magics.py @@ -0,0 +1,43 @@ +from IPython.core.magic import ( + Magics, + magics_class, + line_magic, + cell_magic, + line_cell_magic, +) + +from icortex.kernel import get_icortex_kernel, print_service_help + + +@magics_class +class ICortexMagics(Magics): + @line_magic + def help(self, line): + print_service_help() + return + + @line_magic + def icortex(self, line): + kernel = get_icortex_kernel() + return kernel.cli(line) + + @line_cell_magic + def prompt(self, line, cell=None): + if cell is None: + return self._prompt(line) + else: + return self._prompt(line + " " + cell) + + @line_cell_magic + def p(self, line, cell=None): + "Alias for prompt()" + return self.prompt(line, cell=cell) + + def _prompt(self, input_): + kernel = get_icortex_kernel() + return kernel.prompt(input_) + + +def load_ipython_extension(ipython): + "Register magics for ICortex" + ipython.register_magics(ICortexMagics) diff --git a/icortex/services/service_base.py b/icortex/services/service_base.py index d0df8a7..af76f0f 100644 --- a/icortex/services/service_base.py +++ b/icortex/services/service_base.py @@ -118,7 +118,7 @@ def __init__(self, config: t.Dict): required=False, help="Do not print the generated code.", ) - self.prompt_parser.usage = "/your prompt goes here [-e] [-r] [-i] [-p] ..." + self.prompt_parser.usage = "%p your prompt goes here [-e] [-r] [-i] [-p] ..." self.prompt_parser.description = self.description