pylogue 0.1.0__py3-none-any.whl → 0.2.0__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/_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
@@ -78,15 +78,7 @@ class ChatAppConfig:
78
78
 
79
79
  # %% ../../nbs/4-ChatApp.ipynb 4
80
80
  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
- """
81
+ """Main chat application composing all components."""
90
82
 
91
83
  def __init__(
92
84
  self,
@@ -96,7 +88,7 @@ class ChatApp:
96
88
  config: Optional[ChatAppConfig] = None,
97
89
  ):
98
90
  """
99
- Initialize ChatApp with injected dependencies.
91
+ Initialize ChatApp with dependency injection.
100
92
 
101
93
  Args:
102
94
  session_manager: Manages chat sessions
@@ -155,12 +147,11 @@ class ChatApp:
155
147
  H1(self.config.app_title, style=self.config.header_style),
156
148
  self.renderer.render_messages(initial_messages),
157
149
  self.renderer.render_form(),
158
- hx_ext="ws",
159
- ws_connect=self.config.ws_endpoint,
160
150
  style=container_style,
161
151
  ),
162
152
  )
163
153
 
154
+ # Register WebSocket route with message handler
164
155
  @self.app.ws(
165
156
  self.config.ws_endpoint, conn=self._on_connect, disconn=self._on_disconnect
166
157
  )
@@ -183,12 +174,11 @@ class ChatApp:
183
174
 
184
175
  async def _handle_websocket_message(self, msg: str, send, ws):
185
176
  """
186
- Handle incoming WebSocket message with full lifecycle:
177
+ Handle incoming WebSocket message with streaming support:
187
178
  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
179
+ 2. Add empty assistant message for streaming
180
+ 3. Stream response tokens and update message progressively
181
+ 4. Clear input
192
182
  """
193
183
  session_id = str(id(ws))
194
184
  session = self.session_manager.get_session(session_id)
@@ -204,21 +194,32 @@ class ChatApp:
204
194
  session.add_message("User", msg)
205
195
  await send(self.renderer.render_messages(session.get_messages()))
206
196
 
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()))
197
+ # Step 2: Add empty assistant message for streaming
198
+ assistant_msg = session.add_message("Assistant", "", pending=False)
210
199
 
211
- # Step 3: Process message with chat service
200
+ # Step 3: Stream response and update progressively
212
201
  try:
213
- response = await self.chat_service.process_message(msg, session)
214
- except Exception as e:
215
- response = f"Error: {str(e)}"
202
+ response_chunks = []
203
+ chunk_count = 0
204
+ async for chunk in self.chat_service.process_message_stream(msg, session):
205
+ response_chunks.append(chunk)
206
+ chunk_count += 1
207
+ # Update the assistant message with accumulated response
208
+ full_response = "".join(response_chunks)
209
+ session.update_message(
210
+ assistant_msg.id, content=full_response, pending=False
211
+ )
212
+ # Send updated message list to UI
213
+ print(f"📤 Sending chunk #{chunk_count}: {repr(chunk)}") # Debug
214
+ await send(self.renderer.render_messages(session.get_messages()))
216
215
 
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()))
216
+ except Exception as e:
217
+ # Handle errors
218
+ error_msg = f"Error: {str(e)}"
219
+ session.update_message(assistant_msg.id, content=error_msg, pending=False)
220
+ await send(self.renderer.render_messages(session.get_messages()))
220
221
 
221
- # Step 5: Clear input field
222
+ # Step 4: Clear input field
222
223
  await send(self.renderer.render_input())
223
224
 
224
225
  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.0
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=5_YsGHUfQ5L1Eh4uZjSnsgszN9JSrWnO1VwEzBGN6Z4,273
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=khtkhbjWvkZIHmEYQPLogFlDeK5KjItWXOHVC6_9tkg,9918
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.0.dist-info/licenses/AUTHORS.md,sha256=5Viska6TOr9uggc2olSr8VVnu6BFw_F6_64gP42CwQ4,157
12
+ pylogue-0.2.0.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ pylogue-0.2.0.dist-info/METADATA,sha256=upCfS03poXnbZyeCISnAKqKmsvIORSyhEWSPtLJOEfk,1059
14
+ pylogue-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
15
+ pylogue-0.2.0.dist-info/entry_points.txt,sha256=ha0gwcGgtciKEhmFcMKJO1efTY9Spu1wcTHUopB1bec,40
16
+ pylogue-0.2.0.dist-info/top_level.txt,sha256=oEueWVdlRAUPQt8VfQdqFEqOWxvUObx3UIa3UYa3s6o,8
17
+ pylogue-0.2.0.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,,