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
47 changes: 47 additions & 0 deletions docs/how-to/chatbots/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,52 @@ async def chat(self, message: str, history: ChatFormat, context: ChatContext) ->
yield self.create_text_response(chunk)
```

## Handling File Uploads

Ragbits Chat supports file uploads, allowing users to send files to your chatbot. To enable this feature, implement the `upload_handler` method in your `ChatInterface` subclass.

### Enable File Uploads

Define an async `upload_handler` method that accepts a `fastapi.UploadFile` object:

```python
from collections.abc import AsyncGenerator

from fastapi import UploadFile

from ragbits.chat.interface import ChatInterface
from ragbits.chat.interface.types import ChatContext, ChatResponse
from ragbits.core.prompt import ChatFormat


class MyChat(ChatInterface):
async def upload_handler(self, file: UploadFile) -> None:
"""
Handle file uploads.

Args:
file: The uploaded file (FastAPI UploadFile)
"""
# Read the file content
content = await file.read()
filename = file.filename

# Process the file (e.g., ingest into vector store, save to disk)
print(f"Received file: {filename}, size: {len(content)} bytes")

async def chat(
self,
message: str,
history: ChatFormat,
context: ChatContext,
) -> AsyncGenerator[ChatResponse, None]:
yield self.create_text_response(f"You said: {message}")
```

When this method is implemented, the chat interface will automatically show an attachment icon in the input bar.

> **Note**: The upload handler processes the file but does not directly return a response to the chat stream. The frontend receives a success status via the `/api/upload` endpoint. If you want to acknowledge the upload in the chat, the user typically sends a follow-up message, or you can store the uploaded file reference in state for later use.

## State Management

Ragbits Chat provides secure state management to maintain conversation context across requests. State data is automatically signed using HMAC to prevent tampering.
Expand Down Expand Up @@ -336,6 +382,7 @@ The API server exposes the following endpoints:
- `GET /api/config`: Returns UI configuration including feedback forms
- `POST /api/chat`: Accepts chat messages and returns streaming responses
- `POST /api/feedback`: Accepts feedback from the user
- `POST /api/upload`: Accepts file uploads (only available when `upload_handler` is implemented)

## Server Configuration

Expand Down
67 changes: 67 additions & 0 deletions examples/chat/upload_chat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""
Ragbits Chat Example: File Upload Chat

This example demonstrates how to use the `ChatInterface` with file upload capability.
It shows how to implement the `upload_handler` to process uploaded files.

To run the script, execute the following command:

```bash
ragbits api run examples.chat.upload_chat:UploadChat
```
"""

from collections.abc import AsyncGenerator

from fastapi import UploadFile

from ragbits.chat.interface import ChatInterface
from ragbits.chat.interface.types import ChatContext, ChatResponse
from ragbits.chat.interface.ui_customization import HeaderCustomization, UICustomization
from ragbits.core.prompt import ChatFormat


class UploadChat(ChatInterface):
"""An example ChatInterface that supports file uploads."""

ui_customization = UICustomization(
header=HeaderCustomization(title="Upload Chat Example", subtitle="Demonstrating file uploads", logo="📁"),
welcome_message=(
"Hello! I am a chat bot that can handle file uploads.\n\n"
"Click the attachment icon in the input bar to upload a file. "
"I will tell you the size and name of the file you uploaded."
),
)

async def chat(
self,
message: str,
history: ChatFormat,
context: ChatContext,
) -> AsyncGenerator[ChatResponse, None]:
"""
Simple echo chat that responds to text.
"""
yield self.create_text_response(f"You said: {message}")

async def upload_handler(self, file: UploadFile) -> None: # noqa: PLR6301
"""
Handle file uploads.

Args:
file: The uploaded file (FastAPI UploadFile)
"""
# Read the file content
content = await file.read()
file_size = len(content)
filename = file.filename

# In a real application, you might process the file, ingest it into a vector store, etc.
# Here we just print some info to the console.
print(f"Received file: {filename}, size: {file_size} bytes")

# Note: The upload_handler doesn't return a response to the chat stream directly.
# The frontend receives a success status.
# If you want to notify the user in the chat, the user would usually send a message
# mentioning they uploaded a file, or you could potentially trigger something else.
# Currently the flow is: UI uploads -> Backend handles -> UI gets 200 OK.
1 change: 1 addition & 0 deletions packages/ragbits-chat/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased
- Fix PostgreSQL conversation persistence by ensuring session flush after creating new conversation in SQL storage (#903)
- Add file upload ingestion support with `upload_handler` in `ChatInterface` and `/api/upload` endpoint in `RagbitsAPI`.
- 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)
Expand Down
68 changes: 41 additions & 27 deletions packages/ragbits-chat/src/ragbits/chat/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from typing import Any, cast

import uvicorn
from fastapi import FastAPI, HTTPException, Request, status
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
Expand Down Expand Up @@ -197,34 +197,23 @@ async def oauth2_callback(

return await self._handle_oauth2_callback(code, state, backend)

@self.app.post("/api/chat", response_class=StreamingResponse)
async def chat_message(
request: Request,
chat_request: ChatMessageRequest,
) -> StreamingResponse:
return await self._handle_chat_message(chat_request, request)

@self.app.post("/api/feedback", response_class=JSONResponse)
async def feedback(
request: Request,
feedback_request: FeedbackRequest,
) -> JSONResponse:
return await self._handle_feedback(feedback_request, request)
else:
@self.app.post("/api/chat", response_class=StreamingResponse)
async def chat_message(
request: Request,
chat_request: ChatMessageRequest,
) -> StreamingResponse:
return await self._handle_chat_message(chat_request, request)

@self.app.post("/api/chat", response_class=StreamingResponse)
async def chat_message(
request: Request,
chat_request: ChatMessageRequest,
) -> StreamingResponse:
return await self._handle_chat_message(chat_request, request)
@self.app.post("/api/feedback", response_class=JSONResponse)
async def feedback(
request: Request,
feedback_request: FeedbackRequest,
) -> JSONResponse:
return await self._handle_feedback(feedback_request, request)

@self.app.post("/api/feedback", response_class=JSONResponse)
async def feedback(
request: Request,
feedback_request: FeedbackRequest,
) -> JSONResponse:
return await self._handle_feedback(feedback_request, request)
@self.app.post("/api/upload", response_class=JSONResponse)
async def upload_file(file: UploadFile) -> JSONResponse:
return await self._handle_file_upload(file)

@self.app.get("/api/config", response_class=JSONResponse)
async def config() -> JSONResponse:
Expand Down Expand Up @@ -585,6 +574,31 @@ async def _handle_feedback(self, feedback_request: FeedbackRequest, request: Req
)
raise HTTPException(status_code=500, detail="Internal server error") from None

async def _handle_file_upload(self, file: UploadFile) -> JSONResponse:
"""
Handle file upload requests.

Args:
file: The uploaded file.

Returns:
JSONResponse with status.
"""
if self.chat_interface.upload_handler is None:
raise HTTPException(status_code=400, detail="File upload not supported")

try:
# Check if handler is async and call it
if asyncio.iscoroutinefunction(self.chat_interface.upload_handler):
await self.chat_interface.upload_handler(file)
else:
await asyncio.to_thread(self.chat_interface.upload_handler, file)

return JSONResponse(content={"status": "success", "filename": file.filename})
except Exception as e:
logger.error(f"File upload error: {e}")
raise HTTPException(status_code=500, detail=str(e)) from e

async def get_current_user_from_cookie(self, request: Request) -> User | None:
"""
Get current user from session cookie.
Expand Down
13 changes: 13 additions & 0 deletions packages/ragbits-chat/src/ragbits/chat/interface/_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from collections.abc import AsyncGenerator, Callable
from typing import Any

from fastapi import UploadFile

from ragbits.agents.tools.todo import Task
from ragbits.chat.interface.summary import HeuristicSummaryGenerator, SummaryGenerator
from ragbits.chat.interface.ui_customization import UICustomization
Expand Down Expand Up @@ -202,6 +204,16 @@ class ChatInterface(ABC):
* Text: Regular text responses streamed chunk by chunk
* References: Source documents used to generate the answer
* State updates: Updates to the conversation state

Attributes:
upload_handler: Optional async callback for handling file uploads.
Should accept an UploadFile parameter.

Example::

async def upload_handler(self, file: UploadFile) -> None:
content = await file.read()
# process content
"""

feedback_config: FeedbackConfig = FeedbackConfig()
Expand All @@ -211,6 +223,7 @@ class ChatInterface(ABC):
ui_customization: UICustomization | None = None
history_persistence: HistoryPersistenceStrategy | None = None
summary_generator: SummaryGenerator = HeuristicSummaryGenerator()
upload_handler: Callable[[UploadFile], Any] | None = None

def __init_subclass__(cls, **kwargs: dict) -> None:
"""Automatically apply the with_chat_metadata decorator to the chat method in subclasses."""
Expand Down

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

Large diffs are not rendered by default.

Loading