Skip to content

Commit 1ba2a06

Browse files
authored
feat: usage component (#786)
1 parent ddb1e12 commit 1ba2a06

File tree

19 files changed

+385
-15
lines changed

19 files changed

+385
-15
lines changed

.github/scripts/check_changelog_update.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ fi
1313

1414
CHANGED_PACKAGES=$(echo "$CHANGED_FILES" | grep -oE 'packages/[^/]+/src' | cut -d '/' -f2 | sort -u)
1515

16+
# Treat changes in `typescript` directory as `ragbits-chat` package.
17+
if echo "$CHANGED_FILES" | grep -q "typescript/"; then
18+
CHANGED_PACKAGES="$CHANGED_PACKAGES"$'\n'ragbits-chat
19+
fi
20+
21+
# Deduplicate
22+
CHANGED_PACKAGES=$(echo "$CHANGED_PACKAGES" | sort -u)
23+
1624
if [ -z "$CHANGED_PACKAGES" ]; then
1725
echo "No package changes detected. Skipping changelog check."
1826
exit 0

examples/chat/chat.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ class MyChat(ChatInterface):
8888
)
8989

9090
conversation_history = True
91+
show_usage = True
9192

9293
def __init__(self) -> None:
9394
self.llm = LiteLLM(model_name="gpt-4o-mini")
@@ -143,7 +144,11 @@ async def chat(
143144
yield live_update
144145
await asyncio.sleep(2)
145146

146-
async for chunk in self.llm.generate_streaming([*history, {"role": "user", "content": message}]):
147+
streaming_result = self.llm.generate_streaming([*history, {"role": "user", "content": message}])
148+
async for chunk in streaming_result:
147149
yield self.create_text_response(chunk)
148150

151+
if streaming_result.usage:
152+
yield self.create_usage_response(streaming_result.usage)
153+
149154
yield self.create_followup_messages(["Example Response 1", "Example Response 2", "Example Response 3"])

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/ragbits-chat/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44
- Add `clear_message` event type allowing to reset whole message (#789)
5+
- Add usage component to UI with backend support (#786)
56
- Add authentication handling in the UI (#763)
67
- Add backend authentication (#761)
78

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from collections.abc import AsyncGenerator
66
from contextlib import asynccontextmanager
77
from pathlib import Path
8+
from typing import Any
89

910
import uvicorn
1011
from fastapi import Depends, FastAPI, HTTPException, Request, status
@@ -13,6 +14,7 @@
1314
from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
1415
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
1516
from fastapi.staticfiles import StaticFiles
17+
from pydantic import BaseModel
1618

1719
from ragbits.chat.auth import AuthenticationBackend, User
1820
from ragbits.chat.auth.types import LoginRequest, LoginResponse, LogoutRequest
@@ -170,6 +172,7 @@ async def config() -> JSONResponse:
170172
user_settings=self.chat_interface.user_settings,
171173
debug_mode=self.debug_mode,
172174
conversation_history=self.chat_interface.conversation_history,
175+
show_usage=self.chat_interface.show_usage,
173176
authentication=AuthenticationConfig(
174177
enabled=self.auth_backend is not None,
175178
auth_types=[AuthType.CREDENTIALS],
@@ -537,12 +540,18 @@ async def _chat_response_to_sse(
537540
try:
538541
async for response in responses:
539542
chunk_count += 1
543+
response_to_send: Any = response.content
544+
if isinstance(response.content, dict):
545+
response_to_send = {
546+
key: model.model_dump() if isinstance(model, BaseModel) else model
547+
for key, model in response.content.items()
548+
}
540549
data = json.dumps(
541550
{
542551
"type": response.type.value,
543-
"content": response.content
544-
if isinstance(response.content, str | list | None)
545-
else response.content.model_dump(),
552+
"content": response_to_send.model_dump()
553+
if isinstance(response_to_send, BaseModel)
554+
else response_to_send,
546555
}
547556
)
548557
yield f"data: {data}\n\n"

packages/ragbits-chat/src/ragbits/chat/interface/_interface.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from ragbits.chat.interface.ui_customization import UICustomization
1313
from ragbits.core.audit.metrics import record_metric
1414
from ragbits.core.audit.metrics.base import MetricType
15+
from ragbits.core.llms.base import Usage
1516
from ragbits.core.prompt.base import ChatFormat
1617
from ragbits.core.utils import get_secret_key
1718

@@ -27,6 +28,7 @@
2728
LiveUpdate,
2829
LiveUpdateContent,
2930
LiveUpdateType,
31+
MessageUsage,
3032
Reference,
3133
StateUpdate,
3234
)
@@ -179,6 +181,7 @@ class ChatInterface(ABC):
179181
feedback_config: FeedbackConfig = FeedbackConfig()
180182
user_settings: UserSettings = UserSettings()
181183
conversation_history: bool = False
184+
show_usage: bool = False
182185
ui_customization: UICustomization | None = None
183186
history_persistence: HistoryPersistenceStrategy | None = None
184187

@@ -241,6 +244,13 @@ def create_clear_message_response() -> ChatResponse:
241244
"""Helper method to create an clear message response."""
242245
return ChatResponse(type=ChatResponseType.CLEAR_MESSAGE, content=None)
243246

247+
@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()},
252+
)
253+
244254
@staticmethod
245255
def _sign_state(state: dict[str, Any]) -> str:
246256
"""

packages/ragbits-chat/src/ragbits/chat/interface/types.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from ragbits.chat.interface.forms import UserSettings
77
from ragbits.chat.interface.ui_customization import UICustomization
8+
from ragbits.core.llms.base import Usage
89

910

1011
class MessageRole(str, Enum):
@@ -66,6 +67,35 @@ class Image(BaseModel):
6667
url: str
6768

6869

70+
class MessageUsage(BaseModel):
71+
"""Represents usage for a message. Reflects `Usage` computed properties."""
72+
73+
n_requests: int
74+
estimated_cost: float
75+
prompt_tokens: int
76+
completion_tokens: int
77+
total_tokens: int
78+
79+
@classmethod
80+
def from_usage(cls, usage: Usage) -> "MessageUsage":
81+
"""
82+
Create a MessageUsage object from Usage.
83+
84+
Args:
85+
usage: Usage object to be transformed.
86+
87+
Returns:
88+
The corresponding MessageUsage.
89+
"""
90+
return cls(
91+
completion_tokens=usage.completion_tokens,
92+
estimated_cost=usage.estimated_cost,
93+
n_requests=usage.n_requests,
94+
prompt_tokens=usage.prompt_tokens,
95+
total_tokens=usage.total_tokens,
96+
)
97+
98+
6999
class ChatResponseType(str, Enum):
70100
"""Types of responses that can be returned by the chat interface."""
71101

@@ -78,6 +108,7 @@ class ChatResponseType(str, Enum):
78108
FOLLOWUP_MESSAGES = "followup_messages"
79109
IMAGE = "image"
80110
CLEAR_MESSAGE = "clear_message"
111+
USAGE = "usage"
81112

82113

83114
class ChatContext(BaseModel):
@@ -94,7 +125,7 @@ class ChatResponse(BaseModel):
94125
"""Container for different types of chat responses."""
95126

96127
type: ChatResponseType
97-
content: str | Reference | StateUpdate | LiveUpdate | list[str] | Image | None
128+
content: str | Reference | StateUpdate | LiveUpdate | list[str] | Image | dict[str, MessageUsage] | None
98129

99130
def as_text(self) -> str | None:
100131
"""
@@ -164,6 +195,12 @@ def as_clear_message(self) -> None:
164195
"""
165196
return cast(None, self.content)
166197

198+
def as_usage(self) -> dict[str, MessageUsage] | None:
199+
"""
200+
Return the content as dict from model name to Usage if this is an usage response, else None
201+
"""
202+
return cast(dict[str, MessageUsage], self.content) if self.type == ChatResponseType.USAGE else None
203+
167204

168205
class ChatMessageRequest(BaseModel):
169206
"""Client-side chat request interface."""
@@ -230,5 +267,6 @@ class ConfigResponse(BaseModel):
230267
customization: UICustomization | None = Field(default=None, description="UI customization")
231268
user_settings: UserSettings = Field(default_factory=UserSettings, description="User settings")
232269
debug_mode: bool = Field(default=False, description="Debug mode flag")
233-
conversation_history: bool = Field(default=False, description="Debug mode flag")
270+
conversation_history: bool = Field(default=False, description="Flag to enable conversation history")
271+
show_usage: bool = Field(default=False, description="Flag to enable usage statistics")
234272
authentication: AuthenticationConfig = Field(..., description="Authentication configuration")

packages/ragbits-chat/src/ragbits/chat/providers/model_provider.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ def get_models(self) -> dict[str, type[BaseModel | Enum]]:
6565
LiveUpdateType,
6666
Message,
6767
MessageRole,
68+
MessageUsage,
6869
Reference,
6970
StateUpdate,
7071
)
@@ -89,6 +90,7 @@ def get_models(self) -> dict[str, type[BaseModel | Enum]]:
8990
"ServerState": StateUpdate,
9091
"FeedbackItem": FeedbackItem,
9192
"Image": Image,
93+
"MessageUsage": MessageUsage,
9294
# Configuration models
9395
"HeaderCustomization": HeaderCustomization,
9496
"UICustomization": UICustomization,
@@ -145,6 +147,7 @@ def get_categories(self) -> dict[str, list[str]]:
145147
"Image",
146148
"JWTToken",
147149
"User",
150+
"MessageUsage",
148151
],
149152
"configuration": [
150153
"HeaderCustomization",

scripts/generate_typescript_from_json_schema.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ def _generate_chat_response_union_type() -> str:
200200
("FollowupMessagesChatResponse", "followup_messages", "string[]"),
201201
("ImageChatResponse", "image", "Image"),
202202
("ClearMessageResponse", "clear_message", "never"),
203+
("MessageUsageChatResponse", "usage", "Record<string, MessageUsage>"),
203204
]
204205

205206
for interface_name, response_type, content_type in response_interfaces:

typescript/@ragbits/api-client/src/autogen.types.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const ChatResponseType = {
2121
FollowupMessages: 'followup_messages',
2222
Image: 'image',
2323
ClearMessage: 'clear_message',
24+
Usage: 'usage',
2425
} as const
2526

2627
export type ChatResponseType = TypeFrom<typeof ChatResponseType>
@@ -144,6 +145,17 @@ export interface Image {
144145
url: string
145146
}
146147

148+
/**
149+
* Represents usage for a message. Reflects `Usage` computed properties.
150+
*/
151+
export interface MessageUsage {
152+
n_requests: number
153+
estimated_cost: number
154+
prompt_tokens: number
155+
completion_tokens: number
156+
total_tokens: number
157+
}
158+
147159
/**
148160
* Customization for the header section of the UI.
149161
*/
@@ -227,9 +239,13 @@ export interface ConfigResponse {
227239
*/
228240
debug_mode: boolean
229241
/**
230-
* Debug mode flag
242+
* Flag to enable conversation history
231243
*/
232244
conversation_history: boolean
245+
/**
246+
* Flag to enable usage statistics
247+
*/
248+
show_usage: boolean
233249
authentication: AuthenticationConfig
234250
}
235251

@@ -430,6 +446,11 @@ interface ClearMessageResponse {
430446
content: never
431447
}
432448

449+
interface MessageUsageChatResponse {
450+
type: 'usage'
451+
content: Record<string, MessageUsage>
452+
}
453+
433454
/**
434455
* Typed chat response union
435456
*/
@@ -443,3 +464,4 @@ export type ChatResponse =
443464
| FollowupMessagesChatResponse
444465
| ImageChatResponse
445466
| ClearMessageResponse
467+
| MessageUsageChatResponse

0 commit comments

Comments
 (0)