Skip to content

Commit

Permalink
bump pyproject inside aiconfig/gradio-notebook (#1242)
Browse files Browse the repository at this point in the history
bump pyproject inside aiconfig/gradio-notebook

cc @rossdanlm

Bump the pyproject.toml version number for gradio workbook. Republish
the package from within the aiconfig repo to validate that the repo
migration was successful

## Testplan

1. cd frontend -> yarn
2. gradio cc build --no-build-frontend

Test space:
https://huggingface.co/spaces/Ankush-LM/testwilldelete5

<img width="1511" alt="Screenshot 2024-02-14 at 1 52 30 AM"
src="https://github.com/lastmile-ai/aiconfig/assets/141073967/150fd747-a545-49df-b98b-6e2dcd0c6452">

<img width="1503" alt="Screenshot 2024-02-14 at 1 52 38 AM"
src="https://github.com/lastmile-ai/aiconfig/assets/141073967/f426a8d3-53be-4d0c-8255-3f1e979c5332">

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with
[ReviewStack](https://reviewstack.dev/lastmile-ai/aiconfig/pull/1242).
* __->__ #1242
* #1240
  • Loading branch information
Ankush-lastmile authored Feb 14, 2024
2 parents a03de4b + 2703ad5 commit 0e588a8
Show file tree
Hide file tree
Showing 38 changed files with 71,036 additions and 0 deletions.
27 changes: 27 additions & 0 deletions gradio-notebook/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*

# Dependency directories
node_modules/
jspm_packages/

# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local

.eggs/
*.egg-info
dist/
__pycache__/
*.py[cdio]
*$py.class
__tmp/*
19 changes: 19 additions & 0 deletions gradio-notebook/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Copyright (c) 2024 LastMile AI Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
35 changes: 35 additions & 0 deletions gradio-notebook/README-dev.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
## If running for the FIRST TIME and building on this

Otherwise, go directly to [dev instructions](https://github.com/lastmile-ai/gradio-workbook/blob/main/gradioworkbook/README-dev.md#follow-these-steps-if-you-are-developing-locally)

Just a heads up, the process for getting local development setup for the first time can be a bit tricky!

### Environment Setup

1. Install node.js & yarn
- Latest Node version from [website](https://nodejs.org/en/download/current) using the package installer
- After installing Node, open a new terminal to install [yarn](https://classic.yarnpkg.com/lang/en/docs/install/#mac-stable): `sudo npm install --global yarn`
2. Gradio uses `pip` and `python` for executing commands so you must symlink `python` and `pip` (aliasing is not enough) to `python3` and `pip3`:
- `python3 -m pip install --upgrade --force pip` ([source](https://stackoverflow.com/a/55494352))
- force `python` to be symlinked to `python3` ([instructions](https://stackoverflow.com/a/71957847))
3. Install Gradio itself: `pip install gradio`

### Setting up Gradio repo

1. Go to the project where this is defined. Ex: `~/Projects/gradio-workbook`
2. Delete this entire repo! `rm -rf gradioworkbook`
3. Gradio uses `pip` and `python` for executing commands so you must symlink `python` and `pip` (aliasing is not enough) to `python3` and `pip3`:
- `python3 -m pip install --upgrade --force pip` ([source](https://stackoverflow.com/a/55494352))
- force `python` to be symlinked to `python3` ([instructions](https://stackoverflow.com/a/71957847))
4. Run the command `gradio cc create GradioWorkbook --overwrite`. This will install the necessary setups and dependencies
5. `cd .. && rm -rf gradio-workbook`
6. Clone the repo again: `git clone https://github.com/lastmile-ai/gradio-workbook.git` (with Sapling: `sl clone https://github.com/lastmile-ai/gradio-workbook.git`)
7. `cd gradio-workbook` and follow the steps below!

## Follow these steps if you are developing locally!

```bash
cd gradioworkbook
pip install -r requirements.txt
cd frontend && yarn && yarn dev
```
22 changes: 22 additions & 0 deletions gradio-notebook/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Gradio Notebooks

[Gradio Notebook](https://huggingface.co/spaces/lastmileai/gradio-notebook-quickstart) is a Gradio custom component that creates a notebook playground on Hugging Face Spaces with only [8-lines of code](https://huggingface.co/spaces/lastmileai/gradio-notebook-quickstart/blob/main/app.py)!

You can use this in your Hugging Face space by adding `gradio-notebook` to your Space's [`requirements.txt`](https://huggingface.co/spaces/lastmileai/gradio-notebook-quickstart/blob/main/requirements.txt) file, and then the following lines to your [`app.py`](https://huggingface.co/spaces/lastmileai/gradio-notebook-quickstart/blob/main/app.py) file:

```python
import gradio as gr
from gradio_notebook import GradioNotebook

# AICONFIG_FILE_PATH also be empty or None if you don't have an AIConfig file!
AICONFIG_FILE_PATH = "./my_app.aiconfig.json"
with gr.Blocks() as demo:
GradioNotebook(config_path=AICONFIG_FILE_PATH)

demo.queue().launch()
```

Please see our [documentation page](https://aiconfig.lastmileai.dev/docs/gradio-notebook) for full details.

For the remaining commands for local development, please follow the
instructions from the [`README-dev.md`](https://github.com/lastmile-ai/gradio-workbook/blob/main/gradioworkbook/README-dev.md) file!
27 changes: 27 additions & 0 deletions gradio-notebook/backend/gradio_notebook/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from .aiconfig_manager import AIConfigManager
from .events import (
AddPromptEventData,
DeletePromptEventData,
EventHandler,
RunPromptEventData,
UpdatePromptEventData,
)
from .gradio_notebook import GradioNotebook
from .gradio_notebook_component import GradioNotebookComponent
from .utils import STOP_STREAMING_SIGNAL, QueueIterator, show_debug

event_data_classes = [
"AddPromptEventData",
"DeletePromptEventData",
"RunPromptEventData",
"UpdatePromptEventData",
]
__all__ = event_data_classes + [
"AIConfigManager",
"EventHandler",
"GradioNotebookComponent",
"GradioNotebook",
"QueueIterator",
"STOP_STREAMING_SIGNAL",
"show_debug",
]
259 changes: 259 additions & 0 deletions gradio-notebook/backend/gradio_notebook/aiconfig_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
"""Helper class to reference the AIConfigRuntime state
"""
import copy
import heapq
import time
from threading import Event
from typing import Any, Dict, Literal

from aiconfig import AIConfigRuntime, ModelParserRegistry
from aiconfig.registry import update_model_parser_registry_with_config_runtime
from aiconfig_extension_hugging_face import (
HuggingFaceAutomaticSpeechRecognitionRemoteInference,
HuggingFaceConversationalRemoteInference,
HuggingFaceImage2TextRemoteInference,
HuggingFaceText2ImageRemoteInference,
HuggingFaceText2SpeechRemoteInference,
HuggingFaceTextGenerationRemoteInference,
HuggingFaceTextSummarizationRemoteInference,
HuggingFaceTextTranslationRemoteInference,
)

from .session_data import SessionData
from .utils import (
EXCLUDE_OPTIONS,
get_validated_path,
load_user_parser_module,
show_debug,
)

# TODO (rossdanlm): Use os.path to get better relative path to file
DEFAULT_CONFIG_PATH: Literal = "my_app.aiconfig.json"
DEFAULT_PARSERS_PATH: Literal = "model_parsers.py"
DEFAULT_AICONFIG_SETTINGS: Dict[str, Any] = {
"name": "Gradio Notebook AIConfig",
# TODO (rossdanlm): Link this description to our README docs to help people get
# started
"description": "This is the AIConfig that is used for the current Gradio notebook",
}
NUM_MINUTES_BEFORE_DELETING_OLD_SESSIONS = 120 # 2 hours


class AIConfigManager:
"""
Manages the mapping of client session --> SessionData state so that
we can reference AIConfig from other classes without worrying about it
being stale. This also ensures that there are no circular dependencies for
classes that need to reference the AIConfigRuntime
Also will contain utility methods if needed
"""

session_data_map: Dict[str, SessionData]
session_id_lru_min_heap: list[(float, str)] # (update_time, session_id)
thread_events: dict[str, Event]

def __init__(self, config_path: str, parsers_path: str):
self._clear_default_model_parsers()
self._register_model_parsers(parsers_path)
self.session_data_map = {
"original": SessionData(
config=self._create_or_load_aiconfig(config_path),
update_time=time.time(),
)
}
self.session_id_lru_min_heap = [] # Do not store original in min_heap
self.thread_events = {}

def _clear_default_model_parsers(self):
"""
By default, there are a ton of non-hf models/parsers registered in the
ModelParserRegistry. We want to clear these out so that we can register
only the hf ones to start
"""
ModelParserRegistry.clear_registry()

def _register_model_parsers(self, parsers_path: str):
"""
Register the model parsers to use for the AIConfig.
By default, we register the main HuggingFace parsers.
TODO: Support user-provider parser registration
"""
automatic_speech_recognition = (
HuggingFaceAutomaticSpeechRecognitionRemoteInference()
)
AIConfigRuntime.register_model_parser(
automatic_speech_recognition, "Automatic Speech Recognition"
)

conversational = HuggingFaceConversationalRemoteInference()
AIConfigRuntime.register_model_parser(conversational, "Conversational")

image_to_text = HuggingFaceImage2TextRemoteInference()
AIConfigRuntime.register_model_parser(image_to_text, "Image-to-Text")

text_to_image = HuggingFaceText2ImageRemoteInference()
AIConfigRuntime.register_model_parser(text_to_image, "Text-to-Image")

text_to_speech = HuggingFaceText2SpeechRemoteInference()
AIConfigRuntime.register_model_parser(text_to_speech, "Text-to-Speech")

text_generation = HuggingFaceTextGenerationRemoteInference()
AIConfigRuntime.register_model_parser(text_generation, "Text Generation")

text_summarization = HuggingFaceTextSummarizationRemoteInference()
AIConfigRuntime.register_model_parser(text_summarization, "Summarization")

text_translation = HuggingFaceTextTranslationRemoteInference()
AIConfigRuntime.register_model_parser(text_translation, "Translation")

# By default, model parsers will also have their own ids registered. Remove those
# since we just want the task-based names registered
parsers = [
automatic_speech_recognition,
conversational,
image_to_text,
text_to_image,
text_to_speech,
text_generation,
text_summarization,
text_translation,
]

for parser in parsers:
ModelParserRegistry.remove_model_parser(parser.id())

# Lastly, register any user-provided model parsers, if applicable
if not parsers_path:
print(
f"Warning, no parsers_path was provided so using default path '{DEFAULT_PARSERS_PATH}' instead"
)
parsers_path = DEFAULT_PARSERS_PATH

self._load_user_parser_module_if_exists(parsers_path)

def _load_user_parser_module_if_exists(self, parsers_module_path: str) -> None:
try:
parsers_path = get_validated_path(parsers_module_path)
load_user_parser_module(parsers_path)
print(f"Loaded parsers module from {parsers_module_path}")
except Exception as e:
print(f"Failed to load parsers module: {e}")

def delete_session_id(self, session_id: str) -> None:
"""Delete the session_id from the session_data_map map"""
self.session_data_map.pop(session_id, None)

def get_config(self, session_id: str) -> AIConfigRuntime:
"""Get a config that is mapped to a session id"""
update_time = time.time()
if session_id not in self.session_data_map:
copied_config = copy.deepcopy(self.session_data_map["original"].config)
session_data = SessionData(config=copied_config, update_time=update_time)
self.session_data_map[session_id] = session_data
heapq.heappush(self.session_id_lru_min_heap, (update_time, session_id))

if show_debug():
print(f"{self.session_data_map.keys()=}")
update_times = [
(k, v.update_time) for k, v in self.session_data_map.items()
]
print(f"{update_times=}")

self.session_data_map[session_id].update_time = update_time
self.clear_old_session_ids()
return self.session_data_map[session_id].config

def get_config_json(self, session_id: str) -> dict[str, Any]:
"""Helper function to return the config in json dict format"""
return self.get_config(session_id).model_dump(exclude=EXCLUDE_OPTIONS)

def set_config(self, session_id: str, config: AIConfigRuntime):
"""Set the AIConfigRuntime for a session_id and also reset the update_time"""
session_data = SessionData(config=config, update_time=time.time())
self.session_data_map[session_id] = session_data

def _create_or_load_aiconfig(self, config_path: str) -> AIConfigRuntime:
"""
Create or load an AIConfigRuntime from a provide config_path.
This should only ever be called during init of this AIConfigManager class
"""
already_tried_default_filepath = False
if not config_path:
print(
f"Warning, no config_path was provided so using default path '{DEFAULT_CONFIG_PATH}' instead"
)
config_path = DEFAULT_CONFIG_PATH
already_tried_default_filepath = True

try:
config = AIConfigRuntime.load(config_path)
# TODO (rossdanlm): Test this also with malformed json format to see which error it produces and catch for that
except FileNotFoundError:
try:
if not already_tried_default_filepath:
print(
f"Warning, config_path '{config_path}' not found, trying default config_path '{DEFAULT_CONFIG_PATH}' instead..."
)
config_path = DEFAULT_CONFIG_PATH
config = AIConfigRuntime.load(config_path)
else:
raise FileNotFoundError()
except FileNotFoundError:
print(
f"Warning, config_path '{config_path}' not found, creating new AIConfig."
)
config_path = DEFAULT_CONFIG_PATH
config = AIConfigRuntime.create(**DEFAULT_AICONFIG_SETTINGS)
config.file_path = config_path
update_model_parser_registry_with_config_runtime(config)
return config

def get_models(self) -> list[str]:
"""Helper function to get the models from the ModelParserRegistry"""
return ModelParserRegistry.parser_ids()

def clear_old_session_ids(self):
"""
Clear out old session_ids from the session_id_lru_min_heap and session_data_map
"""
threshold_cutoff_time: float = (
time.time() - NUM_MINUTES_BEFORE_DELETING_OLD_SESSIONS * 60
)
if show_debug():
print(f"{threshold_cutoff_time=}\n")
while (
len(self.session_id_lru_min_heap) > 0
and self.session_id_lru_min_heap[0][0] # update_time
< threshold_cutoff_time
):
session_id = self.session_id_lru_min_heap[0][1]
should_delete = self.remove_lru_session_if_not_updated(
threshold_cutoff_time,
)
if should_delete:
self.session_data_map.pop(session_id, None)

def remove_lru_session_if_not_updated(self, threshold_cutoff_time: float) -> bool:
"""
Remove the least recently used session from the session_id_lru_min_heap
If the session has been updated since it was added to the min_heap, and the
update time is after the threshold cutoff, then we need to add it back to the
min_heap and check again
@return bool: Whether the session was removed or not
"""
old_update_time, session_id = heapq.heappop(self.session_id_lru_min_heap)
actual_update_time = self.session_data_map[session_id].update_time
if (
old_update_time < actual_update_time
and actual_update_time > threshold_cutoff_time
):
# This means that the update_time was updated since we added it to the
# min_heap, so we need to add it back to the min_heap and check again
heapq.heappush(
self.session_id_lru_min_heap, (actual_update_time, session_id)
)
return False
return True
Loading

0 comments on commit 0e588a8

Please sign in to comment.