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.
Files changed (45) hide show
  1. ragbits/chat/__init__.py +44 -0
  2. ragbits/chat/_utils.py +2 -2
  3. ragbits/chat/api.py +115 -19
  4. ragbits/chat/cli.py +6 -0
  5. ragbits/chat/client/conversation.py +24 -19
  6. ragbits/chat/interface/_interface.py +79 -45
  7. ragbits/chat/interface/summary.py +82 -0
  8. ragbits/chat/interface/types.py +582 -48
  9. ragbits/chat/persistence/base.py +2 -1
  10. ragbits/chat/persistence/file.py +2 -1
  11. ragbits/chat/persistence/sql.py +6 -3
  12. ragbits/chat/providers/model_provider.py +30 -3
  13. ragbits/chat/ui-build/assets/{AuthGuard-B3JOY-uC.js → AuthGuard-BTxv1Dj_.js} +1 -1
  14. ragbits/chat/ui-build/assets/ChatHistory-C7A2uiIQ.js +2 -0
  15. ragbits/chat/ui-build/assets/ChatOptionsForm-x2g6iEPU.js +1 -0
  16. ragbits/chat/ui-build/assets/FeedbackForm-BPycljsV.js +1 -0
  17. ragbits/chat/ui-build/assets/{Login-B2kPNK-w.js → Login-BWGQs3cn.js} +1 -1
  18. ragbits/chat/ui-build/assets/{LogoutButton-Bspy5P1_.js → LogoutButton-gJF91B1i.js} +1 -1
  19. ragbits/chat/ui-build/assets/{ShareButton-BvHR4xdz.js → ShareButton-DS3Dsf4s.js} +1 -1
  20. ragbits/chat/ui-build/assets/UsageButton-Bu-ZOHWk.js +1 -0
  21. ragbits/chat/ui-build/assets/{authStore-DLhLSjcY.js → authStore-w3touTBX.js} +1 -1
  22. ragbits/chat/ui-build/assets/chunk-IGSAU2ZA-CZpYjJJG.js +1 -0
  23. ragbits/chat/ui-build/assets/{chunk-SSA7SXE4-DnKzYyYP.js → chunk-SSA7SXE4-26fqANp7.js} +1 -1
  24. ragbits/chat/ui-build/assets/index-BMhtIjmr.js +4 -0
  25. ragbits/chat/ui-build/assets/index-BoBogvMe.js +1 -0
  26. ragbits/chat/ui-build/assets/index-DlZV-Rce.css +1 -0
  27. ragbits/chat/ui-build/assets/index-V0bFpjmJ.js +32 -0
  28. ragbits/chat/ui-build/assets/index-aPw21Xcf.js +127 -0
  29. ragbits/chat/ui-build/assets/useMenuTriggerState-B-4lUpkM.js +1 -0
  30. ragbits/chat/ui-build/assets/useSelectableItem-BaL4tj6I.js +1 -0
  31. ragbits/chat/ui-build/index.html +2 -2
  32. {ragbits_chat-1.4.0.dev202509220622.dist-info → ragbits_chat-1.4.0.dev202511290233.dist-info}/METADATA +2 -2
  33. ragbits_chat-1.4.0.dev202511290233.dist-info/RECORD +52 -0
  34. {ragbits_chat-1.4.0.dev202509220622.dist-info → ragbits_chat-1.4.0.dev202511290233.dist-info}/WHEEL +1 -1
  35. ragbits/chat/ui-build/assets/ChatHistory-D2Pd5V75.js +0 -1
  36. ragbits/chat/ui-build/assets/ChatOptionsForm-DYA6GEAP.js +0 -1
  37. ragbits/chat/ui-build/assets/FeedbackForm-Bovwe8ia.js +0 -1
  38. ragbits/chat/ui-build/assets/UsageButton-BMDI2IGg.js +0 -1
  39. ragbits/chat/ui-build/assets/chunk-IGSAU2ZA-DUS7ku-0.js +0 -1
  40. ragbits/chat/ui-build/assets/index-3BSSmhVm.js +0 -1
  41. ragbits/chat/ui-build/assets/index-BTHsSiyo.css +0 -1
  42. ragbits/chat/ui-build/assets/index-C75huGqt.js +0 -127
  43. ragbits/chat/ui-build/assets/index-I-Ja0wkh.js +0 -4
  44. ragbits/chat/ui-build/assets/index-VYICyW2P.js +0 -32
  45. 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
- ChatResponse,
25
- ChatResponseType,
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[ChatResponse, None]],
41
- ) -> Callable[["ChatInterface", str, ChatFormat | None, ChatContext | None], AsyncGenerator[ChatResponse, None]]:
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[ChatResponse, None]:
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 ChatResponse(type=ChatResponseType.MESSAGE_ID, content=context.message_id)
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 ChatResponse(type=ChatResponseType.CONVERSATION_ID, content=context.conversation_id)
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 response.type == ChatResponseType.TEXT and isinstance(response.content, str):
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) -> ChatResponse:
224
+ def create_text_response(text: str) -> TextResponse:
196
225
  """Helper method to create a text response."""
197
- return ChatResponse(type=ChatResponseType.TEXT, content=text)
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
- ) -> ChatResponse:
233
+ ) -> ReferenceResponse:
205
234
  """Helper method to create a reference response."""
206
- return ChatResponse(
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]) -> ChatResponse:
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 ChatResponse(
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
- ) -> ChatResponse:
246
+ ) -> LiveUpdateResponse:
224
247
  """Helper method to create a live update response."""
225
- return ChatResponse(
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]) -> ChatResponse:
255
+ def create_followup_messages(messages: list[str]) -> FollowupMessagesResponse:
234
256
  """Helper method to create a live update response."""
235
- return ChatResponse(type=ChatResponseType.FOLLOWUP_MESSAGES, content=messages)
257
+ return FollowupMessagesResponse(content=FollowupMessagesContent(messages=messages))
236
258
 
237
259
  @staticmethod
238
- def create_image_response(image_id: str, image_url: str) -> ChatResponse:
260
+ def create_image_response(image_id: str, image_url: str) -> ImageResponse:
239
261
  """Helper method to create an image response."""
240
- return ChatResponse(type=ChatResponseType.IMAGE, content=Image(id=image_id, url=image_url))
262
+ return ImageResponse(content=Image(id=image_id, url=image_url))
241
263
 
242
264
  @staticmethod
243
- def create_clear_message_response() -> ChatResponse:
265
+ def create_clear_message_response() -> ClearMessageResponse:
244
266
  """Helper method to create an clear message response."""
245
- return ChatResponse(type=ChatResponseType.CLEAR_MESSAGE, content=None)
267
+ return ClearMessageResponse(content=ClearMessageContent())
246
268
 
247
269
  @staticmethod
248
- def create_usage_response(usage: Usage) -> ChatResponse:
249
- return ChatResponse(
250
- type=ChatResponseType.USAGE,
251
- content={model: MessageUsage.from_usage(usage) for model, usage in usage.model_breakdown.items()},
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[ChatResponse, None]:
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 text := response.as_text():
333
- print(f"Text: {text}")
334
- elif ref := response.as_reference():
335
- print(f"Reference: {ref.title}")
336
- elif state := response.as_state_update():
337
- if verify_state(state.state, state.signature):
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 ChatResponse(type=ChatResponseType.TEXT, content="Ragbits cannot respond - please implement chat method!")
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)