Skip to content

Commit 5d91a5d

Browse files
authored
feat: add persistency component to ChatInterface and cleanup message/conversation ids (#556)
1 parent ad1eb30 commit 5d91a5d

File tree

24 files changed

+3625
-3264
lines changed

24 files changed

+3625
-3264
lines changed

examples/api/chat.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from ragbits.chat.interface import ChatInterface
1414
from ragbits.chat.interface.forms import FeedbackConfig, FeedbackForm, FormField
15-
from ragbits.chat.interface.types import ChatResponse, Message
15+
from ragbits.chat.interface.types import ChatContext, ChatResponse, Message
1616
from ragbits.core.llms import LiteLLM
1717

1818

@@ -50,7 +50,7 @@ async def chat(
5050
self,
5151
message: str,
5252
history: list[Message] | None = None,
53-
context: dict | None = None,
53+
context: ChatContext | None = None,
5454
) -> AsyncGenerator[ChatResponse, None]:
5555
"""
5656
Example implementation of the ChatInterface.

examples/api/offline_chat.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# /// script
2+
# requires-python = ">=3.10"
3+
# dependencies = [
4+
# "ragbits-chat",
5+
# ]
6+
# ///
7+
#
8+
# To run this example execute following CLI command:
9+
# ragbits api run examples.api.offline_chat:MyChat
10+
11+
import asyncio
12+
from collections.abc import AsyncGenerator
13+
14+
from ragbits.chat.interface import ChatInterface
15+
from ragbits.chat.interface.forms import FeedbackConfig, FeedbackForm, FormField
16+
from ragbits.chat.interface.types import ChatContext, ChatResponse, Message
17+
from ragbits.chat.persistence.file import FileHistoryPersistence
18+
19+
20+
class MyChat(ChatInterface):
21+
"""An offline example implementation of the ChatInterface that demonstrates different response types."""
22+
23+
history_persistence = FileHistoryPersistence(base_path="chat_history")
24+
25+
feedback_config = FeedbackConfig(
26+
like_enabled=True,
27+
like_form=FeedbackForm(
28+
title="Like Form",
29+
fields=[
30+
FormField(name="like_reason", type="text", required=True, label="Why do you like this?"),
31+
],
32+
),
33+
dislike_enabled=True,
34+
dislike_form=FeedbackForm(
35+
title="Dislike Form",
36+
fields=[
37+
FormField(
38+
name="issue_type",
39+
type="select",
40+
required=True,
41+
label="What was the issue?",
42+
options=["Incorrect information", "Not helpful", "Unclear", "Other"],
43+
),
44+
FormField(name="feedback", type="text", required=True, label="Please provide more details"),
45+
],
46+
),
47+
)
48+
49+
@staticmethod
50+
async def _generate_response(message: str) -> AsyncGenerator[str, None]:
51+
"""Simple response generator that simulates streaming text."""
52+
# Simple echo response with some additional text
53+
response = f"I received your message: '{message}'. This is an offline response."
54+
55+
# Simulate streaming by yielding character by character
56+
for char in response:
57+
yield char
58+
await asyncio.sleep(0.05) # Add small delay to simulate streaming
59+
60+
async def chat(
61+
self,
62+
message: str,
63+
history: list[Message] | None = None,
64+
context: ChatContext | None = None,
65+
) -> AsyncGenerator[ChatResponse, None]:
66+
"""
67+
Offline implementation of the ChatInterface.
68+
69+
Args:
70+
message: The current user message
71+
history: Optional list of previous messages in the conversation
72+
context: Optional context
73+
74+
Yields:
75+
ChatResponse objects containing different types of content:
76+
- Text chunks for the actual response
77+
- Reference documents used to generate the response
78+
"""
79+
# Example of yielding a reference
80+
yield self.create_reference(
81+
title="Offline Reference",
82+
content="This is an example reference document that might be relevant to your query.",
83+
url="https://example.com/offline-reference",
84+
)
85+
86+
# Generate and yield text chunks
87+
async for chunk in self._generate_response(message):
88+
yield self.create_text_response(chunk)

packages/ragbits-chat/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- Add persistance component to save chat interactions from ragbits-chat (#556)
6+
- Add conversation_id parameter to chat interface context (#556)
57
- Add uvicorn to dependencies (#578)
68
- Remove HeroUI Pro components (#557)
79

packages/ragbits-chat/src/ragbits/chat/api.py

Lines changed: 17 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import importlib
22
import json
33
import logging
4-
import uuid
54
from collections.abc import AsyncGenerator
65
from pathlib import Path
76
from typing import Any, Literal
@@ -15,7 +14,7 @@
1514
from pydantic import BaseModel, Field
1615

1716
from ragbits.chat.interface import ChatInterface
18-
from ragbits.chat.interface.types import ChatResponse, Message
17+
from ragbits.chat.interface.types import ChatContext, ChatResponse, Message
1918

2019
logger = logging.getLogger(__name__)
2120

@@ -112,33 +111,31 @@ async def chat_message(request: ChatMessageRequest) -> StreamingResponse:
112111
if not self.chat_interface:
113112
raise HTTPException(status_code=500, detail="Chat implementation is not initialized")
114113

115-
# Generate a unique message ID for this conversation message
116-
message_id = str(uuid.uuid4())
114+
# Convert request context to ChatContext
115+
chat_context = ChatContext(**request.context)
117116

118117
# Verify state signature if provided
119118
if "state" in request.context and "signature" in request.context:
120119
state = request.context["state"]
121120
signature = request.context["signature"]
122121
if not ChatInterface.verify_state(state, signature):
123-
logger.warning(f"Invalid state signature received for message {message_id}")
122+
logger.warning(f"Invalid state signature received for message {chat_context.message_id}")
124123
raise HTTPException(
125124
status_code=status.HTTP_400_BAD_REQUEST,
126125
detail="Invalid state signature",
127126
)
128-
# Remove the signature from context after verification
129-
del request.context["signature"]
130-
# Ensure context has a state field if not present
131-
elif "state" not in request.context:
132-
request.context["state"] = {}
127+
# Remove the signature from context after verification (it's already parsed into ChatContext)
133128

134129
# Get the response generator from the chat interface
135130
response_generator = self.chat_interface.chat(
136-
message=request.message, history=[msg.model_dump() for msg in request.history], context=request.context
131+
message=request.message,
132+
history=[msg.model_dump() for msg in request.history],
133+
context=chat_context,
137134
)
138135

139136
# Pass the generator to the SSE formatter
140137
return StreamingResponse(
141-
RagbitsAPI._chat_response_to_sse(response_generator, message_id, self.chat_interface),
138+
RagbitsAPI._chat_response_to_sse(response_generator),
142139
media_type="text/event-stream",
143140
)
144141

@@ -179,46 +176,23 @@ async def config() -> JSONResponse:
179176

180177
@staticmethod
181178
async def _chat_response_to_sse(
182-
responses: AsyncGenerator[ChatResponse], message_id: str, chat_interface: ChatInterface | None = None
179+
responses: AsyncGenerator[ChatResponse],
183180
) -> AsyncGenerator[str, None]:
184181
"""
185182
Formats chat responses into Server-Sent Events (SSE) format for streaming to the client.
186183
Each response is converted to JSON and wrapped in the SSE 'data:' prefix.
187184
188185
Args:
189186
responses: The chat response generator
190-
message_id: The unique identifier for this message
191-
chat_interface: The chat interface instance to use for verifying state (optional)
192187
"""
193-
# Send the message_id as the first SSE event
194-
data = json.dumps({"type": "message_id", "content": message_id})
195-
yield f"data: {data}\n\n"
196-
197188
async for response in responses:
198-
if response.type.value == "state_update":
199-
state_update = response.as_state_update()
200-
if state_update:
201-
# Verification is already done by the chat interface that created the state update
202-
data = json.dumps(
203-
{
204-
"type": "state_update",
205-
"content": {
206-
"state": state_update.state,
207-
"signature": state_update.signature,
208-
},
209-
}
210-
)
211-
yield f"data: {data}\n\n"
212-
else:
213-
data = json.dumps(
214-
{
215-
"type": response.type.value,
216-
"content": response.content
217-
if isinstance(response.content, str)
218-
else response.content.model_dump(),
219-
}
220-
)
221-
yield f"data: {data}\n\n"
189+
data = json.dumps(
190+
{
191+
"type": response.type.value,
192+
"content": response.content if isinstance(response.content, str) else response.content.model_dump(),
193+
}
194+
)
195+
yield f"data: {data}\n\n"
222196

223197
@staticmethod
224198
def _load_chat_interface(implementation: type[ChatInterface] | str) -> ChatInterface:

packages/ragbits-chat/src/ragbits/chat/history/stores/__init__.py

Lines changed: 0 additions & 4 deletions
This file was deleted.

packages/ragbits-chat/src/ragbits/chat/history/stores/base.py

Lines changed: 0 additions & 56 deletions
This file was deleted.

0 commit comments

Comments
 (0)