pylogue 0.1.0__py3-none-any.whl → 0.2.1__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.
pylogue/__init__.py CHANGED
@@ -14,3 +14,5 @@ __version__ = "0.1.0"
14
14
 
15
15
  from .__pre_init__ import cli
16
16
  from .health import *
17
+
18
+ from .chatapp import create_default_chat_app
pylogue/_modidx.py CHANGED
@@ -45,8 +45,6 @@ d = { 'settings': { 'branch': 'main',
45
45
  'pylogue.renderer': { 'pylogue.renderer.ChatRenderer': ('3-renderer.html#chatrenderer', 'pylogue/renderer.py'),
46
46
  'pylogue.renderer.ChatRenderer.__init__': ( '3-renderer.html#chatrenderer.__init__',
47
47
  'pylogue/renderer.py'),
48
- 'pylogue.renderer.ChatRenderer.render_chat_interface': ( '3-renderer.html#chatrenderer.render_chat_interface',
49
- 'pylogue/renderer.py'),
50
48
  'pylogue.renderer.ChatRenderer.render_form': ( '3-renderer.html#chatrenderer.render_form',
51
49
  'pylogue/renderer.py'),
52
50
  'pylogue.renderer.ChatRenderer.render_input': ( '3-renderer.html#chatrenderer.render_input',
@@ -59,8 +57,12 @@ d = { 'settings': { 'branch': 'main',
59
57
  'pylogue/renderer.py')},
60
58
  'pylogue.service': { 'pylogue.service.ChatService': ('2-service.html#chatservice', 'pylogue/service.py'),
61
59
  'pylogue.service.ChatService.__init__': ('2-service.html#chatservice.__init__', 'pylogue/service.py'),
60
+ 'pylogue.service.ChatService._is_async_generator': ( '2-service.html#chatservice._is_async_generator',
61
+ 'pylogue/service.py'),
62
62
  'pylogue.service.ChatService.process_message': ( '2-service.html#chatservice.process_message',
63
63
  'pylogue/service.py'),
64
+ 'pylogue.service.ChatService.process_message_stream': ( '2-service.html#chatservice.process_message_stream',
65
+ 'pylogue/service.py'),
64
66
  'pylogue.service.ChatService.process_session_message': ( '2-service.html#chatservice.process_session_message',
65
67
  'pylogue/service.py'),
66
68
  'pylogue.service.ContextAwareResponder': ('2-service.html#contextawareresponder', 'pylogue/service.py'),
pylogue/chatapp.py CHANGED
@@ -34,6 +34,7 @@ class ChatAppConfig:
34
34
 
35
35
  # WebSocket settings
36
36
  ws_endpoint: str = "/ws"
37
+ chat_endpoint: str = "/chat"
37
38
 
38
39
  # FastHTML extensions and headers
39
40
  markdown_enabled: bool = True
@@ -78,15 +79,7 @@ class ChatAppConfig:
78
79
 
79
80
  # %% ../../nbs/4-ChatApp.ipynb 4
80
81
  class ChatApp:
81
- """
82
- Complete chat application with full dependency injection.
83
-
84
- This is the main orchestration layer that ties together:
85
- - Session management (state)
86
- - Chat service (business logic)
87
- - Renderer (presentation)
88
- - FastHTML + WebSocket (infrastructure)
89
- """
82
+ """Main chat application composing all components."""
90
83
 
91
84
  def __init__(
92
85
  self,
@@ -96,7 +89,7 @@ class ChatApp:
96
89
  config: Optional[ChatAppConfig] = None,
97
90
  ):
98
91
  """
99
- Initialize ChatApp with injected dependencies.
92
+ Initialize ChatApp with dependency injection.
100
93
 
101
94
  Args:
102
95
  session_manager: Manages chat sessions
@@ -139,7 +132,7 @@ class ChatApp:
139
132
  def _register_routes(self):
140
133
  """Register HTTP and WebSocket routes."""
141
134
 
142
- @self.app.route("/")
135
+ @self.app.route(self.config.chat_endpoint)
143
136
  def home():
144
137
  """Main chat interface."""
145
138
  initial_messages = self._get_initial_messages()
@@ -155,12 +148,11 @@ class ChatApp:
155
148
  H1(self.config.app_title, style=self.config.header_style),
156
149
  self.renderer.render_messages(initial_messages),
157
150
  self.renderer.render_form(),
158
- hx_ext="ws",
159
- ws_connect=self.config.ws_endpoint,
160
151
  style=container_style,
161
152
  ),
162
153
  )
163
154
 
155
+ # Register WebSocket route with message handler
164
156
  @self.app.ws(
165
157
  self.config.ws_endpoint, conn=self._on_connect, disconn=self._on_disconnect
166
158
  )
@@ -183,12 +175,11 @@ class ChatApp:
183
175
 
184
176
  async def _handle_websocket_message(self, msg: str, send, ws):
185
177
  """
186
- Handle incoming WebSocket message with full lifecycle:
178
+ Handle incoming WebSocket message with streaming support:
187
179
  1. Add user message
188
- 2. Show pending assistant message with spinner
189
- 3. Process with chat service
190
- 4. Update with actual response
191
- 5. Clear input
180
+ 2. Add empty assistant message for streaming
181
+ 3. Stream response tokens and update message progressively
182
+ 4. Clear input
192
183
  """
193
184
  session_id = str(id(ws))
194
185
  session = self.session_manager.get_session(session_id)
@@ -204,21 +195,32 @@ class ChatApp:
204
195
  session.add_message("User", msg)
205
196
  await send(self.renderer.render_messages(session.get_messages()))
206
197
 
207
- # Step 2: Add pending assistant message with spinner
208
- pending_msg = session.add_message("Assistant", "", pending=True)
209
- await send(self.renderer.render_messages(session.get_messages()))
198
+ # Step 2: Add empty assistant message for streaming
199
+ assistant_msg = session.add_message("Assistant", "", pending=False)
210
200
 
211
- # Step 3: Process message with chat service
201
+ # Step 3: Stream response and update progressively
212
202
  try:
213
- response = await self.chat_service.process_message(msg, session)
214
- except Exception as e:
215
- response = f"Error: {str(e)}"
203
+ response_chunks = []
204
+ chunk_count = 0
205
+ async for chunk in self.chat_service.process_message_stream(msg, session):
206
+ response_chunks.append(chunk)
207
+ chunk_count += 1
208
+ # Update the assistant message with accumulated response
209
+ full_response = "".join(response_chunks)
210
+ session.update_message(
211
+ assistant_msg.id, content=full_response, pending=False
212
+ )
213
+ # Send updated message list to UI
214
+ print(f"📤 Sending chunk #{chunk_count}: {repr(chunk)}") # Debug
215
+ await send(self.renderer.render_messages(session.get_messages()))
216
216
 
217
- # Step 4: Update pending message with actual response
218
- session.update_message(pending_msg.id, content=response, pending=False)
219
- await send(self.renderer.render_messages(session.get_messages()))
217
+ except Exception as e:
218
+ # Handle errors
219
+ error_msg = f"Error: {str(e)}"
220
+ session.update_message(assistant_msg.id, content=error_msg, pending=False)
221
+ await send(self.renderer.render_messages(session.get_messages()))
220
222
 
221
- # Step 5: Clear input field
223
+ # Step 4: Clear input field
222
224
  await send(self.renderer.render_input())
223
225
 
224
226
  def run(
pylogue/renderer.py CHANGED
@@ -21,6 +21,7 @@ class ChatRenderer:
21
21
  input_placeholder: str = "Type a message...",
22
22
  input_style: Optional[str] = None,
23
23
  chat_container_style: Optional[str] = None,
24
+ ws_endpoint: str = "/ws",
24
25
  ):
25
26
  """
26
27
  Initialize ChatRenderer.
@@ -30,9 +31,11 @@ class ChatRenderer:
30
31
  input_placeholder: Placeholder text for input field
31
32
  input_style: Custom CSS style for input field
32
33
  chat_container_style: Custom CSS style for chat container
34
+ ws_endpoint: WebSocket endpoint path
33
35
  """
34
36
  self.card = card or ChatCard()
35
37
  self.input_placeholder = input_placeholder
38
+ self.ws_endpoint = ws_endpoint
36
39
  self.input_style = input_style or (
37
40
  "width: 60%; max-width: 600px; padding: 0.75em; "
38
41
  "font-size: 1em; border-radius: 0.5em"
@@ -100,7 +103,7 @@ class ChatRenderer:
100
103
  ws_send: bool = True,
101
104
  ) -> Any:
102
105
  """
103
- Render the input form with styling.
106
+ Render the input form with WebSocket connection.
104
107
 
105
108
  Args:
106
109
  form_id: HTML ID for the form
@@ -118,34 +121,8 @@ class ChatRenderer:
118
121
  return Form(
119
122
  self.render_input(),
120
123
  id=form_id,
124
+ hx_ext="ws",
125
+ ws_connect=self.ws_endpoint,
121
126
  ws_send=ws_send,
122
127
  style=form_style,
123
128
  )
124
-
125
- def render_chat_interface(
126
- self,
127
- messages: List[Message],
128
- title: str = "Chat",
129
- header_style: Optional[str] = None,
130
- container_style: Optional[str] = None,
131
- ) -> Any:
132
- """
133
- Render complete chat interface with messages and input.
134
-
135
- Args:
136
- messages: List of messages to display
137
- title: Chat title/header
138
- header_style: Custom style for header
139
- container_style: Custom style for main container
140
-
141
- Returns:
142
- FastHTML Div with complete chat interface
143
- """
144
- header_style = header_style or "text-align: center; padding: 1em;"
145
-
146
- return Div(
147
- H1(title, style=header_style),
148
- self.render_messages(messages),
149
- self.render_form(),
150
- style=container_style,
151
- )
pylogue/service.py CHANGED
@@ -60,6 +60,12 @@ class ChatService:
60
60
  self.error_handler = error_handler or DefaultErrorHandler()
61
61
  self.context_provider = context_provider
62
62
 
63
+ def _is_async_generator(self, obj):
64
+ """Check if object is an async generator."""
65
+ return inspect.isasyncgenfunction(obj) or (
66
+ hasattr(obj, "__call__") and inspect.isasyncgenfunction(obj.__call__)
67
+ )
68
+
63
69
  async def process_message(
64
70
  self, user_message: str, session: Optional[ChatSession] = None
65
71
  ) -> str:
@@ -79,7 +85,15 @@ class ChatService:
79
85
  if self.context_provider and session:
80
86
  context = self.context_provider(session)
81
87
 
82
- # Call responder
88
+ # Check if responder is an async generator (streaming)
89
+ if self._is_async_generator(self.responder):
90
+ # For streaming responders, collect all chunks
91
+ chunks = []
92
+ async for chunk in self.responder(user_message, context):
93
+ chunks.append(str(chunk))
94
+ return "".join(chunks)
95
+
96
+ # Non-streaming responder
83
97
  if inspect.iscoroutinefunction(self.responder):
84
98
  result = await self.responder(user_message, context)
85
99
  else:
@@ -92,6 +106,42 @@ class ChatService:
92
106
  except Exception as e:
93
107
  return self.error_handler(e, user_message)
94
108
 
109
+ async def process_message_stream(
110
+ self, user_message: str, session: Optional[ChatSession] = None
111
+ ):
112
+ """
113
+ Process a user message and stream the response token by token.
114
+
115
+ Args:
116
+ user_message: The user's input message
117
+ session: Optional chat session for context
118
+
119
+ Yields:
120
+ Response chunks as they are generated
121
+ """
122
+ try:
123
+ # Extract context if provider exists
124
+ context = None
125
+ if self.context_provider and session:
126
+ context = self.context_provider(session)
127
+
128
+ # Check if responder supports streaming
129
+ if self._is_async_generator(self.responder):
130
+ async for chunk in self.responder(user_message, context):
131
+ yield str(chunk)
132
+ else:
133
+ # Non-streaming responder - yield full response
134
+ if inspect.iscoroutinefunction(self.responder):
135
+ result = await self.responder(user_message, context)
136
+ else:
137
+ result = self.responder(user_message, context)
138
+ if inspect.isawaitable(result):
139
+ result = await result
140
+ yield str(result)
141
+
142
+ except Exception as e:
143
+ yield self.error_handler(e, user_message)
144
+
95
145
  async def process_session_message(
96
146
  self, session: ChatSession, user_message: str, add_to_session: bool = True
97
147
  ) -> Message:
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pylogue
3
- Version: 0.1.0
4
- Summary: A Chatbot UI build for Pydantic-AI agents
3
+ Version: 0.2.1
4
+ Summary: A Chatbot UI built for AI agents
5
5
  Author-email: Yeshwanth Reddy <yyeshr@gmail.com>
6
6
  Maintainer-email: Yeshwanth Reddy <yyeshr@gmail.com>
7
7
  License: MIT license
@@ -0,0 +1,17 @@
1
+ pylogue/__init__.py,sha256=gDXS44jmg6hPtcg8IxTFRdyV-TGsjQTkT-5NjiapYZ4,319
2
+ pylogue/__pre_init__.py,sha256=NiQUAz7feHFR47QTpMwF324j61IVrOreZsk9wY11cds,153
3
+ pylogue/_modidx.py,sha256=apvAlYvaQgShSh_ZrX7fXSpxDdwPBYIKwEQWdEkVJg8,14209
4
+ pylogue/cards.py,sha256=wsOVNbMczFbyoATkujC7xJAeskYgTqsJmxmkHqzHA-g,5385
5
+ pylogue/chat.py,sha256=RyRADCZWxTjCM3W7FgIj0jQ8WrqhRBzbeFEvS9mTZq4,3982
6
+ pylogue/chatapp.py,sha256=Rn2GMmo7GTatsnAdIuYFTalLMb9qTxS1BmikQUVlE3Y,9973
7
+ pylogue/health.py,sha256=8LWhpVzdj2g6qUcErrg455_6WfOsv3lp82OLLwJPtug,443
8
+ pylogue/renderer.py,sha256=KGo0l-Jo8i2pe8NqwlZsM9Qo3ZBA2EV-osrHnBUWzBA,3862
9
+ pylogue/service.py,sha256=e3GfbQrOPlRIj7j0CZZx_qN2ZI_polOuWqBMm7ySXqs,6917
10
+ pylogue/session.py,sha256=fUyGOFIG8b1clhyyw5c_dapdwrJyzd9K2bOc_qKBQcs,5382
11
+ pylogue-0.2.1.dist-info/licenses/AUTHORS.md,sha256=5Viska6TOr9uggc2olSr8VVnu6BFw_F6_64gP42CwQ4,157
12
+ pylogue-0.2.1.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ pylogue-0.2.1.dist-info/METADATA,sha256=efMS8zmtNQAeAlcthiuk8oxhkTh5OeYn6Z7kF-ft_vg,1059
14
+ pylogue-0.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
15
+ pylogue-0.2.1.dist-info/entry_points.txt,sha256=ha0gwcGgtciKEhmFcMKJO1efTY9Spu1wcTHUopB1bec,40
16
+ pylogue-0.2.1.dist-info/top_level.txt,sha256=oEueWVdlRAUPQt8VfQdqFEqOWxvUObx3UIa3UYa3s6o,8
17
+ pylogue-0.2.1.dist-info/RECORD,,
@@ -1,17 +0,0 @@
1
- pylogue/__init__.py,sha256=5_YsGHUfQ5L1Eh4uZjSnsgszN9JSrWnO1VwEzBGN6Z4,273
2
- pylogue/__pre_init__.py,sha256=NiQUAz7feHFR47QTpMwF324j61IVrOreZsk9wY11cds,153
3
- pylogue/_modidx.py,sha256=S-NkgQExGm8mDtWiZztOY9DhMtvieuDNXg1PG8RFTMc,13970
4
- pylogue/cards.py,sha256=wsOVNbMczFbyoATkujC7xJAeskYgTqsJmxmkHqzHA-g,5385
5
- pylogue/chat.py,sha256=RyRADCZWxTjCM3W7FgIj0jQ8WrqhRBzbeFEvS9mTZq4,3982
6
- pylogue/chatapp.py,sha256=V0edaNeQXy2wKwDr5MZ5IcKC3a_AakXPcfN4sqGpDhU,9643
7
- pylogue/health.py,sha256=8LWhpVzdj2g6qUcErrg455_6WfOsv3lp82OLLwJPtug,443
8
- pylogue/renderer.py,sha256=7ZiGNJ83xwCVgvw4pnEEJPB3E9lO60vK6qobAttryNk,4507
9
- pylogue/service.py,sha256=0hdL8Eg7FDpX_NXSuTzC7bJ_wvEnnRsomDR-kNrs2i4,4952
10
- pylogue/session.py,sha256=fUyGOFIG8b1clhyyw5c_dapdwrJyzd9K2bOc_qKBQcs,5382
11
- pylogue-0.1.0.dist-info/licenses/AUTHORS.md,sha256=5Viska6TOr9uggc2olSr8VVnu6BFw_F6_64gP42CwQ4,157
12
- pylogue-0.1.0.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- pylogue-0.1.0.dist-info/METADATA,sha256=0qS4tbqcJYe-ZmLOc9LX2NAG1LTmnWHmghaKjKHUCbM,1068
14
- pylogue-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
15
- pylogue-0.1.0.dist-info/entry_points.txt,sha256=ha0gwcGgtciKEhmFcMKJO1efTY9Spu1wcTHUopB1bec,40
16
- pylogue-0.1.0.dist-info/top_level.txt,sha256=oEueWVdlRAUPQt8VfQdqFEqOWxvUObx3UIa3UYa3s6o,8
17
- pylogue-0.1.0.dist-info/RECORD,,