Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion packages/ragbits-chat/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# CHANGELOG

## Unreleased
- Autoreload support for UI during development (#900)
- Change auth backend from jwt to http-only cookie based authentication, add support for OAuth2 authentication (#867)

- Make `SummaryGenerator` optional in `ChatInterface` by providing a default Heuristic implementation.
- Refactor ragbits-client types to remove excessive use of any (#881)
- Split params into path params, query params in API client (#871)
Expand Down
60 changes: 60 additions & 0 deletions packages/ragbits-chat/src/ragbits/chat/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import logging
import os
import re
import tempfile
import time
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager, suppress
Expand Down Expand Up @@ -945,9 +946,68 @@ def _load_auth_backend(
def run(self, host: str = "127.0.0.1", port: int = 8000) -> None:
"""
Used for starting the API

Args:
host: Host to bind the API server to
port: Port to bind the API server to
"""
uvicorn.run(self.app, host=host, port=port)

@staticmethod
def run_with_reload(
host: str,
port: int,
chat_interface: str | None = None,
cors_origins: list[str] | None = None,
ui_build_dir: str | None = None,
debug_mode: bool = False,
auth_backend: str | None = None,
theme_path: str | None = None,
) -> None:
"""
Run the API server with auto-reload enabled for development.

This method creates a temporary Python file with the API configuration
that uvicorn can import and reload when code changes are detected.

Args:
host: Host to bind the API server to
port: Port to bind the API server to
chat_interface: Path to chat interface module
cors_origins: List of allowed CORS origins
ui_build_dir: Path to custom UI build directory
debug_mode: Enable debug mode
auth_backend: Path to authentication backend module
theme_path: Path to theme configuration file
"""
temp_file_path: Path | None = None
try:
with tempfile.NamedTemporaryFile(
mode="w", suffix=".py", delete=False, dir=".", prefix="_ragbits_app_"
) as temp_file:
temp_file_path = Path(temp_file.name)
temp_file.write(
f"""# Auto-generated file for ragbits reload mode - DO NOT EDIT
from ragbits.chat.api import RagbitsAPI

api = RagbitsAPI(
chat_interface={repr(chat_interface)},
cors_origins={repr(cors_origins)},
ui_build_dir={repr(ui_build_dir)},
debug_mode={repr(debug_mode)},
auth_backend={repr(auth_backend)},
theme_path={repr(theme_path)},
)
app = api.app
"""
)

module_name = temp_file_path.stem
uvicorn.run(f"{module_name}:app", host=host, port=port, reload=True)
finally:
if temp_file_path:
temp_file_path.unlink(missing_ok=True)

@staticmethod
def _convert_heroui_json_to_css(json_content: str) -> str:
"""Convert HeroUI JSON theme configuration to CSS variables."""
Expand Down
31 changes: 22 additions & 9 deletions packages/ragbits-chat/src/ragbits/chat/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,29 @@ def run(
"--theme",
help="Path to a HeroUI theme JSON file from heroui.com/themes",
),
reload: bool = typer.Option(False, "--reload", help="Enable auto-reload on code changes for debugging"),
) -> None:
"""
Run API service with UI demo
"""
api = RagbitsAPI(
chat_interface=chat_interface,
cors_origins=cors_origins,
ui_build_dir=ui_build_dir,
debug_mode=debug_mode,
auth_backend=auth,
theme_path=theme,
)
api.run(host=host, port=port)
if reload:
RagbitsAPI.run_with_reload(
host=host,
port=port,
chat_interface=chat_interface,
cors_origins=cors_origins,
ui_build_dir=ui_build_dir,
debug_mode=debug_mode,
auth_backend=auth,
theme_path=theme,
)
else:
api = RagbitsAPI(
chat_interface=chat_interface,
cors_origins=cors_origins,
ui_build_dir=ui_build_dir,
debug_mode=debug_mode,
auth_backend=auth,
theme_path=theme,
)
api.run(host=host, port=port)
43 changes: 43 additions & 0 deletions packages/ragbits-chat/tests/unit/test_api.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
from collections.abc import AsyncGenerator
from pathlib import Path
from typing import Literal
from unittest.mock import MagicMock, mock_open, patch

Expand Down Expand Up @@ -337,6 +338,48 @@ def test_run_method() -> None:
mock_run.assert_called_once_with(api.app, host="localhost", port=9000)


def test_run_with_reload_method() -> None:
"""Test the run_with_reload method creates temp file and starts uvicorn with reload."""

class TempFileCheck:
"""Helper class to verify temp file exists when uvicorn.run is called."""

existed_at_run_time: bool = False
temp_file_path: Path | None = None

@classmethod
def check_and_record(cls, app_path: str, **kwargs: dict) -> None:
module_name = app_path.split(":")[0]
cls.temp_file_path = Path(f"./{module_name}.py")
cls.existed_at_run_time = cls.temp_file_path.exists()

with patch("uvicorn.run", side_effect=TempFileCheck.check_and_record) as mock_run:
RagbitsAPI.run_with_reload(
host="localhost",
port=9000,
chat_interface="test:TestChat",
debug_mode=True,
)

# Verify uvicorn.run was called with reload=True and an import string
mock_run.assert_called_once()
call_args = mock_run.call_args
assert call_args[1]["reload"] is True
assert call_args[1]["host"] == "localhost"
assert call_args[1]["port"] == 9000
assert isinstance(call_args[0][0], str)
# Module name format: _ragbits_app_<random>:app
assert ":app" in call_args[0][0]
assert "_ragbits_app_" in call_args[0][0]

# Verify temp file existed when uvicorn.run was called
assert TempFileCheck.existed_at_run_time, "Temp file was not created before uvicorn.run"

# Verify temp file was cleaned up after
assert TempFileCheck.temp_file_path is not None
assert not TempFileCheck.temp_file_path.exists(), "Temp file was not cleaned up"


def test_login_endpoint(client: TestClient) -> None:
"""Test the login endpoint with valid credentials sets session cookie."""
response = client.post("/api/auth/login", json={"username": "testuser", "password": "testpass"})
Expand Down
16 changes: 8 additions & 8 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading