Skip to content

Commit bb0557c

Browse files
authored
refactor: simplify ragbits-chat client (#708)
1 parent 6647618 commit bb0557c

File tree

17 files changed

+417
-597
lines changed

17 files changed

+417
-597
lines changed

docs/how-to/chatbots/api.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -277,10 +277,10 @@ The Ragbits ecosystem provides an official Python client that makes it easy to i
277277

278278
```python
279279
import asyncio
280-
from ragbits.chat.clients import AsyncRagbitsChatClient, RagbitsChatClient
280+
from ragbits.chat.client import RagbitsChatClient, SyncRagbitsChatClient
281281

282282
async def main() -> None:
283-
async with AsyncRagbitsChatClient(base_url="http://127.0.0.1:8000") as client:
283+
async with RagbitsChatClient(base_url="http://127.0.0.1:8000") as client:
284284
# One-shot request - collect all text responses
285285
responses = await client.run("What's the weather like in Paris?")
286286
text_content = "".join(chunk.as_text() or "" for chunk in responses)
@@ -294,7 +294,7 @@ async def main() -> None:
294294
print()
295295

296296
# Synchronous client
297-
sync_client = RagbitsChatClient(base_url="http://127.0.0.1:8000")
297+
sync_client = SyncRagbitsChatClient(base_url="http://127.0.0.1:8000")
298298

299299
# Simple one-off request
300300
responses = sync_client.run("Hello, Ragbits!")
@@ -317,10 +317,10 @@ The client returns `ChatResponse` objects that can contain different types of co
317317

318318
```python
319319
import asyncio
320-
from ragbits.chat.clients import AsyncRagbitsChatClient
320+
from ragbits.chat.client import RagbitsChatClient
321321

322322
async def handle_responses() -> None:
323-
async with AsyncRagbitsChatClient(base_url="http://127.0.0.1:8000") as client:
323+
async with RagbitsChatClient(base_url="http://127.0.0.1:8000") as client:
324324
conv = client.new_conversation()
325325

326326
async for chunk in conv.run_streaming("Tell me about Python"):
Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
from .clients import (
2-
AsyncConversation,
3-
AsyncRagbitsChatClient,
4-
Conversation,
1+
from .client import (
52
RagbitsChatClient,
3+
RagbitsConversation,
4+
SyncRagbitsChatClient,
5+
SyncRagbitsConversation,
66
)
77
from .interface.types import (
88
ChatResponse,
@@ -14,14 +14,14 @@
1414
)
1515

1616
__all__ = [
17-
"AsyncConversation",
18-
"AsyncRagbitsChatClient",
1917
"ChatResponse",
2018
"ChatResponseType",
21-
"Conversation",
2219
"Message",
2320
"MessageRole",
2421
"RagbitsChatClient",
22+
"RagbitsConversation",
2523
"Reference",
2624
"StateUpdate",
25+
"SyncRagbitsChatClient",
26+
"SyncRagbitsConversation",
2727
]
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from .client import RagbitsChatClient, SyncRagbitsChatClient
2+
from .conversation import RagbitsConversation, SyncRagbitsConversation
3+
from .exceptions import ChatClientRequestError, ChatClientResponseError
4+
5+
__all__ = [
6+
"ChatClientRequestError",
7+
"ChatClientResponseError",
8+
"RagbitsChatClient",
9+
"RagbitsConversation",
10+
"SyncRagbitsChatClient",
11+
"SyncRagbitsConversation",
12+
]
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import AsyncGenerator, Generator
4+
from types import TracebackType
5+
from typing import Any
6+
7+
import httpx
8+
9+
from ..interface.types import ChatResponse
10+
from .conversation import RagbitsConversation, SyncRagbitsConversation
11+
12+
__all__ = ["RagbitsChatClient", "SyncRagbitsChatClient"]
13+
14+
_DEFAULT_HEADERS = {
15+
"Content-Type": "application/json",
16+
"Accept": "text/event-stream",
17+
}
18+
19+
20+
class RagbitsChatClient:
21+
"""Stateless **asynchronous** Ragbits chat client."""
22+
23+
def __init__(
24+
self,
25+
base_url: str = "http://127.0.0.1:8000",
26+
*,
27+
timeout: float | None = None,
28+
) -> None:
29+
self._base_url = base_url.rstrip("/")
30+
self._client = httpx.AsyncClient(timeout=timeout, headers=_DEFAULT_HEADERS)
31+
32+
def new_conversation(self) -> RagbitsConversation:
33+
"""Return a brand-new RagbitsConversation."""
34+
return RagbitsConversation(base_url=self._base_url, http_client=self._client)
35+
36+
async def aclose(self) -> None:
37+
"""Close the underlying *httpx.AsyncClient* session."""
38+
await self._client.aclose()
39+
40+
async def run(
41+
self,
42+
message: str,
43+
*,
44+
context: dict[str, Any] | None = None,
45+
) -> list[ChatResponse]:
46+
"""Send *message* and return **all** response chunks."""
47+
conv = self.new_conversation()
48+
return await conv.run(message, context=context)
49+
50+
async def run_streaming(
51+
self,
52+
message: str,
53+
*,
54+
context: dict[str, Any] | None = None,
55+
) -> AsyncGenerator[ChatResponse, None]:
56+
"""Send *message* and yield streaming :class:`ChatResponse` chunks."""
57+
conv = self.new_conversation()
58+
async for chunk in conv.run_streaming(message, context=context):
59+
yield chunk
60+
61+
async def __aenter__(self) -> RagbitsChatClient:
62+
"""Return *self* inside an ``async with`` block."""
63+
return self
64+
65+
async def __aexit__(
66+
self,
67+
exc_type: type[BaseException] | None,
68+
exc: BaseException | None,
69+
tb: TracebackType | None,
70+
) -> None:
71+
"""Ensure the underlying async HTTP session is closed on exit."""
72+
await self.aclose()
73+
74+
75+
class SyncRagbitsChatClient:
76+
"""Stateless *synchronous* Ragbits chat client.
77+
78+
The sole responsibility of this class is to spawn new
79+
SyncRagbitsConversation objects. All
80+
conversation-specific state (history, server-state, etc.) lives in the
81+
returned *SyncRagbitsConversation* instance.
82+
"""
83+
84+
def __init__(
85+
self,
86+
base_url: str = "http://127.0.0.1:8000",
87+
*,
88+
timeout: float | None = None,
89+
) -> None:
90+
self._base_url = base_url.rstrip("/")
91+
self._client = httpx.Client(timeout=timeout, headers=_DEFAULT_HEADERS)
92+
93+
def new_conversation(self) -> SyncRagbitsConversation:
94+
"""Return a brand-new SyncRagbitsConversation."""
95+
return SyncRagbitsConversation(base_url=self._base_url, http_client=self._client)
96+
97+
def close(self) -> None:
98+
"""Close the underlying *httpx.Client* session."""
99+
self._client.close()
100+
101+
def run(
102+
self,
103+
message: str,
104+
*,
105+
context: dict[str, Any] | None = None,
106+
) -> list[ChatResponse]:
107+
"""Send *message* and return **all** response chunks."""
108+
conv = self.new_conversation()
109+
return conv.run(message, context=context)
110+
111+
def run_streaming(
112+
self,
113+
message: str,
114+
*,
115+
context: dict[str, Any] | None = None,
116+
) -> Generator[ChatResponse, None, None]:
117+
"""Send *message* and yield streaming :class:`ChatResponse` chunks."""
118+
conv = self.new_conversation()
119+
yield from conv.run_streaming(message, context=context)
120+
121+
def __enter__(self) -> SyncRagbitsChatClient:
122+
"""Return *self* to allow usage via the *with* statement."""
123+
return self
124+
125+
def __exit__(
126+
self,
127+
exc_type: type[BaseException] | None,
128+
exc: BaseException | None,
129+
tb: TracebackType | None,
130+
) -> None:
131+
"""Ensure the underlying HTTP session is closed on context exit."""
132+
self.close()

0 commit comments

Comments
 (0)