Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ licenses.txt
*.pkl
*.mar
*.torchscript
*.db
*.sqlite
*.sqlite3
**/.ipynb_checkpoints
**/dist/
**/checkpoints/
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ All necessary details are provided in the comments at the top of each script.
| [Upload Chat Interface](/examples/chat/upload_chat.py) | [ragbits-chat](/packages/ragbits-chat) | Example of how to use the `ChatInterface` with file upload support via `upload_handler`. |
| [File Explorer Agent](/examples/chat/file_explorer_agent.py) | [ragbits-chat](/packages/ragbits-chat) | Secure file management agent with path validation and confirmation for all file operations within a restricted directory. |
| [Code Planner](/examples/chat/code_planner.py) | [ragbits-chat](/packages/ragbits-chat) | Example of how to use the `ChatInterface` with planning tools for code architecture tasks. |
| [Shared Chat Interface](/examples/chat/shared_chat.py) | [ragbits-chat](/packages/ragbits-chat) | Example of how to use the `ChatInterface` with conversation sharing via `SQLSharePersistence` and `SQLHistoryPersistence`. |

### Agents (`examples/agents/`)

Expand Down
124 changes: 124 additions & 0 deletions examples/chat/shared_chat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
r"""
Ragbits Chat Example: Conversation Sharing

This example demonstrates how to enable conversation sharing between authenticated
users by wiring `SQLSharePersistence` into `RagbitsAPI` alongside
`SQLHistoryPersistence`.

By default it uses SQLite for both stores so the example runs with no external
dependencies. Set ``USE_POSTGRES=1`` (and the optional ``PG_*`` env vars) to
point share persistence at a Postgres instance instead, e.g.:

```bash
docker run --rm -d --name ragbits-pg \
-e POSTGRES_USER=ragbits -e POSTGRES_PASSWORD=ragbits \
-e POSTGRES_DB=ragbits -p 5432:5432 postgres:16
USE_POSTGRES=1 uv run examples/chat/shared_chat.py
```

To run the script, execute the following command:

```bash
uv run examples/chat/shared_chat.py
```

Log in as `alice` / `alice123` and `bob` / `bob123` in two browser windows to try it.
"""

# /// script
# requires-python = ">=3.10"
# dependencies = [
# "ragbits-chat[sql]",
# "ragbits-core[sqlite,postgres]",
# ]
# ///

import os
import tempfile
from collections.abc import AsyncGenerator
from pathlib import Path

from sqlalchemy.ext.asyncio import create_async_engine

from ragbits.chat.api import RagbitsAPI
from ragbits.chat.auth.backends import ListAuthenticationBackend
from ragbits.chat.auth.session_store import InMemorySessionStore
from ragbits.chat.interface import ChatInterface
from ragbits.chat.interface.types import ChatContext, ChatResponse
from ragbits.chat.persistence.share import SQLSharePersistence
from ragbits.chat.persistence.sql import SQLHistoryPersistence
from ragbits.core.llms import LiteLLM
from ragbits.core.prompt import ChatFormat
from ragbits.core.storage.connections import PostgresConnection, SQLiteConnection

DB_FILE = Path(tempfile.gettempdir()) / "ragbits_shared_chat.db"
SHARES_DB_FILE = Path(tempfile.gettempdir()) / "ragbits_shared_chat_shares.db"


class SharedChat(ChatInterface):
"""An example ChatInterface with SQLite history so conversations can be shared."""

conversation_history = True

def __init__(self) -> None:
self.llm = LiteLLM(model_name="gpt-4o-mini")
self.history_persistence = SQLHistoryPersistence(create_async_engine(f"sqlite+aiosqlite:///{DB_FILE}"))

async def chat(
self,
message: str,
history: ChatFormat,
context: ChatContext,
) -> AsyncGenerator[ChatResponse, None]:
"""Stream a reply from the LLM; persisted history is owned by `context.user`."""
async for chunk in self.llm.generate_streaming([*history, {"role": "user", "content": message}]):
yield self.create_text_response(chunk)


def get_auth_backend() -> ListAuthenticationBackend:
"""Factory for a simple username/password backend with two demo users."""
users = [
{
"user_id": "alice",
"username": "alice",
"password": "alice123",
"email": "alice@example.com",
"full_name": "Alice",
"roles": ["user"],
},
{
"user_id": "bob",
"username": "bob",
"password": "bob123",
"email": "bob@example.com",
"full_name": "Bob",
"roles": ["user"],
},
]
return ListAuthenticationBackend(users=users, session_store=InMemorySessionStore())


def get_share_persistence() -> SQLSharePersistence:
"""Factory for `SQLSharePersistence` backed by `ragbits.core.storage`.

Defaults to a local SQLite file. Set ``USE_POSTGRES=1`` (with optional
``PG_*`` overrides) to use a PostgreSQL connection instead.
"""
if os.environ.get("USE_POSTGRES"):
connection = PostgresConnection(
host=os.environ.get("PG_HOST", "localhost"),
port=int(os.environ.get("PG_PORT", "5432")),
database=os.environ.get("PG_DB", "ragbits"),
user=os.environ.get("PG_USER", "ragbits"),
password=os.environ.get("PG_PASS", "ragbits"),
)
return SQLSharePersistence(connection)
return SQLSharePersistence(SQLiteConnection(SHARES_DB_FILE))


if __name__ == "__main__":
RagbitsAPI(
SharedChat,
auth_backend=get_auth_backend(),
share_persistence=get_share_persistence(),
).run()
4 changes: 2 additions & 2 deletions package-lock.json

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

9 changes: 9 additions & 0 deletions packages/ragbits-chat/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

## Unreleased

### Added

- Add conversation sharing: new `SQLSharePersistence` SQL-backed store (`ragbits.chat.persistence.SQLSharePersistence`) and `share_persistence` parameter on `RagbitsAPI` that together expose REST endpoints for listing/sharing/unsharing/dismissing conversations and wire the `ShareButton`, shared-conversation sidebar entries and read-only conversation guard in the default UI.
- Extend `SQLHistoryPersistence` with `user_id` and `summary` columns on the `Conversation` model and `list_conversations`, `get_conversation_summaries`, `delete_conversation` and `get_conversation_owner` helpers used by the sharing endpoints. The `summary` column is updated automatically on every `save_interaction` that includes a `ConversationSummaryResponse`.
- Add `sharing` flag on `ConfigResponse` plus `ShareConversationRequest`, `ConversationShareResponse`, `ConversationMeta` and `ConversationDetail` schemas for the new sharing API.
- Add `examples/chat/shared_chat.py` demonstrating an end-to-end conversation sharing setup with `ListAuthenticationBackend`, `SQLHistoryPersistence` and a Postgres-backed `SQLSharePersistence`.

## 1.6.2 (2026-03-26)

- ragbits-agents updated to version v1.6.2
Expand Down Expand Up @@ -44,13 +51,15 @@
- Fix a UI build that had a hardcoded `127.0.0.1:8000` address

## 1.4.1 (2026-02-08)

### Changed

- ragbits-core updated to version v1.4.1

- Fix FastAPI installation by adding `standard` extras to ensure all required dependencies are included

## 1.4.0 (2026-02-04)

### Changed

- ragbits-core updated to version v1.4.0
Expand Down
64 changes: 63 additions & 1 deletion packages/ragbits-chat/src/ragbits/chat/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,17 @@
from fastapi import FastAPI, HTTPException, Request, UploadFile, status
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse, RedirectResponse, StreamingResponse
from fastapi.responses import (
HTMLResponse,
JSONResponse,
PlainTextResponse,
RedirectResponse,
StreamingResponse,
)
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel

from ragbits.chat.api_routes import build_conversations_router
from ragbits.chat.auth import AuthenticationBackend, User
from ragbits.chat.auth.backends import MultiAuthenticationBackend, OAuth2AuthenticationBackend
from ragbits.chat.auth.provider_config import get_provider_visual_config
Expand All @@ -39,6 +46,7 @@
ImageResponse,
OAuth2ProviderConfig,
)
from ragbits.chat.persistence.base import HistoryPersistenceStrategy, SharePersistenceStrategy
from ragbits.core.audit.metrics import record_metric
from ragbits.core.audit.metrics.base import MetricType
from ragbits.core.audit.traces import trace
Expand All @@ -48,6 +56,16 @@
logger = logging.getLogger(__name__)


def _supports_conversation_listing(history_persistence: HistoryPersistenceStrategy) -> bool:
"""Return True if the strategy overrides ``list_conversations`` from the base class.

Used to decide whether conversation-sharing routes can be registered.
"""
impl = type(history_persistence).list_conversations
base = HistoryPersistenceStrategy.list_conversations
return impl is not base


class RagbitsAPI:
"""
RagbitsAPI class for running API with Demo UI for testing purposes
Expand All @@ -61,6 +79,7 @@ def __init__(
debug_mode: bool = False,
auth_backend: AuthenticationBackend | type[AuthenticationBackend] | str | None = None,
theme_path: str | None = None,
share_persistence: SharePersistenceStrategy | None = None,
) -> None:
"""
Initialize the RagbitsAPI.
Expand All @@ -73,13 +92,17 @@ def __init__(
debug_mode: Flag enabling debug tools in the default UI
auth_backend: Authentication backend for user authentication. If None, no authentication required.
theme_path: Path to a JSON file containing HeroUI theme configuration from heroui.com/themes
share_persistence: Optional share persistence for conversation sharing. Requires auth_backend
and a HistoryPersistenceStrategy that supports ownership tracking (see
:meth:`HistoryPersistenceStrategy.get_conversation_owner`).
"""
self.chat_interface: ChatInterface = self._load_chat_interface(chat_interface)
self.dist_dir = Path(ui_build_dir) if ui_build_dir else Path(__file__).parent / "ui-build"
self.cors_origins = cors_origins or []
self.debug_mode = debug_mode
self.auth_backend = self._load_auth_backend(auth_backend)
self.theme_path = Path(theme_path) if theme_path else None
self.share_persistence = share_persistence

self.frontend_base_url = BASE_URL

Expand Down Expand Up @@ -269,6 +292,7 @@ async def config() -> JSONResponse:
oauth2_providers=oauth2_providers,
),
supports_upload=self.chat_interface.upload_handler is not None,
sharing=self.share_persistence is not None,
)

return JSONResponse(content=config_response.model_dump())
Expand Down Expand Up @@ -301,12 +325,50 @@ async def theme() -> PlainTextResponse:
logger.error(f"Error serving theme: {e}")
raise HTTPException(status_code=500, detail="Error loading theme") from e

if self.auth_backend:
self._setup_conversation_routes()

@self.app.get("/{full_path:path}", response_class=HTMLResponse)
async def root() -> HTMLResponse:
index_file = self.dist_dir / "index.html"
with open(str(index_file)) as file:
return HTMLResponse(content=file.read())

def _setup_conversation_routes(self) -> None:
"""Register routes for authenticated conversation management.

Requires ``auth_backend`` and a ``history_persistence`` that supports
ownership tracking (i.e. overrides
:meth:`HistoryPersistenceStrategy.list_conversations` to return the
user's conversations). When ``share_persistence`` is configured, sharing
endpoints are enabled as part of the same router.
"""
history_persistence = self.chat_interface.history_persistence
if history_persistence is None:
logger.warning("Conversation routes require a history_persistence strategy; routes disabled.")
return

if not _supports_conversation_listing(history_persistence):
logger.warning(
"history_persistence does not support conversation listing; conversation routes disabled. "
"Override HistoryPersistenceStrategy.list_conversations to enable conversation routes."
)
return

async def require_user(request: Request) -> User:
user = await self.require_authenticated_user(request)
if user is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication required")
return user

self.app.include_router(
build_conversations_router(
history_persistence=history_persistence,
require_user=require_user,
share_persistence=self.share_persistence,
)
)

@staticmethod
def _prepare_chat_context(
request: ChatMessageRequest,
Expand Down
10 changes: 10 additions & 0 deletions packages/ragbits-chat/src/ragbits/chat/api_routes/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""FastAPI routers that `RagbitsAPI` composes.

Extracting routes into routers keeps the main ``RagbitsAPI`` class focused on
wiring (middleware, dependencies, lifespan) and makes individual feature
groups importable and unit-testable on their own.
"""

from ragbits.chat.api_routes.conversations import build_conversations_router, build_share_router

__all__ = ["build_conversations_router", "build_share_router"]
Loading
Loading