Add Panel Chat Pack (#703)

* first version of panel-chatbot

* add panel to library

* improve ui and make async

* update README

* add ticks

* rename to VectorStoreIndex

* simplify credits

* include extra_files

* fix anchor css

* update readme

* add png url

* fix test issue

* change to async

* moving panel import

* revert back

* remove run execution in base.py

* raise instead of warn

* Update base.py

---------

Co-authored-by: Andrei Fajardo <92402603+nerdai@users.noreply.github.com>
This commit is contained in:
Marc Skov Madsen 2023-12-08 02:16:17 +01:00 committed by GitHub
parent b15fc3c0bd
commit 43e607faff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 498 additions and 1 deletions

3
.gitignore vendored
View File

@ -8,4 +8,5 @@
.idea/
llama-hub.iml
llamahub/
img_cache/
img_cache/
.cache/

View File

@ -34,6 +34,12 @@
"author": "AdkSarsen",
"keywords": ["deeplake", "multimodal", "retriever"]
},
"PanelChatPack": {
"id": "llama_packs/panel_chatbot",
"author": "MarcSkovMadsen",
"keywords": ["panel", "chatbot", "github", "openai", "index"],
"extra_files": ["app.py", "llama_by_sophia_yang.png"]
},
"StreamlitChatPack": {
"id": "llama_packs/streamlit_chatbot",
"author": "carolinedlu",

View File

@ -0,0 +1,66 @@
# 🦙 Panel ChatBot Pack
Build a chatbot to talk to your Github repository.
Powered by LlamaIndex, OpenAI ChatGPT and [HoloViz Panel](https://panel.holoviz.org/reference/chat/ChatInterface.html).
![Panel Chat Bot](panel_chatbot.png)
## 💁‍♀️ Explanation
This template
- Downloads and indexes a Github repository using the the `llama_index` [`GithubRepositoryReader`](https://llamahub.ai/l/github_repo). The default repository is [holoviz/panel](https://github.com/holoviz/panel).
- Creates a [VectorStoreIndex](https://docs.llamaindex.ai/en/stable/changes/deprecated_terms.html#VectorStoreIndex) powered chat engine that will retrieve context from that data to respond to each user query.
- Creates a Panel [`ChatInterface`](https://panel.holoviz.org/reference/chat/ChatInterface.html) UI that will stream each answer from the chat engine.
## 🖥️ CLI Usage
You can download llamapacks directly using `llamaindex-cli`, which comes installed with the `llama-index` python package:
```bash
llamaindex-cli download-llamapack PanelChatPack --download-dir ./panel_chat_pack
```
You can then inspect the files at `./panel_chat_pack` and use them as a template for your own project!
To run the app directly, use in your terminal:
```bash
export OPENAI_API_KEY="sk-..."
export GITHUB_TOKEN='...'
panel serve ./panel_chat_pack/base.py
```
As an alternative to `panel serve`, you can run
```bash
python ./panel_chat_pack/base.py
```
## 🎓 Learn More
- [`GithubRepositoryReader`](https://llamahub.ai/l/github_repo)
- [`VectorStoreIndex`](https://docs.llamaindex.ai/en/stable/changes/deprecated_terms.html#VectorStoreIndex)
- [Panel Chat Components](https://panel.holoviz.org/reference/index.html#chat)
- [Panel Chat Examples](https://github.com/holoviz-topics/panel-chat-examples)
## 👍 Credits
- [Marc Skov Madsen](https://twitter.com/MarcSkovMadsen) for creating the template
- [Sophia Yang](https://twitter.com/sophiamyang) for creating the cute LLama image.
## 📈 Potential Improvements
- [ ] Improved Multi-user support
- [ ] Loading queue: Users should not be able to download the same repository at the same time.
- [ ] Service Context
- [ ] Enable users to define the service context including `model`, `temperature` etc.
- [ ] Better loading experience
- [ ] Let the chat assistant show (more fine-grained) status messages. And provide more status changes
- [ ] Focus on the streaming text
- [ ] The streaming text is not always in focus. I believe its a matter of adjusting the `auto_scroll_limit` limit.
- [ ] Fix minor CSS issues
- [ ] See the `CSS_FIXES_TO_BE_UPSTREAMED_TO_PANEL` variable in the code
- [ ] Repo Manager
- [ ] Support using multiple repos. For example the full HoloViz suite

View File

@ -0,0 +1,380 @@
"""Provides a ChatBot UI for a Github Repository. Powered by Llama Index and Panel"""
import os
import pickle
from pathlib import Path
import nest_asyncio
import panel as pn
import param
from llama_index import VectorStoreIndex, download_loader
from llama_hub.github_repo import GithubClient, GithubRepositoryReader
# needed because both Panel and GithubRepositoryReader starts up the ioloop
nest_asyncio.apply()
CACHE_PATH = Path(".cache/panel_chatbot")
CACHE_PATH.mkdir(parents=True, exist_ok=True)
CHAT_GPT_LOGO = "https://upload.wikimedia.org/wikipedia/commons/thumb/0/04/ChatGPT_logo.svg/512px-ChatGPT_logo.svg.png"
CHAT_GPT_URL = "https://chat.openai.com/"
LLAMA_INDEX_LOGO = (
"https://cdn-images-1.medium.com/max/280/1*_mrG8FG_LiD23x0-mEtUkw@2x.jpeg"
)
PANEL_LOGO = {
"default": "https://panel.holoviz.org/_static/logo_horizontal_light_theme.png",
"dark": "https://panel.holoviz.org/_static/logo_horizontal_dark_theme.png",
}
GITHUB_LOGO = "https://github.githubassets.com/assets/GitHub-Mark-ea2971cee799.png"
GITHUB_URL = "https://github.com/"
LLAMA_INDEX_URL = "https://www.llamaindex.ai/"
PANEL_URL = "https://panel.holoviz.org/index.html"
GITHUB_COPILOT_LOGO = (
"https://plugins.jetbrains.com/files/17718/447537/icon/pluginIcon.svg"
)
INDEX_NOT_LOADED = "No repository loaded"
INDEX_LOADED = "Repository loaded"
LOADING_EXISTING_DOCS = "Loading existing docs"
LOADING_NEW_DOCS = "Downloading documents"
LOADING_EXISTING_INDEX = "Loading existing index"
LOADING_NEW_INDEX = "Creating index"
CUTE_LLAMA = Path(__file__).parent / "llama_by_sophia_yang.png"
CUTE_LLAMA_URL = "https://x.com/sophiamyang/status/1729810715467252080?s=20"
pn.chat.ChatMessage.default_avatars.update(
{
"assistant": GITHUB_COPILOT_LOGO,
"user": "🦙",
}
)
pn.chat.ChatMessage.show_reaction_icons = False
ACCENT = "#ec4899"
CSS_FIXES_TO_BE_UPSTREAMED_TO_PANEL = """
#sidebar {
padding-left: 5px !important;
background-color: var(--panel-surface-color);
}
.pn-wrapper {
height: calc( 100vh - 150px);
}
.bk-active.bk-btn-primary {border-color: var(--accent-fill-active)}
.bk-btn-primary:hover {border-color: var(--accent-fill-hover)}
.bk-btn-primary {border-color: var(--accent-fill-rest)}
a {color: var(--accent-fill-rest) !important;}
a:hover {color: var(--accent-fill-hover) !important;}
"""
def _split_and_clean(cstext):
return cstext.split(",")
class IndexLoader(pn.viewable.Viewer):
"""The IndexLoader enables the user to interactively create a VectorStoreIndex from a
github repository of choice"""
value: VectorStoreIndex = param.ClassSelector(class_=VectorStoreIndex)
status = param.String(constant=True, doc="A status message")
owner: str = param.String(
default="holoviz", doc="The repository owner. For example 'holoviz'"
)
repo: str = param.String(
default="panel", doc="The repository name. For example 'panel'"
)
filter_directories: str = param.String(
default="examples,docs,panel",
label="Folders",
doc="A comma separated list of folders to include. For example 'examples,docs,panel'",
)
filter_file_extensions: str = param.String(
default=".py,.md,.ipynb",
label="File Extensions",
doc="A comma separated list of file extensions to include. For example '.py,.md,.ipynb'",
)
_load = param.Event(
label="LOAD",
doc="Loads the repository index from the cache if it exists and otherwise from scratch",
)
_reload = param.Event(
default=False,
label="RELOAD ALL",
doc="Loads the repository index from scratch",
)
def __init__(self):
super().__init__()
if self.index_exists:
pn.state.execute(self.load)
else:
self._update_status(INDEX_NOT_LOADED)
self._layout = pn.Column(
self.param.owner,
self.param.repo,
self.param.filter_directories,
self.param.filter_file_extensions,
pn.pane.HTML(self.github_url),
pn.widgets.Button.from_param(
self.param._load,
button_type="primary",
disabled=self._is_loading,
loading=self._is_loading,
),
pn.widgets.Button.from_param(
self.param._reload,
button_type="primary",
button_style="outline",
disabled=self._is_loading,
loading=self._is_loading,
),
pn.pane.Markdown("### Status", margin=(3, 5)),
pn.pane.Str(self.param.status),
)
def __panel__(self):
return self._layout
@property
def _unique_id(self):
uid = (
self.owner
+ self.repo
+ self.filter_directories
+ self.filter_file_extensions
)
uid = uid.replace(",", "").replace(".", "")
return uid
@property
def _cached_docs_path(self):
return CACHE_PATH / f"docs_{self._unique_id}.pickle"
@property
def _cached_index_path(self):
return CACHE_PATH / f"index_{self._unique_id}.pickle"
async def _download_docs(self):
download_loader("GithubRepositoryReader")
github_client = GithubClient(os.getenv("GITHUB_TOKEN"))
filter_directories = _split_and_clean(self.filter_directories)
filter_file_extensions = _split_and_clean(self.filter_file_extensions)
loader = GithubRepositoryReader(
github_client,
owner=self.owner,
repo=self.repo,
filter_directories=(
filter_directories,
GithubRepositoryReader.FilterType.INCLUDE,
),
filter_file_extensions=(
filter_file_extensions,
GithubRepositoryReader.FilterType.INCLUDE,
),
verbose=True,
concurrent_requests=10,
)
return loader.load_data(branch="main")
async def _get_docs(self):
docs_path = self._cached_docs_path
index_path = self._cached_index_path
if docs_path.exists():
self._update_status(LOADING_EXISTING_DOCS)
with docs_path.open("rb") as f:
return pickle.load(f)
self._update_status(LOADING_NEW_DOCS)
docs = await self._download_docs()
with docs_path.open("wb") as f:
pickle.dump(docs, f, pickle.HIGHEST_PROTOCOL)
if index_path.exists():
index_path.unlink()
return docs
async def _create_index(self, docs):
return VectorStoreIndex.from_documents(docs, use_async=True)
async def _get_index(self, index):
index_path = self._cached_index_path
if index_path.exists():
self._update_status(LOADING_EXISTING_INDEX)
with index_path.open("rb") as f:
return pickle.load(f)
self._update_status(LOADING_NEW_INDEX)
index = await self._create_index(index)
with index_path.open("wb") as f:
pickle.dump(index, f, pickle.HIGHEST_PROTOCOL)
return index
@param.depends("status")
def _is_loading(self):
return self.status not in [INDEX_LOADED, INDEX_NOT_LOADED]
@param.depends("status")
def _is_not_loading(self):
return self.status in [INDEX_LOADED, INDEX_NOT_LOADED]
@param.depends("_load", watch=True)
async def load(self):
"""Loads the repository index either from the cache or by downloading from
the repository"""
self._update_status("Loading ...")
self.value = None
docs = await self._get_docs()
self.value = await self._get_index(docs)
self._update_status(INDEX_LOADED)
@param.depends("_reload", watch=True)
async def reload(self):
self._update_status("Deleteing cached index ...")
if self._cached_docs_path.exists():
self._cached_docs_path.unlink()
if self._cached_index_path.exists():
self._cached_index_path.unlink()
await self.load()
def _update_status(self, text):
with param.edit_constant(self):
self.status = text
print(text)
@param.depends("owner", "repo")
def github_url(self):
"""Returns a html string with a link to the github repository"""
text = f"{self.owner}/{self.repo}"
href = f"https://github.com/{text}"
return f"<a href='{href}' target='_blank'>{text}</a>"
@property
def index_exists(self):
"""Returns True if the index already exists"""
return self._cached_docs_path.exists() and self._cached_index_path.exists()
def powered_by():
"""Returns a component describing the frameworks powering the chat ui"""
params = {"height": 40, "sizing_mode": "fixed", "margin": (0, 10)}
return pn.Column(
pn.pane.Markdown("### AI Powered By", margin=(10, 5, 10, 0)),
pn.Row(
pn.pane.Image(LLAMA_INDEX_LOGO, link_url=LLAMA_INDEX_URL, **params),
pn.pane.Image(CHAT_GPT_LOGO, link_url=CHAT_GPT_URL, **params),
pn.pane.Image(PANEL_LOGO[pn.config.theme], link_url=PANEL_URL, **params),
align="center",
),
)
async def chat_component(index: VectorStoreIndex, index_loader: IndexLoader):
"""Returns the chat component powering the main area of the application"""
if not index:
return pn.Column(
pn.chat.ChatMessage(
"You are a now a *GitHub Repository assistant*.",
user="System",
),
pn.chat.ChatMessage(
"Please **load a GitHub Repository** to start chatting with me. This can take from seconds to minutes!",
user="Assistant",
),
)
chat_engine = index.as_chat_engine(chat_mode="context", verbose=True)
async def generate_response(contents, user, instance):
response = await chat_engine.astream_chat(contents)
text = ""
async for token in response.async_response_gen():
text += token
yield text
chat_interface = pn.chat.ChatInterface(
callback=generate_response,
sizing_mode="stretch_both",
)
chat_interface.send(
pn.chat.ChatMessage(
"You are a now a *GitHub Repository Assistant*.", user="System"
),
respond=False,
)
chat_interface.send(
pn.chat.ChatMessage(
f"Hello! you can ask me anything about {index_loader.github_url()}.",
user="Assistant",
),
respond=False,
)
return chat_interface
def settings_components(index_loader: IndexLoader):
"""Returns a list of the components to add to the sidebar"""
return [
pn.pane.Image(
CUTE_LLAMA,
height=250,
align="center",
margin=(10, 5, 25, 5),
link_url=CUTE_LLAMA_URL,
),
"## Github Repository",
index_loader,
powered_by(),
]
def create_chat_ui():
"""Returns the Chat UI"""
pn.extension(
sizing_mode="stretch_width", raw_css=[CSS_FIXES_TO_BE_UPSTREAMED_TO_PANEL]
)
index_loader = IndexLoader()
pn.state.location.sync(
index_loader,
parameters={
"owner": "owner",
"repo": "repo",
"filter_directories": "folders",
"filter_file_extensions": "file_extensions",
},
)
bound_chat_interface = pn.bind(
chat_component, index=index_loader.param.value, index_loader=index_loader
)
return pn.template.FastListTemplate(
title="Chat with GitHub",
sidebar=settings_components(index_loader),
main=[bound_chat_interface],
accent=ACCENT,
main_max_width="1000px",
main_layout=None,
)
if pn.state.served:
create_chat_ui().servable()

View File

@ -0,0 +1,40 @@
"""Provides the PanelChatPack"""
import os
from typing import Any, Dict
from llama_index.llama_pack.base import BaseLlamaPack
ENVIRONMENT_VARIABLES = [
"GITHUB_TOKEN",
"OPENAI_API_KEY",
]
class PanelChatPack(BaseLlamaPack):
"""Panel chatbot pack."""
def get_modules(self) -> Dict[str, Any]:
"""Get modules."""
return {}
def run(self, *args: Any, **kwargs: Any) -> Any:
"""Run the pipeline."""
for variable in ENVIRONMENT_VARIABLES:
if variable not in os.environ:
raise ValueError("%s environment variable is not set", variable)
import panel as pn
if __name__ == "__main__":
# 'pytest tests' will fail if app is imported elsewhere
from app import create_chat_ui
pn.serve(create_chat_ui)
elif __name__.startswith("bokeh"):
from app import create_chat_ui
create_chat_ui().servable()
else:
print(
"To serve the Panel ChatBot please run this file with 'panel serve' or 'python'"
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 KiB

View File

@ -0,0 +1,4 @@
llama-hub
nest-asyncio
openai
panel