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
ragbits/chat/__init__.py CHANGED
@@ -14,10 +14,32 @@ from ragbits.chat.client import (
14
14
  from ragbits.chat.interface.types import (
15
15
  ChatResponse,
16
16
  ChatResponseType,
17
+ ChatResponseUnion,
18
+ ClearMessageContent,
19
+ ClearMessageResponse,
20
+ ConversationIdContent,
21
+ ConversationIdResponse,
22
+ ConversationSummaryContent,
23
+ ConversationSummaryResponse,
24
+ FollowupMessagesContent,
25
+ FollowupMessagesResponse,
26
+ ImageResponse,
27
+ LiveUpdateResponse,
17
28
  Message,
29
+ MessageIdContent,
30
+ MessageIdResponse,
18
31
  MessageRole,
19
32
  Reference,
33
+ ReferenceResponse,
34
+ ResponseContent,
20
35
  StateUpdate,
36
+ StateUpdateResponse,
37
+ TextContent,
38
+ TextResponse,
39
+ TodoItemContent,
40
+ TodoItemResponse,
41
+ UsageContent,
42
+ UsageResponse,
21
43
  )
22
44
 
23
45
  __all__ = [
@@ -25,15 +47,37 @@ __all__ = [
25
47
  "AuthenticationResponse",
26
48
  "ChatResponse",
27
49
  "ChatResponseType",
50
+ "ChatResponseUnion",
51
+ "ClearMessageContent",
52
+ "ClearMessageResponse",
53
+ "ConversationIdContent",
54
+ "ConversationIdResponse",
55
+ "ConversationSummaryContent",
56
+ "ConversationSummaryResponse",
57
+ "FollowupMessagesContent",
58
+ "FollowupMessagesResponse",
59
+ "ImageResponse",
28
60
  "ListAuthenticationBackend",
61
+ "LiveUpdateResponse",
29
62
  "Message",
63
+ "MessageIdContent",
64
+ "MessageIdResponse",
30
65
  "MessageRole",
31
66
  "RagbitsChatClient",
32
67
  "RagbitsConversation",
33
68
  "Reference",
69
+ "ReferenceResponse",
70
+ "ResponseContent",
34
71
  "StateUpdate",
72
+ "StateUpdateResponse",
35
73
  "SyncRagbitsChatClient",
36
74
  "SyncRagbitsConversation",
75
+ "TextContent",
76
+ "TextResponse",
77
+ "TodoItemContent",
78
+ "TodoItemResponse",
79
+ "UsageContent",
80
+ "UsageResponse",
37
81
  "User",
38
82
  "UserCredentials",
39
83
  ]
ragbits/chat/_utils.py CHANGED
@@ -5,7 +5,7 @@ import logging
5
5
 
6
6
  from pydantic import TypeAdapter
7
7
 
8
- from .interface.types import ChatResponse
8
+ from .interface.types import ChatResponse, ChatResponseUnion
9
9
 
10
10
  logger = logging.getLogger(__name__)
11
11
 
@@ -33,7 +33,7 @@ def parse_sse_line(line: str) -> ChatResponse | None:
33
33
  try:
34
34
  json_payload = line[len(PREFIX) :].strip()
35
35
  data = json.loads(json_payload)
36
- adapter: TypeAdapter[ChatResponse] = TypeAdapter(ChatResponse)
36
+ adapter: TypeAdapter[ChatResponseUnion] = TypeAdapter(ChatResponseUnion)
37
37
  return adapter.validate_python(data)
38
38
  except Exception as exc:
39
39
  logger.error("Failed to parse SSE line: %s", exc, exc_info=True)
ragbits/chat/api.py CHANGED
@@ -1,17 +1,18 @@
1
1
  import importlib
2
2
  import json
3
3
  import logging
4
+ import re
4
5
  import time
5
6
  from collections.abc import AsyncGenerator
6
7
  from contextlib import asynccontextmanager
7
8
  from pathlib import Path
8
- from typing import Any, Literal, cast
9
+ from typing import Any, cast
9
10
 
10
11
  import uvicorn
11
12
  from fastapi import Depends, FastAPI, HTTPException, Request, status
12
13
  from fastapi.exceptions import RequestValidationError
13
14
  from fastapi.middleware.cors import CORSMiddleware
14
- from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
15
+ from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse, StreamingResponse
15
16
  from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
16
17
  from fastapi.staticfiles import StaticFiles
17
18
  from pydantic import BaseModel
@@ -24,14 +25,14 @@ from ragbits.chat.interface.types import (
24
25
  AuthType,
25
26
  ChatContext,
26
27
  ChatMessageRequest,
27
- ChatResponse,
28
- ChatResponseType,
28
+ ChatResponseUnion,
29
29
  ChunkedContent,
30
30
  ConfigResponse,
31
31
  FeedbackConfig,
32
32
  FeedbackItem,
33
33
  FeedbackRequest,
34
34
  Image,
35
+ ImageResponse,
35
36
  )
36
37
  from ragbits.core.audit.metrics import record_metric
37
38
  from ragbits.core.audit.metrics.base import MetricType
@@ -59,6 +60,7 @@ class RagbitsAPI:
59
60
  ui_build_dir: str | None = None,
60
61
  debug_mode: bool = False,
61
62
  auth_backend: AuthenticationBackend | type[AuthenticationBackend] | str | None = None,
63
+ theme_path: str | None = None,
62
64
  ) -> None:
63
65
  """
64
66
  Initialize the RagbitsAPI.
@@ -70,6 +72,7 @@ class RagbitsAPI:
70
72
  ui_build_dir: Path to a custom UI build directory. If None, uses the default package UI.
71
73
  debug_mode: Flag enabling debug tools in the default UI
72
74
  auth_backend: Authentication backend for user authentication. If None, no authentication required.
75
+ theme_path: Path to a JSON file containing HeroUI theme configuration from heroui.com/themes
73
76
  """
74
77
  self.chat_interface: ChatInterface = self._load_chat_interface(chat_interface)
75
78
  self.dist_dir = Path(ui_build_dir) if ui_build_dir else Path(__file__).parent / "ui-build"
@@ -77,6 +80,7 @@ class RagbitsAPI:
77
80
  self.debug_mode = debug_mode
78
81
  self.auth_backend = self._load_auth_backend(auth_backend)
79
82
  self.security = HTTPBearer(auto_error=False) if auth_backend else None
83
+ self.theme_path = Path(theme_path) if theme_path else None
80
84
 
81
85
  @asynccontextmanager
82
86
  async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
@@ -188,6 +192,25 @@ class RagbitsAPI:
188
192
 
189
193
  return JSONResponse(content=config_response.model_dump())
190
194
 
195
+ # Theme CSS endpoint - always available, returns 404 if no theme configured
196
+ @self.app.get("/api/theme", response_class=PlainTextResponse)
197
+ async def theme() -> PlainTextResponse:
198
+ if not self.theme_path or not self.theme_path.exists():
199
+ raise HTTPException(status_code=404, detail="No theme configured")
200
+
201
+ try:
202
+ with open(self.theme_path, encoding="utf-8") as f:
203
+ json_content = f.read().strip()
204
+
205
+ css_content = RagbitsAPI._convert_heroui_json_to_css(json_content)
206
+
207
+ return PlainTextResponse(
208
+ content=css_content, media_type="text/css", headers={"Cache-Control": "public, max-age=3600"}
209
+ )
210
+ except Exception as e:
211
+ logger.error(f"Error serving theme: {e}")
212
+ raise HTTPException(status_code=500, detail="Error loading theme") from e
213
+
191
214
  @self.app.get("/{full_path:path}", response_class=HTMLResponse)
192
215
  async def root() -> HTMLResponse:
193
216
  index_file = self.dist_dir / "index.html"
@@ -305,17 +328,17 @@ class RagbitsAPI:
305
328
  content = str(data_dict.get("content", ""))
306
329
 
307
330
  match data_dict.get("type"):
308
- case ChatResponseType.TEXT:
331
+ case "text":
309
332
  response_text += content
310
- case ChatResponseType.REFERENCE:
333
+ case "reference":
311
334
  reference_text += content
312
- case ChatResponseType.STATE_UPDATE:
335
+ case "state_update":
313
336
  state_update_text += content
314
- case ChatResponseType.MESSAGE_ID:
337
+ case "message_id":
315
338
  outputs.message_id = content
316
- case ChatResponseType.CONVERSATION_ID:
339
+ case "conversation_id":
317
340
  outputs.conversation_id = content
318
- case ChatResponseType.IMAGE:
341
+ case "image":
319
342
  outputs.image_url = content
320
343
 
321
344
  yield chunk
@@ -532,7 +555,7 @@ class RagbitsAPI:
532
555
 
533
556
  @staticmethod
534
557
  async def _chat_response_to_sse(
535
- responses: AsyncGenerator[ChatResponse],
558
+ responses: AsyncGenerator[ChatResponseUnion],
536
559
  ) -> AsyncGenerator[str, None]:
537
560
  """
538
561
  Formats chat responses into Server-Sent Events (SSE) format for streaming to the client.
@@ -556,9 +579,9 @@ class RagbitsAPI:
556
579
  }
557
580
 
558
581
  # Auto-chunk large images using ChunkedContent model
559
- if response.type == ChatResponseType.IMAGE and cast(Image, response.content).url.startswith("data:"):
582
+ if isinstance(response, ImageResponse) and cast(Image, response.content).url.startswith("data:"):
560
583
  # Auto-chunk the image
561
- async for chunk_response in RagbitsAPI._create_chunked_responses(ChatResponseType.IMAGE, response):
584
+ async for chunk_response in RagbitsAPI._create_chunked_responses(response):
562
585
  yield f"data: {json.dumps(chunk_response)}\n\n"
563
586
 
564
587
  continue # Skip normal processing for chunked images
@@ -568,7 +591,7 @@ class RagbitsAPI:
568
591
  # - Regular URL images (https://..., http://..., /path/to/image.jpg)
569
592
  data = json.dumps(
570
593
  {
571
- "type": response.type.value,
594
+ "type": response.get_type(),
572
595
  "content": response_to_send.model_dump()
573
596
  if isinstance(response_to_send, BaseModel)
574
597
  else response_to_send,
@@ -592,9 +615,7 @@ class RagbitsAPI:
592
615
  )
593
616
 
594
617
  @staticmethod
595
- async def _create_chunked_responses(
596
- type: Literal[ChatResponseType.IMAGE], base64_response: ChatResponse
597
- ) -> AsyncGenerator[dict, None]:
618
+ async def _create_chunked_responses(base64_response: ImageResponse) -> AsyncGenerator[dict, None]:
598
619
  """Create chunked responses from a base64 response."""
599
620
  image_content = cast(Image, base64_response.content)
600
621
  mime_type, base64_data = image_content.url.split(",", 1)
@@ -604,14 +625,14 @@ class RagbitsAPI:
604
625
  for i, chunk in enumerate(chunks):
605
626
  chunked_content = ChunkedContent(
606
627
  id=image_content.id,
607
- content_type=type.value,
628
+ content_type="image",
608
629
  chunk_index=i,
609
630
  total_chunks=len(chunks),
610
631
  mime_type=mime_type,
611
632
  data=chunk,
612
633
  )
613
634
 
614
- yield {"type": ChatResponseType.CHUNKED_CONTENT.value, "content": chunked_content.model_dump()}
635
+ yield {"type": "chunked_content", "content": chunked_content.model_dump()}
615
636
 
616
637
  @staticmethod
617
638
  def _load_chat_interface(implementation: type[ChatInterface] | str) -> ChatInterface:
@@ -680,3 +701,78 @@ class RagbitsAPI:
680
701
  Used for starting the API
681
702
  """
682
703
  uvicorn.run(self.app, host=host, port=port)
704
+
705
+ @staticmethod
706
+ def _convert_heroui_json_to_css(json_content: str) -> str:
707
+ """Convert HeroUI JSON theme configuration to CSS variables."""
708
+ try:
709
+ theme_config = json.loads(json_content)
710
+ css_lines = [
711
+ "/* Auto-generated CSS from HeroUI theme configuration */",
712
+ "",
713
+ ":root {",
714
+ ]
715
+
716
+ # Process light theme
717
+ if "themes" in theme_config and "light" in theme_config["themes"]:
718
+ light_colors = theme_config["themes"]["light"]["colors"]
719
+ css_lines.extend(RagbitsAPI._process_theme_colors(light_colors))
720
+
721
+ # Add layout properties
722
+ if "layout" in theme_config:
723
+ for prop, value in theme_config["layout"].items():
724
+ css_prop = re.sub(r"([A-Z])", r"-\1", prop).lower()
725
+ css_lines.append(f" --heroui-{css_prop}: {value};")
726
+
727
+ css_lines.append("}")
728
+ css_lines.append("")
729
+
730
+ # Process dark theme
731
+ if "themes" in theme_config and "dark" in theme_config["themes"]:
732
+ css_lines.append(".dark {")
733
+ dark_colors = theme_config["themes"]["dark"]["colors"]
734
+ css_lines.extend(RagbitsAPI._process_theme_colors(dark_colors))
735
+
736
+ if "layout" in theme_config:
737
+ for prop, value in theme_config["layout"].items():
738
+ css_prop = re.sub(r"([A-Z])", r"-\1", prop).lower()
739
+ css_lines.append(f" --heroui-{css_prop}: {value};")
740
+
741
+ css_lines.append("}")
742
+
743
+ # Add body styling
744
+ css_lines.extend(
745
+ [
746
+ "",
747
+ "body {",
748
+ " background-color: var(--heroui-background);",
749
+ " color: var(--heroui-foreground);",
750
+ "}",
751
+ ]
752
+ )
753
+
754
+ return "\n".join(css_lines)
755
+
756
+ except json.JSONDecodeError as e:
757
+ logger.error(f"Invalid JSON in theme file: {e}")
758
+ raise HTTPException(status_code=400, detail="Invalid JSON theme file") from e
759
+ except Exception as e:
760
+ logger.error(f"Error converting theme: {e}")
761
+ raise HTTPException(status_code=500, detail="Error processing theme") from e
762
+
763
+ @staticmethod
764
+ def _process_theme_colors(colors: dict) -> list[str]:
765
+ """Process theme colors and return CSS variable declarations."""
766
+ css_lines = []
767
+
768
+ for color_name, color_value in colors.items():
769
+ if isinstance(color_value, dict):
770
+ for shade, value in color_value.items():
771
+ if shade == "DEFAULT":
772
+ css_lines.append(f" --heroui-{color_name}: {value};")
773
+ elif isinstance(value, str):
774
+ css_lines.append(f" --heroui-{color_name}-{shade}: {value};")
775
+ elif isinstance(color_value, str):
776
+ css_lines.append(f" --heroui-{color_name}: {color_value};")
777
+
778
+ return css_lines
ragbits/chat/cli.py CHANGED
@@ -32,6 +32,11 @@ def run(
32
32
  ),
33
33
  debug_mode: bool = typer.Option(False, "--debug", help="Flag enabling debug tools in the default UI"),
34
34
  auth: str = typer.Option(None, help="Path to a module with Authentication Backend"),
35
+ theme: str = typer.Option(
36
+ None,
37
+ "--theme",
38
+ help="Path to a HeroUI theme JSON file from heroui.com/themes",
39
+ ),
35
40
  ) -> None:
36
41
  """
37
42
  Run API service with UI demo
@@ -42,5 +47,6 @@ def run(
42
47
  ui_build_dir=ui_build_dir,
43
48
  debug_mode=debug_mode,
44
49
  auth_backend=auth,
50
+ theme_path=theme,
45
51
  )
46
52
  api.run(host=host, port=port)
@@ -8,10 +8,15 @@ import httpx
8
8
  from .._utils import build_api_url, parse_sse_line
9
9
  from ..interface.types import (
10
10
  ChatResponse,
11
- ChatResponseType,
11
+ ConversationIdResponse,
12
+ LiveUpdateResponse,
12
13
  Message,
14
+ MessageIdResponse,
13
15
  MessageRole,
16
+ ReferenceResponse,
14
17
  StateUpdate,
18
+ StateUpdateResponse,
19
+ TextResponse,
15
20
  )
16
21
  from .exceptions import ChatClientRequestError, ChatClientResponseError
17
22
 
@@ -103,21 +108,21 @@ class RagbitsConversation:
103
108
 
104
109
  def _process_incoming(self, resp: ChatResponse, assistant_index: int) -> None:
105
110
  """Update local state based on *resp*."""
106
- if resp.as_state_update() is not None:
107
- self.conversation_state = resp.as_state_update()
108
- elif resp.as_conversation_id() is not None:
109
- self.conversation_id = resp.as_conversation_id()
110
- elif resp.type is ChatResponseType.MESSAGE_ID:
111
+ if isinstance(resp, StateUpdateResponse):
112
+ self.conversation_state = resp.content
113
+ elif isinstance(resp, ConversationIdResponse):
114
+ self.conversation_id = resp.content.conversation_id
115
+ elif isinstance(resp, MessageIdResponse):
111
116
  return
112
117
 
113
118
  assistant_msg = self.history[assistant_index]
114
119
 
115
- if resp.type is ChatResponseType.LIVE_UPDATE:
120
+ if isinstance(resp, LiveUpdateResponse):
116
121
  assistant_msg.content += f"\n[LIVE_UPDATE]: {resp.content}\n"
117
- elif resp.as_reference() is not None:
122
+ elif isinstance(resp, ReferenceResponse):
118
123
  assistant_msg.content += f"\n[REFERENCE]: {resp.content}\n"
119
- elif (text_content := resp.as_text()) is not None:
120
- assistant_msg.content += text_content
124
+ elif isinstance(resp, TextResponse):
125
+ assistant_msg.content += resp.content.text
121
126
 
122
127
 
123
128
  class SyncRagbitsConversation:
@@ -212,18 +217,18 @@ class SyncRagbitsConversation:
212
217
 
213
218
  def _process_incoming(self, resp: ChatResponse, assistant_index: int) -> None:
214
219
  """Update local state based on *resp*."""
215
- if resp.as_state_update() is not None:
216
- self.conversation_state = resp.as_state_update()
217
- elif resp.as_conversation_id() is not None:
218
- self.conversation_id = resp.as_conversation_id()
219
- elif resp.type is ChatResponseType.MESSAGE_ID:
220
+ if isinstance(resp, StateUpdateResponse):
221
+ self.conversation_state = resp.content
222
+ elif isinstance(resp, ConversationIdResponse):
223
+ self.conversation_id = resp.content.conversation_id
224
+ elif isinstance(resp, MessageIdResponse):
220
225
  return
221
226
 
222
227
  assistant_msg = self.history[assistant_index]
223
228
 
224
- if resp.type is ChatResponseType.LIVE_UPDATE:
229
+ if isinstance(resp, LiveUpdateResponse):
225
230
  assistant_msg.content += f"\n[LIVE_UPDATE]: {resp.content}\n"
226
- elif resp.as_reference() is not None:
231
+ elif isinstance(resp, ReferenceResponse):
227
232
  assistant_msg.content += f"\n[REFERENCE]: {resp.content}\n"
228
- elif (text_content := resp.as_text()) is not None:
229
- assistant_msg.content += text_content
233
+ elif isinstance(resp, TextResponse):
234
+ assistant_msg.content += resp.content.text