chat-console 0.2.8__py3-none-any.whl → 0.2.9__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.
- app/__init__.py +1 -1
- app/api/ollama.py +46 -14
- app/main.py +27 -12
- app/ui/chat_interface.py +22 -3
- app/utils.py +79 -18
- {chat_console-0.2.8.dist-info → chat_console-0.2.9.dist-info}/METADATA +1 -1
- {chat_console-0.2.8.dist-info → chat_console-0.2.9.dist-info}/RECORD +11 -11
- {chat_console-0.2.8.dist-info → chat_console-0.2.9.dist-info}/WHEEL +0 -0
- {chat_console-0.2.8.dist-info → chat_console-0.2.9.dist-info}/entry_points.txt +0 -0
- {chat_console-0.2.8.dist-info → chat_console-0.2.9.dist-info}/licenses/LICENSE +0 -0
- {chat_console-0.2.8.dist-info → chat_console-0.2.9.dist-info}/top_level.txt +0 -0
app/__init__.py
CHANGED
app/api/ollama.py
CHANGED
@@ -22,6 +22,9 @@ class OllamaClient(BaseModelClient):
|
|
22
22
|
# Track active stream session
|
23
23
|
self._active_stream_session = None
|
24
24
|
|
25
|
+
# Track model loading state
|
26
|
+
self._model_loading = False
|
27
|
+
|
25
28
|
# Path to the cached models file
|
26
29
|
self.models_cache_path = Path(__file__).parent.parent / "data" / "ollama-models.json"
|
27
30
|
|
@@ -191,6 +194,10 @@ class OllamaClient(BaseModelClient):
|
|
191
194
|
raise aiohttp.ClientError("Model not ready")
|
192
195
|
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
|
193
196
|
logger.info(f"Model cold start detected: {str(e)}")
|
197
|
+
# Set model loading flag
|
198
|
+
self._model_loading = True
|
199
|
+
logger.info("Setting model_loading state to True")
|
200
|
+
|
194
201
|
# Model might need loading, try pulling it
|
195
202
|
async with session.post(
|
196
203
|
f"{self.base_url}/api/pull",
|
@@ -199,8 +206,10 @@ class OllamaClient(BaseModelClient):
|
|
199
206
|
) as pull_response:
|
200
207
|
if pull_response.status != 200:
|
201
208
|
logger.error("Failed to pull model")
|
209
|
+
self._model_loading = False # Reset flag on failure
|
202
210
|
raise Exception("Failed to pull model")
|
203
211
|
logger.info("Model pulled successfully")
|
212
|
+
self._model_loading = False # Reset flag after successful pull
|
204
213
|
|
205
214
|
# Now proceed with actual generation
|
206
215
|
session = aiohttp.ClientSession()
|
@@ -208,7 +217,7 @@ class OllamaClient(BaseModelClient):
|
|
208
217
|
|
209
218
|
try:
|
210
219
|
logger.debug(f"Sending streaming request to {self.base_url}/api/generate")
|
211
|
-
|
220
|
+
response = await session.post(
|
212
221
|
f"{self.base_url}/api/generate",
|
213
222
|
json={
|
214
223
|
"model": model,
|
@@ -217,19 +226,36 @@ class OllamaClient(BaseModelClient):
|
|
217
226
|
"stream": True
|
218
227
|
},
|
219
228
|
timeout=60 # Longer timeout for actual generation
|
220
|
-
)
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
229
|
+
)
|
230
|
+
response.raise_for_status()
|
231
|
+
|
232
|
+
# Process the response stream
|
233
|
+
while True:
|
234
|
+
if not self._active_stream_session:
|
235
|
+
logger.info("Stream session was closed externally")
|
236
|
+
break
|
237
|
+
|
238
|
+
try:
|
239
|
+
line = await asyncio.wait_for(response.content.readline(), timeout=0.5)
|
240
|
+
if not line: # End of stream
|
241
|
+
break
|
242
|
+
|
243
|
+
chunk = line.decode().strip()
|
244
|
+
try:
|
245
|
+
data = json.loads(chunk)
|
246
|
+
if "response" in data:
|
247
|
+
yield data["response"]
|
248
|
+
except json.JSONDecodeError:
|
249
|
+
continue
|
250
|
+
except asyncio.TimeoutError:
|
251
|
+
# This allows checking for cancellation regularly
|
252
|
+
continue
|
253
|
+
except asyncio.CancelledError:
|
254
|
+
logger.info("Stream processing was cancelled")
|
255
|
+
raise
|
256
|
+
|
257
|
+
logger.info("Streaming completed successfully")
|
258
|
+
return
|
233
259
|
finally:
|
234
260
|
self._active_stream_session = None # Clear reference when done
|
235
261
|
await session.close() # Ensure session is closed
|
@@ -260,6 +286,12 @@ class OllamaClient(BaseModelClient):
|
|
260
286
|
logger.info("Cancelling active stream session")
|
261
287
|
await self._active_stream_session.close()
|
262
288
|
self._active_stream_session = None
|
289
|
+
self._model_loading = False
|
290
|
+
logger.info("Stream session closed successfully")
|
291
|
+
|
292
|
+
def is_loading_model(self) -> bool:
|
293
|
+
"""Check if Ollama is currently loading a model"""
|
294
|
+
return self._model_loading
|
263
295
|
|
264
296
|
async def get_model_details(self, model_id: str) -> Dict[str, Any]:
|
265
297
|
"""Get detailed information about a specific Ollama model"""
|
app/main.py
CHANGED
@@ -477,30 +477,45 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
|
|
477
477
|
log("Attempting to cancel generation task")
|
478
478
|
if self.current_generation_task and not self.current_generation_task.done():
|
479
479
|
log("Cancelling active generation task.")
|
480
|
-
|
480
|
+
|
481
|
+
# Get the client for the current model first and cancel the connection
|
481
482
|
try:
|
482
483
|
model = self.selected_model
|
483
484
|
client = BaseModelClient.get_client_for_model(model)
|
484
|
-
|
485
|
+
|
486
|
+
# Call the client's cancel method if it's supported
|
485
487
|
if hasattr(client, 'cancel_stream'):
|
486
488
|
log("Calling client.cancel_stream() to terminate API session")
|
487
|
-
|
489
|
+
try:
|
490
|
+
# This will close the HTTP connection to Ollama server
|
491
|
+
await client.cancel_stream()
|
492
|
+
log("Client stream cancelled successfully")
|
493
|
+
except Exception as e:
|
494
|
+
log.error(f"Error in client.cancel_stream(): {str(e)}")
|
495
|
+
except Exception as e:
|
496
|
+
log.error(f"Error setting up client cancellation: {str(e)}")
|
497
|
+
|
498
|
+
# Now cancel the asyncio task - this should raise CancelledError in the task
|
499
|
+
try:
|
500
|
+
log("Cancelling asyncio task")
|
501
|
+
self.current_generation_task.cancel()
|
502
|
+
# Give a moment for cancellation to propagate
|
503
|
+
await asyncio.sleep(0.1)
|
504
|
+
log(f"Task cancelled. Task done: {self.current_generation_task.done()}")
|
488
505
|
except Exception as e:
|
489
|
-
log.error(f"Error cancelling
|
506
|
+
log.error(f"Error cancelling task: {str(e)}")
|
490
507
|
|
491
|
-
#
|
492
|
-
self.
|
493
|
-
# The finally block in generate_response will handle is_generating = False and UI updates
|
494
|
-
self.notify("Stopping generation...", severity="warning", timeout=2) # Notify user immediately
|
508
|
+
# Notify user that we're stopping
|
509
|
+
self.notify("Stopping generation...", severity="warning", timeout=2)
|
495
510
|
else:
|
496
|
-
# This
|
497
|
-
|
511
|
+
# This happens if is_generating is True, but no active task found to cancel
|
512
|
+
log("No active generation task found, but is_generating=True. Resetting state.")
|
513
|
+
self.is_generating = False
|
498
514
|
loading = self.query_one("#loading-indicator")
|
499
515
|
loading.add_class("hidden")
|
500
516
|
else:
|
501
517
|
log("Escape pressed, but settings not visible and not actively generating.")
|
502
|
-
# Optionally add other escape behaviors here if needed
|
503
|
-
# e.g., clear input, deselect item, etc.
|
518
|
+
# Optionally add other escape behaviors here if needed
|
504
519
|
|
505
520
|
def update_app_info(self) -> None:
|
506
521
|
"""Update the displayed app information."""
|
app/ui/chat_interface.py
CHANGED
@@ -204,6 +204,11 @@ class ChatInterface(Container):
|
|
204
204
|
display: none;
|
205
205
|
padding: 0 1;
|
206
206
|
}
|
207
|
+
|
208
|
+
#loading-indicator.model-loading {
|
209
|
+
background: $warning;
|
210
|
+
color: $text;
|
211
|
+
}
|
207
212
|
"""
|
208
213
|
|
209
214
|
class MessageSent(Message):
|
@@ -238,7 +243,7 @@ class ChatInterface(Container):
|
|
238
243
|
yield MessageDisplay(message, highlight_code=CONFIG["highlight_code"])
|
239
244
|
with Container(id="input-area"):
|
240
245
|
yield Container(
|
241
|
-
Label("Generating response...", id="loading-text"),
|
246
|
+
Label("▪▪▪ Generating response...", id="loading-text", markup=True),
|
242
247
|
id="loading-indicator"
|
243
248
|
)
|
244
249
|
with Container(id="controls"):
|
@@ -328,16 +333,30 @@ class ChatInterface(Container):
|
|
328
333
|
if input_widget.has_focus:
|
329
334
|
input_widget.focus()
|
330
335
|
|
331
|
-
def start_loading(self) -> None:
|
332
|
-
"""Show loading indicator
|
336
|
+
def start_loading(self, model_loading: bool = False) -> None:
|
337
|
+
"""Show loading indicator
|
338
|
+
|
339
|
+
Args:
|
340
|
+
model_loading: If True, indicates Ollama is loading a model
|
341
|
+
"""
|
333
342
|
self.is_loading = True
|
334
343
|
loading = self.query_one("#loading-indicator")
|
344
|
+
loading_text = self.query_one("#loading-text")
|
345
|
+
|
346
|
+
if model_loading:
|
347
|
+
loading.add_class("model-loading")
|
348
|
+
loading_text.update("⚙️ Loading Ollama model...")
|
349
|
+
else:
|
350
|
+
loading.remove_class("model-loading")
|
351
|
+
loading_text.update("▪▪▪ Generating response...")
|
352
|
+
|
335
353
|
loading.display = True
|
336
354
|
|
337
355
|
def stop_loading(self) -> None:
|
338
356
|
"""Hide loading indicator"""
|
339
357
|
self.is_loading = False
|
340
358
|
loading = self.query_one("#loading-indicator")
|
359
|
+
loading.remove_class("model-loading")
|
341
360
|
loading.display = False
|
342
361
|
|
343
362
|
def clear_messages(self) -> None:
|
app/utils.py
CHANGED
@@ -86,12 +86,71 @@ async def generate_streaming_response(app: 'SimpleChatApp', messages: List[Dict]
|
|
86
86
|
buffer = []
|
87
87
|
last_update = time.time()
|
88
88
|
update_interval = 0.1 # Update UI every 100ms
|
89
|
-
generation_task = None
|
90
89
|
|
91
90
|
try:
|
92
|
-
#
|
93
|
-
|
94
|
-
|
91
|
+
# Update UI with model loading state if it's an Ollama client
|
92
|
+
if hasattr(client, 'is_loading_model'):
|
93
|
+
# Send signal to update UI for model loading if needed
|
94
|
+
try:
|
95
|
+
# The client might be in model loading state even before generating
|
96
|
+
model_loading = client.is_loading_model()
|
97
|
+
logger.info(f"Initial model loading state: {model_loading}")
|
98
|
+
|
99
|
+
# Get the chat interface and update loading indicator
|
100
|
+
if hasattr(app, 'query_one'):
|
101
|
+
loading = app.query_one("#loading-indicator")
|
102
|
+
if model_loading:
|
103
|
+
loading.add_class("model-loading")
|
104
|
+
app.query_one("#loading-text").update("Loading Ollama model...")
|
105
|
+
else:
|
106
|
+
loading.remove_class("model-loading")
|
107
|
+
except Exception as e:
|
108
|
+
logger.error(f"Error setting initial loading state: {str(e)}")
|
109
|
+
|
110
|
+
stream_generator = client.generate_stream(messages, model, style)
|
111
|
+
|
112
|
+
# Check if we just entered model loading state
|
113
|
+
if hasattr(client, 'is_loading_model') and client.is_loading_model():
|
114
|
+
logger.info("Model loading started during generation")
|
115
|
+
try:
|
116
|
+
if hasattr(app, 'query_one'):
|
117
|
+
loading = app.query_one("#loading-indicator")
|
118
|
+
loading.add_class("model-loading")
|
119
|
+
app.query_one("#loading-text").update("Loading Ollama model...")
|
120
|
+
except Exception as e:
|
121
|
+
logger.error(f"Error updating UI for model loading: {str(e)}")
|
122
|
+
|
123
|
+
# Use asyncio.shield to ensure we can properly interrupt the stream processing
|
124
|
+
async for chunk in stream_generator:
|
125
|
+
# Check for cancellation frequently
|
126
|
+
if asyncio.current_task().cancelled():
|
127
|
+
logger.info("Task cancellation detected during chunk processing")
|
128
|
+
# Close the client stream if possible
|
129
|
+
if hasattr(client, 'cancel_stream'):
|
130
|
+
await client.cancel_stream()
|
131
|
+
raise asyncio.CancelledError()
|
132
|
+
|
133
|
+
# Check if model loading state changed
|
134
|
+
if hasattr(client, 'is_loading_model'):
|
135
|
+
model_loading = client.is_loading_model()
|
136
|
+
try:
|
137
|
+
if hasattr(app, 'query_one'):
|
138
|
+
loading = app.query_one("#loading-indicator")
|
139
|
+
loading_text = app.query_one("#loading-text")
|
140
|
+
|
141
|
+
if model_loading and not loading.has_class("model-loading"):
|
142
|
+
# Model loading started
|
143
|
+
logger.info("Model loading started during streaming")
|
144
|
+
loading.add_class("model-loading")
|
145
|
+
loading_text.update("⚙️ Loading Ollama model...")
|
146
|
+
elif not model_loading and loading.has_class("model-loading"):
|
147
|
+
# Model loading finished
|
148
|
+
logger.info("Model loading finished during streaming")
|
149
|
+
loading.remove_class("model-loading")
|
150
|
+
loading_text.update("▪▪▪ Generating response...")
|
151
|
+
except Exception as e:
|
152
|
+
logger.error(f"Error updating loading state during streaming: {str(e)}")
|
153
|
+
|
95
154
|
if chunk: # Only process non-empty chunks
|
96
155
|
buffer.append(chunk)
|
97
156
|
current_time = time.time()
|
@@ -100,7 +159,7 @@ async def generate_streaming_response(app: 'SimpleChatApp', messages: List[Dict]
|
|
100
159
|
if current_time - last_update >= update_interval or len(''.join(buffer)) > 100:
|
101
160
|
new_content = ''.join(buffer)
|
102
161
|
full_response += new_content
|
103
|
-
#
|
162
|
+
# Send content to UI
|
104
163
|
await callback(full_response)
|
105
164
|
buffer = []
|
106
165
|
last_update = current_time
|
@@ -114,23 +173,25 @@ async def generate_streaming_response(app: 'SimpleChatApp', messages: List[Dict]
|
|
114
173
|
full_response += new_content
|
115
174
|
await callback(full_response)
|
116
175
|
|
117
|
-
logger.info("Streaming response
|
118
|
-
# Add log before returning
|
119
|
-
logger.info(f"generate_streaming_response returning normally. Full response length: {len(full_response)}")
|
176
|
+
logger.info(f"Streaming response completed successfully. Response length: {len(full_response)}")
|
120
177
|
return full_response
|
178
|
+
|
121
179
|
except asyncio.CancelledError:
|
122
180
|
# This is expected when the user cancels via Escape
|
123
|
-
logger.info("Streaming response task cancelled.
|
124
|
-
#
|
125
|
-
|
126
|
-
|
127
|
-
|
181
|
+
logger.info(f"Streaming response task cancelled. Partial response length: {len(full_response)}")
|
182
|
+
# Ensure the client stream is closed
|
183
|
+
if hasattr(client, 'cancel_stream'):
|
184
|
+
await client.cancel_stream()
|
185
|
+
# Return whatever was collected so far
|
186
|
+
return full_response
|
187
|
+
|
128
188
|
except Exception as e:
|
129
|
-
logger.error(f"Error during streaming response: {str(e)}")
|
130
|
-
#
|
131
|
-
if
|
132
|
-
|
133
|
-
|
189
|
+
logger.error(f"Error during streaming response: {str(e)}")
|
190
|
+
# Close the client stream if possible
|
191
|
+
if hasattr(client, 'cancel_stream'):
|
192
|
+
await client.cancel_stream()
|
193
|
+
# Re-raise the exception for the caller to handle
|
194
|
+
raise
|
134
195
|
|
135
196
|
def ensure_ollama_running() -> bool:
|
136
197
|
"""
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: chat-console
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.9
|
4
4
|
Summary: A command-line interface for chatting with LLMs, storing chats and (future) rag interactions
|
5
5
|
Home-page: https://github.com/wazacraftrfid/chat-console
|
6
6
|
Author: Johnathan Greenaway
|
@@ -1,24 +1,24 @@
|
|
1
|
-
app/__init__.py,sha256=
|
1
|
+
app/__init__.py,sha256=g2BzewDN5X96Dl5Zzw8uag1TBEdPIU1ceTm7u-BJrjM,130
|
2
2
|
app/config.py,sha256=sKNp6Za4ZfW-CZBOvEv0TncAS77AnKi86hTM51C4KQ4,5227
|
3
3
|
app/database.py,sha256=nt8CVuDpy6zw8mOYqDcfUmNw611t7Ln7pz22M0b6-MI,9967
|
4
|
-
app/main.py,sha256=
|
4
|
+
app/main.py,sha256=k726xRBcuPgbUsUg4s-REhtaljccjDLNzA_C-fPkQk4,48866
|
5
5
|
app/models.py,sha256=4-y9Lytay2exWPFi0FDlVeRL3K2-I7E-jBqNzTfokqY,2644
|
6
|
-
app/utils.py,sha256=
|
6
|
+
app/utils.py,sha256=IyINMrM6oGXtN5HRPuKoFEyfKg0fR4FVXIi_0e2KxI0,11798
|
7
7
|
app/api/__init__.py,sha256=A8UL84ldYlv8l7O-yKzraVFcfww86SgWfpl4p7R03-w,62
|
8
8
|
app/api/anthropic.py,sha256=x5PmBXEKe_ow2NWk8XdqSPR0hLOdCc_ypY5QAySeA78,4234
|
9
9
|
app/api/base.py,sha256=-6RSxSpqe-OMwkaq1wVWbu3pVkte-ZYy8rmdvt-Qh48,3953
|
10
|
-
app/api/ollama.py,sha256=
|
10
|
+
app/api/ollama.py,sha256=FTIlgZmvpZd6K4HL2nUD19-p9Xb1TA859LfnCgewpcU,51354
|
11
11
|
app/api/openai.py,sha256=1fYgFXXL6yj_7lQ893Yj28RYG4M8d6gt_q1gzhhjcig,3641
|
12
12
|
app/ui/__init__.py,sha256=RndfbQ1Tv47qdSiuQzvWP96lPS547SDaGE-BgOtiP_w,55
|
13
|
-
app/ui/chat_interface.py,sha256=
|
13
|
+
app/ui/chat_interface.py,sha256=R8tdy72TcT7veemUzcJOjbPY32WizBdNHgfmq69EFfA,14275
|
14
14
|
app/ui/chat_list.py,sha256=WQTYVNSSXlx_gQal3YqILZZKL9UiTjmNMIDX2I9pAMM,11205
|
15
15
|
app/ui/model_browser.py,sha256=5h3gVsuGIUrXjYVF-QclZFhYtX2kH14LvT22Ufm9etg,49453
|
16
16
|
app/ui/model_selector.py,sha256=Aj1irAs9DQMn8wfcPsFZGxWmx0JTzHjSe7pVdDMwqTQ,13182
|
17
17
|
app/ui/search.py,sha256=b-m14kG3ovqW1-i0qDQ8KnAqFJbi5b1FLM9dOnbTyIs,9763
|
18
18
|
app/ui/styles.py,sha256=04AhPuLrOd2yenfRySFRestPeuTPeMLzhmMB67NdGvw,5615
|
19
|
-
chat_console-0.2.
|
20
|
-
chat_console-0.2.
|
21
|
-
chat_console-0.2.
|
22
|
-
chat_console-0.2.
|
23
|
-
chat_console-0.2.
|
24
|
-
chat_console-0.2.
|
19
|
+
chat_console-0.2.9.dist-info/licenses/LICENSE,sha256=srHZ3fvcAuZY1LHxE7P6XWju2njRCHyK6h_ftEbzxSE,1057
|
20
|
+
chat_console-0.2.9.dist-info/METADATA,sha256=zTSJePqMsi0n6fEz8s4gtLwHe_726-ijfTjPwH_Mumw,2921
|
21
|
+
chat_console-0.2.9.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
22
|
+
chat_console-0.2.9.dist-info/entry_points.txt,sha256=kkVdEc22U9PAi2AeruoKklfkng_a_aHAP6VRVwrAD7c,67
|
23
|
+
chat_console-0.2.9.dist-info/top_level.txt,sha256=io9g7LCbfmTG1SFKgEOGXmCFB9uMP2H5lerm0HiHWQE,4
|
24
|
+
chat_console-0.2.9.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|