Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

use backend engine in UI #443

Merged
merged 6 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 6 additions & 16 deletions ragna/_cli/core.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from pathlib import Path
from typing import Annotated, Optional

import httpx
import rich
import typer
import uvicorn
Expand Down Expand Up @@ -74,13 +73,12 @@ def deploy(
*,
config: ConfigOption = "./ragna.toml", # type: ignore[assignment]
api: Annotated[
Optional[bool],
bool,
typer.Option(
"--api/--no-api",
help="Deploy the Ragna REST API.",
show_default="True if UI is not deployed and otherwise check availability",
),
] = None,
] = True,
ui: Annotated[
bool,
typer.Option(
Expand All @@ -98,22 +96,14 @@ def deploy(
] = False,
open_browser: Annotated[
Optional[bool],
typer.Option(help="Open a browser when Ragna is deployed."),
typer.Option(
help="Open a browser when Ragna is deployed.",
show_default="value of ui / no-ui",
),
] = None,
) -> None:
def api_available() -> bool:
try:
return httpx.get(f"{config._url}/health").is_success
except httpx.ConnectError:
return False

if api is None:
api = not api_available() if ui else True

if not (api or ui):
raise Exception
elif ui and not api and not api_available():
raise Exception

if open_browser is None:
open_browser = ui
Expand Down
2 changes: 1 addition & 1 deletion ragna/deploy/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def server_available() -> bool:
app.include_router(make_api_router(engine), prefix="/api")

if ui:
panel_app = make_ui_app(config=config)
panel_app = make_ui_app(engine)
panel_app.serve_with_fastapi(app, endpoint="/ui")

@app.get("/", include_in_schema=False)
Expand Down
67 changes: 29 additions & 38 deletions ragna/deploy/_ui/api_wrapper.py
Original file line number Diff line number Diff line change
@@ -1,62 +1,53 @@
import json
import uuid
from datetime import datetime

import emoji
import httpx
import param

from ragna.core._utils import default_user
from ragna.deploy import _schemas as schemas
from ragna.deploy._engine import Engine

# The goal is this class is to provide ready-to-use functions to interact with the API
class ApiWrapper(param.Parameterized):
def __init__(self, api_url, **params):
self.api_url = api_url
self.client = httpx.AsyncClient(base_url=api_url, timeout=60)

super().__init__(**params)
class ApiWrapper(param.Parameterized):
def __init__(self, engine: Engine):
super().__init__()
self._user = default_user()
self._engine = engine

async def get_chats(self):
json_data = (await self.client.get("/chats")).raise_for_status().json()
json_data = [
chat.model_dump(mode="json")
for chat in self._engine.get_chats(user=self._user)
]
for chat in json_data:
chat["messages"] = [self.improve_message(msg) for msg in chat["messages"]]
return json_data

async def answer(self, chat_id, prompt):
async with self.client.stream(
"POST",
f"/chats/{chat_id}/answer",
json={"prompt": prompt, "stream": True},
) as response:
async for data in response.aiter_lines():
yield self.improve_message(json.loads(data))
async for message in self._engine.answer_stream(
user=self._user, chat_id=uuid.UUID(chat_id), prompt=prompt
):
yield self.improve_message(message.model_dump(mode="json"))

async def get_components(self):
return (await self.client.get("/components")).raise_for_status().json()

# Upload and related functions
def upload_endpoints(self):
return {
"informations_endpoint": f"{self.api_url}/document",
}
return self._engine.get_components().model_dump(mode="json")

async def start_and_prepare(
self, name, documents, source_storage, assistant, params
):
response = await self.client.post(
"/chats",
json={
"name": name,
"documents": documents,
"source_storage": source_storage,
"assistant": assistant,
"params": params,
},
chat = self._engine.create_chat(
user=self._user,
chat_creation=schemas.ChatCreation(
name=name,
document_ids=[document.id for document in documents],
source_storage=source_storage,
assistant=assistant,
params=params,
),
)
chat = response.raise_for_status().json()

response = await self.client.post(f"/chats/{chat['id']}/prepare", timeout=None)
response.raise_for_status()

return chat["id"]
await self._engine.prepare_chat(user=self._user, id=chat.id)
return str(chat.id)

def improve_message(self, msg):
msg["timestamp"] = datetime.strptime(msg["timestamp"], "%Y-%m-%dT%H:%M:%S.%f")
Expand Down
14 changes: 7 additions & 7 deletions ragna/deploy/_ui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles

from ragna.deploy import Config
from ragna.deploy._engine import Engine

from . import js
from . import styles as ui
Expand All @@ -22,13 +22,13 @@


class App(param.Parameterized):
def __init__(self, *, api_url):
def __init__(self, engine: Engine):
super().__init__()

# Apply the design modifiers to the panel components
# It returns all the CSS files of the modifiers
self.css_filepaths = ui.apply_design_modifiers()
self.api_url = api_url
self._engine = engine

def get_template(self):
# A bit hacky, but works.
Expand Down Expand Up @@ -73,7 +73,7 @@ def get_template(self):
return template

def index_page(self):
api_wrapper = ApiWrapper(api_url=self.api_url)
api_wrapper = ApiWrapper(self._engine)

template = self.get_template()
main_page = MainPage(api_wrapper=api_wrapper, template=template)
Expand Down Expand Up @@ -131,13 +131,13 @@ def get_component_resource(path: str):
def serve_with_fastapi(self, app: FastAPI, endpoint: str):
self.add_panel_app(app, self.index_page, endpoint)

for dir in ["css", "imgs", "resources"]:
for dir in ["css", "imgs"]:
app.mount(
f"/{dir}",
StaticFiles(directory=str(Path(__file__).parent / dir)),
name=dir,
)


def app(*, config: Config) -> App:
return App(api_url=f"{config._url}/api")
def app(engine: Engine) -> App:
return App(engine)
35 changes: 20 additions & 15 deletions ragna/deploy/_ui/central_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,11 +189,11 @@ def on_click_chat_info_wrapper(self, event):
pills = "".join(
[
f"""<div class='chat_document_pill'>{d['name']}</div>"""
for d in self.current_chat["metadata"]["documents"]
for d in self.current_chat["documents"]
]
)

grid_height = len(self.current_chat["metadata"]["documents"]) // 3
grid_height = len(self.current_chat["documents"]) // 3

markdown = "\n".join(
[
Expand All @@ -202,14 +202,14 @@ def on_click_chat_info_wrapper(self, event):
f"<div class='pills_list'>{pills}</div><br />\n\n",
"----",
"**Source Storage**",
f"""<span>{self.current_chat['metadata']['source_storage']}</span>\n""",
f"""<span>{self.current_chat['source_storage']}</span>\n""",
"----",
"**Assistant**",
f"""<span>{self.current_chat['metadata']['assistant']}</span>\n""",
f"""<span>{self.current_chat['assistant']}</span>\n""",
"**Advanced configuration**",
*[
f"- **{key.replace('_', ' ').title()}**: {value}"
for key, value in self.current_chat["metadata"]["params"].items()
for key, value in self.current_chat["params"].items()
],
]
)
Expand Down Expand Up @@ -275,7 +275,7 @@ def get_user_from_role(self, role: Literal["system", "user", "assistant"]) -> st
elif role == "user":
return cast(str, self.user)
elif role == "assistant":
return cast(str, self.current_chat["metadata"]["assistant"])
return cast(str, self.current_chat["assistant"])
else:
raise RuntimeError

Expand All @@ -301,12 +301,15 @@ async def chat_callback(
message.clipboard_button.value = message.content_pane.object
message.assistant_toolbar.visible = True

except Exception:
except Exception as exc:
import traceback

yield RagnaChatMessage(
(
"Sorry, something went wrong. "
"If this problem persists, please contact your administrator."
),
# (
# "Sorry, something went wrong. "
# "If this problem persists, please contact your administrator."
# ),
"".join(traceback.format_exception(type(exc), exc, exc.__traceback__)),
role="system",
user=self.get_user_from_role("system"),
)
Expand Down Expand Up @@ -358,7 +361,7 @@ def header(self):

current_chat_name = ""
if self.current_chat is not None:
current_chat_name = self.current_chat["metadata"]["name"]
current_chat_name = self.current_chat["name"]

chat_name_header = pn.pane.HTML(
f"<p>{current_chat_name}</p>",
Expand All @@ -370,9 +373,9 @@ def header(self):
if (
self.current_chat is not None
and "metadata" in self.current_chat
and "documents" in self.current_chat["metadata"]
and "documents" in self.current_chat
):
doc_names = [d["name"] for d in self.current_chat["metadata"]["documents"]]
doc_names = [d["name"] for d in self.current_chat["documents"]]

# FIXME: Instead of setting a hard limit of 20 documents here, this should
# scale automatically with the width of page
Expand All @@ -385,7 +388,9 @@ def header(self):

chat_documents_pills.append(pill)

self.chat_info_button.name = f"{self.current_chat['metadata']['assistant']} | {self.current_chat['metadata']['source_storage']}"
self.chat_info_button.name = (
f"{self.current_chat['assistant']} | {self.current_chat['source_storage']}"
)

return pn.Row(
chat_name_header,
Expand Down
2 changes: 1 addition & 1 deletion ragna/deploy/_ui/left_sidebar.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def __panel__(self):
self.chat_buttons = []
for chat in self.chats:
button = pn.widgets.Button(
name=chat["metadata"]["name"],
name=chat["name"],
css_classes=["chat_button"],
)
button.on_click(lambda event, c=chat: self.on_click_chat_wrapper(event, c))
Expand Down
44 changes: 34 additions & 10 deletions ragna/deploy/_ui/modal_configuration.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from datetime import datetime, timedelta, timezone
from typing import AsyncIterator

import panel as pn
import param

from ragna.deploy import _schemas as schemas

from . import js
from . import styles as ui
from .components.file_uploader import FileUploader


def get_default_chat_name(timezone_offset=None):
Expand Down Expand Up @@ -82,15 +84,11 @@ def __init__(self, api_wrapper, **params):

self.api_wrapper = api_wrapper

upload_endpoints = self.api_wrapper.upload_endpoints()

self.chat_name_input = pn.widgets.TextInput.from_param(
self.param.chat_name,
)
self.document_uploader = FileUploader(
[], # the allowed documents are set in the model_section function
upload_endpoints["informations_endpoint"],
)
# FIXME: accept
self.document_uploader = pn.widgets.FileInput(multiple=True)

# Most widgets (including those that use from_param) should be placed after the super init call
self.cancel_button = pn.widgets.Button(
Expand All @@ -114,12 +112,38 @@ def __init__(self, api_wrapper, **params):

self.got_timezone = False

def did_click_on_start_chat_button(self, event):
if not self.document_uploader.can_proceed_to_upload():
async def did_click_on_start_chat_button(self, event):
if not self.document_uploader.value:
self.change_upload_files_label("missing_file")
else:
self.start_chat_button.disabled = True
self.document_uploader.perform_upload(event, self.did_finish_upload)
documents = self.api_wrapper._engine.register_documents(
user=self.api_wrapper._user,
document_registrations=[
schemas.DocumentRegistration(name=name)
for name in self.document_uploader.filename
],
)

if self.api_wrapper._engine.supports_store_documents:

def make_content_stream(data: bytes) -> AsyncIterator[bytes]:
async def content_stream() -> AsyncIterator[bytes]:
yield data

return content_stream()

await self.api_wrapper._engine.store_documents(
user=self.api_wrapper._user,
ids_and_streams=[
(document.id, make_content_stream(data))
for document, data in zip(
documents, self.document_uploader.value
)
],
)

await self.did_finish_upload(documents)

async def did_finish_upload(self, uploaded_documents):
# at this point, the UI has uploaded the files to the API.
Expand Down
Loading