ragbits-chat 1.4.0.dev202509220622__py3-none-any.whl → 1.4.0.dev202511290233__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ragbits/chat/__init__.py +44 -0
- ragbits/chat/_utils.py +2 -2
- ragbits/chat/api.py +115 -19
- ragbits/chat/cli.py +6 -0
- ragbits/chat/client/conversation.py +24 -19
- ragbits/chat/interface/_interface.py +79 -45
- ragbits/chat/interface/summary.py +82 -0
- ragbits/chat/interface/types.py +582 -48
- ragbits/chat/persistence/base.py +2 -1
- ragbits/chat/persistence/file.py +2 -1
- ragbits/chat/persistence/sql.py +6 -3
- ragbits/chat/providers/model_provider.py +30 -3
- ragbits/chat/ui-build/assets/{AuthGuard-B3JOY-uC.js → AuthGuard-BTxv1Dj_.js} +1 -1
- ragbits/chat/ui-build/assets/ChatHistory-C7A2uiIQ.js +2 -0
- ragbits/chat/ui-build/assets/ChatOptionsForm-x2g6iEPU.js +1 -0
- ragbits/chat/ui-build/assets/FeedbackForm-BPycljsV.js +1 -0
- ragbits/chat/ui-build/assets/{Login-B2kPNK-w.js → Login-BWGQs3cn.js} +1 -1
- ragbits/chat/ui-build/assets/{LogoutButton-Bspy5P1_.js → LogoutButton-gJF91B1i.js} +1 -1
- ragbits/chat/ui-build/assets/{ShareButton-BvHR4xdz.js → ShareButton-DS3Dsf4s.js} +1 -1
- ragbits/chat/ui-build/assets/UsageButton-Bu-ZOHWk.js +1 -0
- ragbits/chat/ui-build/assets/{authStore-DLhLSjcY.js → authStore-w3touTBX.js} +1 -1
- ragbits/chat/ui-build/assets/chunk-IGSAU2ZA-CZpYjJJG.js +1 -0
- ragbits/chat/ui-build/assets/{chunk-SSA7SXE4-DnKzYyYP.js → chunk-SSA7SXE4-26fqANp7.js} +1 -1
- ragbits/chat/ui-build/assets/index-BMhtIjmr.js +4 -0
- ragbits/chat/ui-build/assets/index-BoBogvMe.js +1 -0
- ragbits/chat/ui-build/assets/index-DlZV-Rce.css +1 -0
- ragbits/chat/ui-build/assets/index-V0bFpjmJ.js +32 -0
- ragbits/chat/ui-build/assets/index-aPw21Xcf.js +127 -0
- ragbits/chat/ui-build/assets/useMenuTriggerState-B-4lUpkM.js +1 -0
- ragbits/chat/ui-build/assets/useSelectableItem-BaL4tj6I.js +1 -0
- ragbits/chat/ui-build/index.html +2 -2
- {ragbits_chat-1.4.0.dev202509220622.dist-info → ragbits_chat-1.4.0.dev202511290233.dist-info}/METADATA +2 -2
- ragbits_chat-1.4.0.dev202511290233.dist-info/RECORD +52 -0
- {ragbits_chat-1.4.0.dev202509220622.dist-info → ragbits_chat-1.4.0.dev202511290233.dist-info}/WHEEL +1 -1
- ragbits/chat/ui-build/assets/ChatHistory-D2Pd5V75.js +0 -1
- ragbits/chat/ui-build/assets/ChatOptionsForm-DYA6GEAP.js +0 -1
- ragbits/chat/ui-build/assets/FeedbackForm-Bovwe8ia.js +0 -1
- ragbits/chat/ui-build/assets/UsageButton-BMDI2IGg.js +0 -1
- ragbits/chat/ui-build/assets/chunk-IGSAU2ZA-DUS7ku-0.js +0 -1
- ragbits/chat/ui-build/assets/index-3BSSmhVm.js +0 -1
- ragbits/chat/ui-build/assets/index-BTHsSiyo.css +0 -1
- ragbits/chat/ui-build/assets/index-C75huGqt.js +0 -127
- ragbits/chat/ui-build/assets/index-I-Ja0wkh.js +0 -4
- ragbits/chat/ui-build/assets/index-VYICyW2P.js +0 -32
- ragbits_chat-1.4.0.dev202509220622.dist-info/RECORD +0 -49
|
@@ -9,6 +9,8 @@ from abc import ABC, abstractmethod
|
|
|
9
9
|
from collections.abc import AsyncGenerator, Callable
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
|
+
from ragbits.agents.tools.todo import Task
|
|
13
|
+
from ragbits.chat.interface.summary import SummaryGenerator
|
|
12
14
|
from ragbits.chat.interface.ui_customization import UICustomization
|
|
13
15
|
from ragbits.core.audit.metrics import record_metric
|
|
14
16
|
from ragbits.core.audit.metrics.base import MetricType
|
|
@@ -21,24 +23,43 @@ from ..persistence import HistoryPersistenceStrategy
|
|
|
21
23
|
from .forms import FeedbackConfig, UserSettings
|
|
22
24
|
from .types import (
|
|
23
25
|
ChatContext,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
ChatResponseUnion,
|
|
27
|
+
ClearMessageContent,
|
|
28
|
+
ClearMessageResponse,
|
|
29
|
+
ConversationIdContent,
|
|
30
|
+
ConversationIdResponse,
|
|
31
|
+
ConversationSummaryContent,
|
|
32
|
+
ConversationSummaryResponse,
|
|
26
33
|
FeedbackType,
|
|
34
|
+
FollowupMessagesContent,
|
|
35
|
+
FollowupMessagesResponse,
|
|
27
36
|
Image,
|
|
37
|
+
ImageResponse,
|
|
28
38
|
LiveUpdate,
|
|
29
39
|
LiveUpdateContent,
|
|
40
|
+
LiveUpdateResponse,
|
|
30
41
|
LiveUpdateType,
|
|
42
|
+
MessageIdContent,
|
|
43
|
+
MessageIdResponse,
|
|
31
44
|
MessageUsage,
|
|
32
45
|
Reference,
|
|
46
|
+
ReferenceResponse,
|
|
33
47
|
StateUpdate,
|
|
48
|
+
StateUpdateResponse,
|
|
49
|
+
TextContent,
|
|
50
|
+
TextResponse,
|
|
51
|
+
TodoItemContent,
|
|
52
|
+
TodoItemResponse,
|
|
53
|
+
UsageContent,
|
|
54
|
+
UsageResponse,
|
|
34
55
|
)
|
|
35
56
|
|
|
36
57
|
logger = logging.getLogger(__name__)
|
|
37
58
|
|
|
38
59
|
|
|
39
60
|
def with_chat_metadata(
|
|
40
|
-
func: Callable[["ChatInterface", str, ChatFormat, ChatContext], AsyncGenerator[
|
|
41
|
-
) -> Callable[["ChatInterface", str, ChatFormat | None, ChatContext | None], AsyncGenerator[
|
|
61
|
+
func: Callable[["ChatInterface", str, ChatFormat, ChatContext], AsyncGenerator[ChatResponseUnion, None]],
|
|
62
|
+
) -> Callable[["ChatInterface", str, ChatFormat | None, ChatContext | None], AsyncGenerator[ChatResponseUnion, None]]:
|
|
42
63
|
"""
|
|
43
64
|
Decorator that adds message and conversation metadata to the chat method and handles history persistence.
|
|
44
65
|
Generates message_id and conversation_id (if first message) and stores them in context.
|
|
@@ -48,7 +69,7 @@ def with_chat_metadata(
|
|
|
48
69
|
@functools.wraps(func)
|
|
49
70
|
async def wrapper(
|
|
50
71
|
self: "ChatInterface", message: str, history: ChatFormat | None = None, context: ChatContext | None = None
|
|
51
|
-
) -> AsyncGenerator[
|
|
72
|
+
) -> AsyncGenerator[ChatResponseUnion, None]:
|
|
52
73
|
start_time = time.time()
|
|
53
74
|
|
|
54
75
|
# Assure history and context are not None
|
|
@@ -67,14 +88,14 @@ def with_chat_metadata(
|
|
|
67
88
|
# Generate message_id if not present
|
|
68
89
|
if not context.message_id:
|
|
69
90
|
context.message_id = str(uuid.uuid4())
|
|
70
|
-
yield
|
|
91
|
+
yield MessageIdResponse(content=MessageIdContent(message_id=context.message_id))
|
|
71
92
|
|
|
72
93
|
# Generate conversation_id if this is the first message
|
|
73
94
|
is_new_conversation = False
|
|
74
95
|
if not context.conversation_id:
|
|
75
96
|
context.conversation_id = str(uuid.uuid4())
|
|
76
97
|
is_new_conversation = True
|
|
77
|
-
yield
|
|
98
|
+
yield ConversationIdResponse(content=ConversationIdContent(conversation_id=context.conversation_id))
|
|
78
99
|
|
|
79
100
|
# Track new conversation
|
|
80
101
|
record_metric(
|
|
@@ -84,6 +105,13 @@ def with_chat_metadata(
|
|
|
84
105
|
interface_class=self.__class__.__name__,
|
|
85
106
|
)
|
|
86
107
|
|
|
108
|
+
# Generate summary to serve as title for new conversations
|
|
109
|
+
try:
|
|
110
|
+
summary = await self.generate_conversation_summary(message, history, context)
|
|
111
|
+
yield ConversationSummaryResponse(content=ConversationSummaryContent(summary=summary))
|
|
112
|
+
except Exception:
|
|
113
|
+
logger.exception("Failed to generate conversation title")
|
|
114
|
+
|
|
87
115
|
responses = []
|
|
88
116
|
main_response = ""
|
|
89
117
|
extra_responses = []
|
|
@@ -94,9 +122,9 @@ def with_chat_metadata(
|
|
|
94
122
|
try:
|
|
95
123
|
async for response in func(self, message, history, context):
|
|
96
124
|
responses.append(response)
|
|
97
|
-
if
|
|
125
|
+
if isinstance(response, TextResponse):
|
|
98
126
|
# Record time to first token on the first TEXT response
|
|
99
|
-
if first_token_time is None and response.content:
|
|
127
|
+
if first_token_time is None and response.content.text:
|
|
100
128
|
first_token_time = time.time() - start_time
|
|
101
129
|
record_metric(
|
|
102
130
|
ChatHistogramMetric.CHAT_TIME_TO_FIRST_TOKEN,
|
|
@@ -107,9 +135,9 @@ def with_chat_metadata(
|
|
|
107
135
|
history_length=str(history_length),
|
|
108
136
|
)
|
|
109
137
|
|
|
110
|
-
main_response = main_response + response.content
|
|
138
|
+
main_response = main_response + response.content.text
|
|
111
139
|
# Rough token estimation (words * 1.3 for subword tokens)
|
|
112
|
-
response_token_count += len(response.content.split()) * 1.3
|
|
140
|
+
response_token_count += len(response.content.text.split()) * 1.3
|
|
113
141
|
else:
|
|
114
142
|
extra_responses.append(response)
|
|
115
143
|
yield response
|
|
@@ -184,6 +212,7 @@ class ChatInterface(ABC):
|
|
|
184
212
|
show_usage: bool = False
|
|
185
213
|
ui_customization: UICustomization | None = None
|
|
186
214
|
history_persistence: HistoryPersistenceStrategy | None = None
|
|
215
|
+
summary_generator: SummaryGenerator | None = None
|
|
187
216
|
|
|
188
217
|
def __init_subclass__(cls, **kwargs: dict) -> None:
|
|
189
218
|
"""Automatically apply the with_chat_metadata decorator to the chat method in subclasses."""
|
|
@@ -192,65 +221,63 @@ class ChatInterface(ABC):
|
|
|
192
221
|
cls.chat = functools.wraps(cls.chat)(with_chat_metadata(cls.chat)) # type: ignore
|
|
193
222
|
|
|
194
223
|
@staticmethod
|
|
195
|
-
def create_text_response(text: str) ->
|
|
224
|
+
def create_text_response(text: str) -> TextResponse:
|
|
196
225
|
"""Helper method to create a text response."""
|
|
197
|
-
return
|
|
226
|
+
return TextResponse(content=TextContent(text=text))
|
|
198
227
|
|
|
199
228
|
@staticmethod
|
|
200
229
|
def create_reference(
|
|
201
230
|
title: str,
|
|
202
231
|
content: str,
|
|
203
232
|
url: str | None = None,
|
|
204
|
-
) ->
|
|
233
|
+
) -> ReferenceResponse:
|
|
205
234
|
"""Helper method to create a reference response."""
|
|
206
|
-
return
|
|
207
|
-
type=ChatResponseType.REFERENCE,
|
|
208
|
-
content=Reference(title=title, content=content, url=url),
|
|
209
|
-
)
|
|
235
|
+
return ReferenceResponse(content=Reference(title=title, content=content, url=url))
|
|
210
236
|
|
|
211
237
|
@staticmethod
|
|
212
|
-
def create_state_update(state: dict[str, Any]) ->
|
|
238
|
+
def create_state_update(state: dict[str, Any]) -> StateUpdateResponse:
|
|
213
239
|
"""Helper method to create a state update response with signature."""
|
|
214
240
|
signature = ChatInterface._sign_state(state)
|
|
215
|
-
return
|
|
216
|
-
type=ChatResponseType.STATE_UPDATE,
|
|
217
|
-
content=StateUpdate(state=state, signature=signature),
|
|
218
|
-
)
|
|
241
|
+
return StateUpdateResponse(content=StateUpdate(state=state, signature=signature))
|
|
219
242
|
|
|
220
243
|
@staticmethod
|
|
221
244
|
def create_live_update(
|
|
222
245
|
update_id: str, type: LiveUpdateType, label: str, description: str | None = None
|
|
223
|
-
) ->
|
|
246
|
+
) -> LiveUpdateResponse:
|
|
224
247
|
"""Helper method to create a live update response."""
|
|
225
|
-
return
|
|
226
|
-
type=ChatResponseType.LIVE_UPDATE,
|
|
248
|
+
return LiveUpdateResponse(
|
|
227
249
|
content=LiveUpdate(
|
|
228
250
|
update_id=update_id, type=type, content=LiveUpdateContent(label=label, description=description)
|
|
229
|
-
)
|
|
251
|
+
)
|
|
230
252
|
)
|
|
231
253
|
|
|
232
254
|
@staticmethod
|
|
233
|
-
def create_followup_messages(messages: list[str]) ->
|
|
255
|
+
def create_followup_messages(messages: list[str]) -> FollowupMessagesResponse:
|
|
234
256
|
"""Helper method to create a live update response."""
|
|
235
|
-
return
|
|
257
|
+
return FollowupMessagesResponse(content=FollowupMessagesContent(messages=messages))
|
|
236
258
|
|
|
237
259
|
@staticmethod
|
|
238
|
-
def create_image_response(image_id: str, image_url: str) ->
|
|
260
|
+
def create_image_response(image_id: str, image_url: str) -> ImageResponse:
|
|
239
261
|
"""Helper method to create an image response."""
|
|
240
|
-
return
|
|
262
|
+
return ImageResponse(content=Image(id=image_id, url=image_url))
|
|
241
263
|
|
|
242
264
|
@staticmethod
|
|
243
|
-
def create_clear_message_response() ->
|
|
265
|
+
def create_clear_message_response() -> ClearMessageResponse:
|
|
244
266
|
"""Helper method to create an clear message response."""
|
|
245
|
-
return
|
|
267
|
+
return ClearMessageResponse(content=ClearMessageContent())
|
|
246
268
|
|
|
247
269
|
@staticmethod
|
|
248
|
-
def create_usage_response(usage: Usage) ->
|
|
249
|
-
return
|
|
250
|
-
|
|
251
|
-
|
|
270
|
+
def create_usage_response(usage: Usage) -> UsageResponse:
|
|
271
|
+
return UsageResponse(
|
|
272
|
+
content=UsageContent(
|
|
273
|
+
usage={model: MessageUsage.from_usage(usage) for model, usage in usage.model_breakdown.items()}
|
|
274
|
+
)
|
|
252
275
|
)
|
|
253
276
|
|
|
277
|
+
@staticmethod
|
|
278
|
+
def create_todo_item_response(task: Task) -> TodoItemResponse:
|
|
279
|
+
return TodoItemResponse(content=TodoItemContent(task=task))
|
|
280
|
+
|
|
254
281
|
@staticmethod
|
|
255
282
|
def _sign_state(state: dict[str, Any]) -> str:
|
|
256
283
|
"""
|
|
@@ -309,7 +336,7 @@ class ChatInterface(ABC):
|
|
|
309
336
|
message: str,
|
|
310
337
|
history: ChatFormat,
|
|
311
338
|
context: ChatContext,
|
|
312
|
-
) -> AsyncGenerator[
|
|
339
|
+
) -> AsyncGenerator[ChatResponseUnion, None]:
|
|
313
340
|
"""
|
|
314
341
|
Process a chat message and yield responses asynchronously.
|
|
315
342
|
|
|
@@ -329,17 +356,17 @@ class ChatInterface(ABC):
|
|
|
329
356
|
```python
|
|
330
357
|
chat = MyChatImplementation()
|
|
331
358
|
async for response in chat.chat("What is Python?"):
|
|
332
|
-
if
|
|
333
|
-
print(f"Text: {
|
|
334
|
-
elif
|
|
335
|
-
print(f"Reference: {
|
|
336
|
-
elif
|
|
337
|
-
if verify_state(
|
|
359
|
+
if isinstance(response, TextResponse):
|
|
360
|
+
print(f"Text: {response.content}")
|
|
361
|
+
elif isinstance(response, ReferenceResponse):
|
|
362
|
+
print(f"Reference: {response.content.title}")
|
|
363
|
+
elif isinstance(response, StateUpdateResponse):
|
|
364
|
+
if verify_state(response.content.state, response.content.signature):
|
|
338
365
|
# Update client state
|
|
339
366
|
pass
|
|
340
367
|
```
|
|
341
368
|
"""
|
|
342
|
-
yield
|
|
369
|
+
yield TextResponse(content=TextContent(text="Ragbits cannot respond - please implement chat method!"))
|
|
343
370
|
raise NotImplementedError("Chat implementations must implement chat method")
|
|
344
371
|
|
|
345
372
|
async def save_feedback(
|
|
@@ -366,3 +393,10 @@ class ChatInterface(ABC):
|
|
|
366
393
|
)
|
|
367
394
|
|
|
368
395
|
logger.info(f"[{self.__class__.__name__}] Saving {feedback} for message {message_id} with payload {payload}")
|
|
396
|
+
|
|
397
|
+
async def generate_conversation_summary(self, message: str, history: ChatFormat, context: ChatContext) -> str:
|
|
398
|
+
"""Delegate to the configured summary generator."""
|
|
399
|
+
if not self.summary_generator:
|
|
400
|
+
raise Exception("Tried to invoke `generate_conversation_summary`. No SummaryGenerator found.")
|
|
401
|
+
|
|
402
|
+
return await self.summary_generator.generate(message, history, context)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
|
|
5
|
+
from ragbits.chat.interface.types import ChatContext
|
|
6
|
+
from ragbits.core.llms.base import LLM
|
|
7
|
+
from ragbits.core.prompt.base import ChatFormat
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SummaryGenerator(ABC):
|
|
13
|
+
"""Base class for summary generators."""
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
async def generate(self, message: str, history: ChatFormat, context: ChatContext) -> str:
|
|
17
|
+
"""Generate a concise conversation title."""
|
|
18
|
+
...
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HeuristicSummaryGenerator(SummaryGenerator):
|
|
22
|
+
"""Simple title generator using heuristics (no LLM)."""
|
|
23
|
+
|
|
24
|
+
SIMPLE_MESSAGE_LENGTH = 2
|
|
25
|
+
|
|
26
|
+
def __init__(self, max_words: int = 6, fallback_title: str = "New chat"):
|
|
27
|
+
self.max_words = max_words
|
|
28
|
+
self.fallback_title = fallback_title
|
|
29
|
+
|
|
30
|
+
async def generate(self, message: str, _history: ChatFormat, _context: ChatContext) -> str:
|
|
31
|
+
"""Generate a concise conversation title using heuristic approach."""
|
|
32
|
+
t = (message or "").strip()
|
|
33
|
+
if not t or len(t.split()) <= self.SIMPLE_MESSAGE_LENGTH:
|
|
34
|
+
return self.fallback_title
|
|
35
|
+
return " ".join(t.split()[: self.max_words])
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class LLMSummaryGenerator(SummaryGenerator):
|
|
39
|
+
"""Generates a short conversation title using an LLM."""
|
|
40
|
+
|
|
41
|
+
DEFAULT_PROMPT = (
|
|
42
|
+
"You are a concise title generator for conversation threads. "
|
|
43
|
+
"Given the user's first message, return a single short title (3–8 words) summarizing the topic. "
|
|
44
|
+
"If the message is just a greeting or otherwise generic, respond exactly with: New chat\n\n"
|
|
45
|
+
"User message:\n{message}\n\nTitle:"
|
|
46
|
+
)
|
|
47
|
+
DEFAULT_TIMEOUT = 5
|
|
48
|
+
|
|
49
|
+
def __init__(self, llm: LLM, timeout: int = DEFAULT_TIMEOUT, prompt_template: str | None = None):
|
|
50
|
+
self.llm = llm
|
|
51
|
+
self.timeout = timeout
|
|
52
|
+
self.prompt_template = prompt_template or self.DEFAULT_PROMPT
|
|
53
|
+
|
|
54
|
+
async def generate(self, message: str, _history: ChatFormat, _context: ChatContext) -> str:
|
|
55
|
+
"""Generate a concise conversation title using LLM."""
|
|
56
|
+
prompt = self.prompt_template.format(message=message)
|
|
57
|
+
try:
|
|
58
|
+
raw = await asyncio.wait_for(self.llm.generate(prompt), self.timeout)
|
|
59
|
+
title = str(raw).strip().splitlines()[0]
|
|
60
|
+
return title or "New chat"
|
|
61
|
+
except asyncio.TimeoutError:
|
|
62
|
+
logger.warning("LLM title generation timed out — using fallback.")
|
|
63
|
+
return "New chat"
|
|
64
|
+
except Exception:
|
|
65
|
+
logger.exception("Error generating title with LLM — using fallback.")
|
|
66
|
+
return "New chat"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class HybridSummaryGenerator(SummaryGenerator):
|
|
70
|
+
"""Generates a short conversation title using an LLM with heuristic approach as a fallback."""
|
|
71
|
+
|
|
72
|
+
def __init__(self, llm: LLM):
|
|
73
|
+
self.llm_gen = LLMSummaryGenerator(llm)
|
|
74
|
+
self.heuristic = HeuristicSummaryGenerator()
|
|
75
|
+
|
|
76
|
+
async def generate(self, message: str, history: ChatFormat, context: ChatContext) -> str:
|
|
77
|
+
"""Generate summary using either LLM with heuristic approach as a fallback."""
|
|
78
|
+
try:
|
|
79
|
+
title = await self.llm_gen.generate(message, history, context)
|
|
80
|
+
return title or await self.heuristic.generate(message, history, context)
|
|
81
|
+
except Exception:
|
|
82
|
+
return await self.heuristic.generate(message, history, context)
|