Open Tina-Sterite opened 1 month ago
A change would be needed here:
Ensuring a file name is valid cross-platform is not trivial: https://stackoverflow.com/questions/295135/turn-a-string-into-a-valid-filename but at least removing colons seems like a good idea (as these can be also used for Jupyter drives).
I think removing colons, slashes, and backslashes would be a good start. Restricting filenames to just ASCII alphanumerics might be good, with some kind of fallback logic if the title has only non-ASCII characters (e.g., if it's in Chinese or Japanese).
I have the same problems with the "/generate" command.
All code content is generated in memory successfully, but fails to save to the file system. For complete tutorials and long code documents, it generates everything, but the save-to-file operation fails.
"I think removing colons, slashes, and backslashes would be a good
start."
I removed all colons, slashes, backslashes, commas, and all other punctuation. None of that works for me. The longer the prompt the more often it fails to save the generated content. I removed all punctuation and shortened the prompts to trivial lengths. Certain words seem to trigger the insertion of COLON characters in the filename, like "tutorial", "lessons", "detailed explanations", and other longish prompts.
MISSING DESIGN REQUIREMENT: Give the user the option to EXPLICITLY specify the output file name, and not let AI LLMs do that.
I tried hacking the code generate.py" from the chat_handlers folder for JupyterLab-AI in the site-packages directory,
" C:\ProgramData\Anaconda3\envs\JupyterLabAI_Tools\Lib\site-packages\jupyter_ai\chat_handlers\generate.py "
I added this function to the beginning of the file:
import re
def sanitize_filename(filename):
return re.sub(r'[\\/:*?"<>|]', '_', filename)
and then added the following code (in blue) to the _generate_notebook() function in generate.py.
async def _generate_notebook(self, prompt: str):
"""Generate a notebook and save to local disk"""
# create outline
outline = await generate_outline(prompt, llm=self.llm, verbose=True)
# Save the user input prompt, the description property is now LLM
generated. outline["prompt"] = prompt
if self.llm.allows_concurrency:
# fill the outline concurrently
await afill_outline(outline, llm=self.llm, verbose=True)
else:
# fill outline
await fill_outline(outline, llm=self.llm, verbose=True)
# create and write the notebook to disk
notebook = create_notebook(outline)
final_path = os.path.join(self.output_dir,
sanitize_filename(outline["title"])[:250] + ".ipynb") try:
sanitized_title = sanitize_filename(outline["title"])[:250]
final_path = os.path.join(self.output_dir, sanitized_title + ".ipynb")
nbformat.write(notebook, final_path)
except Exception as e: print(f"Failed to save the notebook to {final_path}: {str(e)}") raise
The code change did not fix the problem. I restarted the Jupyter Server to make sure the change was picked up from the back-end component for JupyterLabAI.
But now I get a security error. I have not set up a full development project. I simply added the code to convert non-valid characters in a Windows, MacOS, or Linux filepath. I am on Windows so I am most concerned about getting it working for Windows right now, but it needs to be done right for all platforms, not just a hack for one.
[W 2024-09-10 13:16:50.411 ServerApp] 404 GET /api/ai/chats?token=[secret] (5cc00c5dc7d04d669b976d2f569697c5@::1) 2.04ms referer=None [W 2024-09-10 13:16:51.164 ServerApp] 404 GET /api/ai/completion/inline?token=[secret] (5cc00c5dc7d04d669b976d2f569697c5@::1) 2.00ms referer=None
When I have time later to analyze this more fully, and fix it, I will submit a pull request. If someone has time to test this with a full development environment, I would appreciate it.
Thank you.
Best Regards,
Rich Lysakowski, Ph.D. Data Scientist, AI & Analytics Engineer, and Senior Business Systems Analyst 781-640-2048 mobile
On Tue, Sep 10, 2024 at 5:24 PM Jason Weill @.***> wrote:
I think removing colons, slashes, and backslashes would be a good start. Restricting filenames to just ASCII alphanumerics might be good, with some kind of fallback logic if the title has only non-ASCII characters (e.g., if it's in Chinese or Japanese).
— Reply to this email directly, view it on GitHub https://github.com/jupyterlab/jupyter-ai/issues/990#issuecomment-2342036033, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACHJVLZ4ZEB6BFDSAUTMAJLZV5PRFAVCNFSM6AAAAABN7FYN5CVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDGNBSGAZTMMBTGM . You are receiving this because you are subscribed to this thread.
import asyncio import os import time import traceback from pathlib import Path
import re
import os
def sanitize_filename(filename):
return re.sub(r'[\\/:*?"<>|]', '_', filename)
from typing import Dict, List, Optional, Type
import nbformat from jupyter_ai.chat_handlers import BaseChatHandler, SlashCommandRoutingType from jupyter_ai.models import HumanChatMessage from jupyter_ai_magics.providers import BaseProvider from langchain.chains import LLMChain from langchain.llms import BaseLLM from langchain.output_parsers import PydanticOutputParser from langchain.pydantic_v1 import BaseModel from langchain.schema.output_parser import BaseOutputParser from langchain_core.prompts import PromptTemplate
class OutlineSection(BaseModel): title: str content: str
class Outline(BaseModel): description: Optional[str] = None sections: List[OutlineSection]
class NotebookOutlineChain(LLMChain): """Chain to generate a notebook outline, with section titles and descriptions."""
@classmethod
def from_llm(
cls, llm: BaseLLM, parser: BaseOutputParser[Outline], verbose: bool
= False ) -> LLMChain: task_creation_template = ( "You are an AI that creates a detailed content outline for a Jupyter notebook on a given topic.\n" "{format_instructions}\n" "Here is a description of the notebook you will create an outline for: {description}\n" "Don't include an introduction or conclusion section in the outline, focus only on description and sections that will need code.\n" ) prompt = PromptTemplate( template=task_creation_template, input_variables=["description"], partial_variables={"format_instructions": parser.get_format_instructions()}, ) return cls(prompt=prompt, llm=llm, verbose=verbose)
async def generate_outline(description, llm=None, verbose=False): """Generate an outline of sections given a description of a notebook.""" parser = PydanticOutputParser(pydantic_object=Outline) chain = NotebookOutlineChain.from_llm(llm=llm, parser=parser, verbose=verbose) outline = await chain.apredict(description=description) outline = parser.parse(outline) return outline.dict()
class CodeImproverChain(LLMChain): """Chain to improve source code."""
@classmethod
def from_llm(cls, llm: BaseLLM, verbose: bool = False) -> LLMChain:
task_creation_template = (
"Improve the following code and make sure it is valid. Make
sure to return the improved code only - don't give an explanation of the improvements.\n" "{code}" ) prompt = PromptTemplate( template=task_creation_template, input_variables=[ "code", ], ) return cls(prompt=prompt, llm=llm, verbose=verbose)
class NotebookSectionCodeChain(LLMChain): """Chain to generate source code for a notebook section."""
@classmethod
def from_llm(cls, llm: BaseLLM, verbose: bool = False) -> LLMChain:
task_creation_template = (
"You are an AI that writes code for a single section of a
Jupyter notebook.\n" "Overall topic of the notebook: {description}\n" "Title of the notebook section: {title}\n" "Description of the notebok section: {content}\n" "Given this information, write all the code for this section and this section only." " Your output should be valid code with inline comments.\n" ) prompt = PromptTemplate( template=task_creation_template, input_variables=["description", "title", "content"], ) return cls(prompt=prompt, llm=llm, verbose=verbose)
class NotebookSummaryChain(LLMChain): """Chain to generate a short summary of a notebook."""
@classmethod
def from_llm(cls, llm: BaseLLM, verbose: bool = False) -> LLMChain:
task_creation_template = (
"Create a markdown summary for a Jupyter notebook with the
following content." " The summary should consist of a single paragraph.\n" "Content:\n{content}" ) prompt = PromptTemplate( template=task_creation_template, input_variables=[ "content", ], ) return cls(prompt=prompt, llm=llm, verbose=verbose)
class NotebookTitleChain(LLMChain): """Chain to generate the title of a notebook."""
@classmethod
def from_llm(cls, llm: BaseLLM, verbose: bool = False) -> LLMChain:
task_creation_template = (
"Create a short, few word, descriptive title for a Jupyter
notebook with the following content.\n" "Content:\n{content}\n" "Don't return anything other than the title." ) prompt = PromptTemplate( template=task_creation_template, input_variables=[ "content", ], ) return cls(prompt=prompt, llm=llm, verbose=verbose)
async def improve_code(code, llm=None, verbose=False): """Improve source code using an LLM.""" chain = CodeImproverChain.from_llm(llm=llm, verbose=verbose) improved_code = await chain.apredict(code=code) improved_code = "\n".join( [line for line in improved_code.split("\n") if not line.startswith("```")] ) return improved_code
async def generate_code(section, description, llm=None, verbose=False) -> None: """ Function that accepts a section and adds code under the "code" key when awaited. """ chain = NotebookSectionCodeChain.from_llm(llm=llm, verbose=verbose) code = await chain.apredict( description=description, title=section["title"], content=section["content"], ) improved_code = await improve_code(code, llm=llm, verbose=verbose) section["code"] = improved_code
async def generate_title(outline, llm=None, verbose: bool = False): """Generate a title of a notebook outline using an LLM.""" title_chain = NotebookTitleChain.from_llm(llm=llm, verbose=verbose) title = await title_chain.apredict(content=outline) title = title.strip() title = title.strip("'\"") outline["title"] = title
async def generate_summary(outline, llm=None, verbose: bool = False): """Generate a summary of a notebook using an LLM.""" summary_chain = NotebookSummaryChain.from_llm(llm=llm, verbose=verbose) summary = await summary_chain.apredict(content=outline) outline["summary"] = summary
async def fill_outline(outline, llm, verbose=False): """Generate title and content of a notebook sections using an LLM.""" shared_kwargs = {"outline": outline, "llm": llm, "verbose": verbose}
await generate_title(**shared_kwargs)
await generate_summary(**shared_kwargs)
for section in outline["sections"]:
await generate_code(section, outline["description"], llm=llm,
verbose=verbose)
async def afill_outline(outline, llm, verbose=False): """Concurrently generate title and content of notebook sections using an LLM.""" shared_kwargs = {"outline": outline, "llm": llm, "verbose": verbose}
all_coros = []
all_coros.append(generate_title(**shared_kwargs))
all_coros.append(generate_summary(**shared_kwargs))
for section in outline["sections"]:
all_coros.append(
generate_code(section, outline["description"], llm=llm,
verbose=verbose) ) await asyncio.gather(*all_coros)
def create_notebook(outline): """Create an nbformat Notebook object for a notebook outline.""" nbf = nbformat.v4 nb = nbf.new_notebook() nb["cells"].append(nbf.new_markdown_cell("# " + outline["title"])) nb["cells"].append(nbf.new_markdown_cell("## Introduction")) disclaimer = f"This notebook was created by Jupyter AI with the following prompt:\n\n> {outline['prompt']}" nb["cells"].append(nbf.new_markdown_cell(disclaimer)) nb["cells"].append(nbf.new_markdown_cell(outline["summary"]))
for section in outline["sections"][1:]:
nb["cells"].append(nbf.new_markdown_cell("## " + section["title"]))
for code_block in section["code"].split("\n\n"):
nb["cells"].append(nbf.new_code_cell(code_block))
return nb
class GenerateChatHandler(BaseChatHandler): id = "generate" name = "Generate Notebook" help = "Generate a Jupyter notebook from a text prompt" routing_type = SlashCommandRoutingType(slash_id="generate")
uses_llm = True
def __init__(self, log_dir: Optional[str], *args, **kwargs):
super().__init__(*args, **kwargs)
self.log_dir = Path(log_dir) if log_dir else None
self.llm = None
def create_llm_chain(
self, provider: Type[BaseProvider], provider_params: Dict[str, str]
):
unified_parameters = {
**provider_params,
**(self.get_model_parameters(provider, provider_params)),
}
llm = provider(**unified_parameters)
self.llm = llm
return llm
async def _generate_notebook(self, prompt: str):
"""Generate a notebook and save to local disk"""
# create outline
outline = await generate_outline(prompt, llm=self.llm, verbose=True)
# Save the user input prompt, the description property is now LLM
generated. outline["prompt"] = prompt
if self.llm.allows_concurrency:
# fill the outline concurrently
await afill_outline(outline, llm=self.llm, verbose=True)
else:
# fill outline
await fill_outline(outline, llm=self.llm, verbose=True)
# create and write the notebook to disk
notebook = create_notebook(outline)
final_path = os.path.join(self.output_dir,
sanitize_filename(outline["title"])[:250] + ".ipynb") try:
sanitized_title = sanitize_filename(outline["title"])[:250]
final_path = os.path.join(self.output_dir, sanitized_title + ".ipynb")
nbformat.write(notebook, final_path)
except Exception as e: print(f"Failed to save the notebook to {final_path}: {str(e)}") raise
return final_path
async def process_message(self, message: HumanChatMessage):
self.get_llm_chain()
# first send a verification message to user
response = "👍 Great, I will get started on your notebook. It may
take a few minutes, but I will reply here when the notebook is ready. In the meantime, you can continue to ask me other questions." self.reply(response, message)
final_path = await self._generate_notebook(prompt=message.body)
response = f"""🎉 I have created your notebook and saved it to the
location {final_path}. I am still learning how to create notebooks, so please review all code before running it.""" self.reply(response, message)
async def handle_exc(self, e: Exception, message: HumanChatMessage):
timestamp = time.strftime("%Y-%m-%d-%H.%M.%S")
default_log_dir = Path(self.output_dir) / "jupyter-ai-logs"
log_dir = self.log_dir or default_log_dir
log_dir.mkdir(parents=True, exist_ok=True)
log_path = log_dir / f"generate-{timestamp}.log"
with log_path.open("w") as log:
traceback.print_exc(file=log)
response = f"An error occurred while generating the notebook. The
error details have been saved to ./{log_path}
.\n\nTry running /generate
again, as some language models require multiple attempts before a notebook
is generated."
self.reply(response, message)
Message ID: @.***>
I think removing colons, slashes, and backslashes would be a good start. Restricting filenames to just ASCII alphanumerics might be good, with some kind of fallback logic if the title has only non-ASCII characters (e.g., if it's in Chinese or Japanese).
As a simple and deterministic fix, can we just add an option to the /generate command?
I am thinking it could be something like one of these two suggestions below:
/generate:filename /generate --filename="my_notebook_name.ipynb"
This is to let the user provide an explicit "notebook_filename" for the NB to override the AI-generated filename. The AI-generated filename is often problematic, too long, or not very accurate.
I don't know how to implement this myself yet, because I don't understand the code for the slash commands implementation yet.
Description
I've been using the /generate command to create Jupyter notebooks from text prompts, but it seems to be generating filenames that contain colons (:). This is causing issues, especially on Windows systems where colons are not allowed in filenames. Could you please provide a fix or a workaround to prevent colons from being included in the generated filenames?
Reproduce
This issue is random.
There are 2 examples in this screenshot.. here only a portion of the file name (probably before the colon) is displayed.
Here are some examples of prompts and file names that are rendered: prompt: /generate how to format markdown cells in jupyter notebooks result: I have created your notebook and saved it to the location C:\Users\steri\Desktop\GitHub\JupyterLabAI\Mastering Markdown in Jupyter Notebooks: A Comprehensive Guide.ipynb
prompt: /generate formatting markdown cells in jupyter notebooks result: I have created your notebook and saved it to the location C:\Users\steri\Desktop\GitHub\JupyterLabAI\Markdown Formatting in Jupyter Notebooks: A Comprehensive Guide.ipynb
prompt: /generate formatting markdown cells in jupyter notebooks. name this notebook markdown.ipynb result: SUCCESS! Third time was a charm! I have created your notebook and saved it to the location C:\Users\steri\Desktop\GitHub\JupyterLabAI\Mastering Markdown in Jupyter Notebooks.ipynb
prompt: /generate using Langchain Extraction via the Langchain REST API with with detailed code examples and detailed explanations result: I have created your notebook and saved it to the location C:\Users\steri\Desktop\GitHub\JupyterLabAI\Langchain Text Extraction: API Integration & Advanced Techniques.ipynb
prompt: /generate Langchain REST API Langchain Extraction result: SUCCESS! I have created your notebook and saved it to the location C:\Users\steri\Desktop\GitHub\JupyterLabAI\Extracting Data from REST APIs with Langchain in Python.ipynb
I'll paste the details from the server below..
Expected behavior
I'm expecting files to be generated without a colon in the name. Every jupyter notebook file generated without a colon in the name is usable.
Context
Operating System and version: Microsoft Windows 11 Pro 10.0.22621 Build 22621
Browser and version: Chrome Version 128.0.6613.113
JupyterLab version: Version 4.2.5
Troubleshoot Output
Command Line Output
Browser Output