chat-console 0.2.0.dev1__py3-none-any.whl → 0.2.2.dev1__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/api/ollama.py +21 -1
- app/main.py +69 -40
- app/utils.py +23 -22
- {chat_console-0.2.0.dev1.dist-info → chat_console-0.2.2.dev1.dist-info}/METADATA +1 -1
- {chat_console-0.2.0.dev1.dist-info → chat_console-0.2.2.dev1.dist-info}/RECORD +9 -9
- {chat_console-0.2.0.dev1.dist-info → chat_console-0.2.2.dev1.dist-info}/WHEEL +0 -0
- {chat_console-0.2.0.dev1.dist-info → chat_console-0.2.2.dev1.dist-info}/entry_points.txt +0 -0
- {chat_console-0.2.0.dev1.dist-info → chat_console-0.2.2.dev1.dist-info}/licenses/LICENSE +0 -0
- {chat_console-0.2.0.dev1.dist-info → chat_console-0.2.2.dev1.dist-info}/top_level.txt +0 -0
app/api/ollama.py
CHANGED
@@ -15,6 +15,9 @@ class OllamaClient(BaseModelClient):
|
|
15
15
|
self.base_url = OLLAMA_BASE_URL.rstrip('/')
|
16
16
|
logger.info(f"Initializing Ollama client with base URL: {self.base_url}")
|
17
17
|
|
18
|
+
# Track active stream session
|
19
|
+
self._active_stream_session = None
|
20
|
+
|
18
21
|
# Try to start Ollama if not running
|
19
22
|
if not ensure_ollama_running():
|
20
23
|
raise Exception(f"Failed to start Ollama server. Please ensure Ollama is installed and try again.")
|
@@ -158,6 +161,7 @@ class OllamaClient(BaseModelClient):
|
|
158
161
|
prompt = self._prepare_messages(messages, style)
|
159
162
|
retries = 2
|
160
163
|
last_error = None
|
164
|
+
self._active_stream_session = None # Track the active session
|
161
165
|
|
162
166
|
while retries >= 0:
|
163
167
|
try:
|
@@ -192,7 +196,10 @@ class OllamaClient(BaseModelClient):
|
|
192
196
|
logger.info("Model pulled successfully")
|
193
197
|
|
194
198
|
# Now proceed with actual generation
|
195
|
-
|
199
|
+
session = aiohttp.ClientSession()
|
200
|
+
self._active_stream_session = session # Store reference to active session
|
201
|
+
|
202
|
+
try:
|
196
203
|
logger.debug(f"Sending streaming request to {self.base_url}/api/generate")
|
197
204
|
async with session.post(
|
198
205
|
f"{self.base_url}/api/generate",
|
@@ -216,6 +223,9 @@ class OllamaClient(BaseModelClient):
|
|
216
223
|
continue
|
217
224
|
logger.info("Streaming completed successfully")
|
218
225
|
return
|
226
|
+
finally:
|
227
|
+
self._active_stream_session = None # Clear reference when done
|
228
|
+
await session.close() # Ensure session is closed
|
219
229
|
|
220
230
|
except aiohttp.ClientConnectorError:
|
221
231
|
last_error = "Could not connect to Ollama server. Make sure Ollama is running and accessible at " + self.base_url
|
@@ -223,6 +233,9 @@ class OllamaClient(BaseModelClient):
|
|
223
233
|
last_error = f"Ollama API error: {e.status} - {e.message}"
|
224
234
|
except aiohttp.ClientTimeout:
|
225
235
|
last_error = "Request to Ollama server timed out"
|
236
|
+
except asyncio.CancelledError:
|
237
|
+
logger.info("Streaming cancelled by client")
|
238
|
+
raise # Propagate cancellation
|
226
239
|
except Exception as e:
|
227
240
|
last_error = f"Error streaming completion: {str(e)}"
|
228
241
|
|
@@ -233,3 +246,10 @@ class OllamaClient(BaseModelClient):
|
|
233
246
|
await asyncio.sleep(1)
|
234
247
|
|
235
248
|
raise Exception(last_error)
|
249
|
+
|
250
|
+
async def cancel_stream(self) -> None:
|
251
|
+
"""Cancel any active streaming request"""
|
252
|
+
if self._active_stream_session:
|
253
|
+
logger.info("Cancelling active stream session")
|
254
|
+
await self._active_stream_session.close()
|
255
|
+
self._active_stream_session = None
|
app/main.py
CHANGED
@@ -291,6 +291,7 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
|
|
291
291
|
|
292
292
|
current_conversation = reactive(None) # Keep SimpleChatApp reactive var
|
293
293
|
is_generating = reactive(False) # Keep SimpleChatApp reactive var
|
294
|
+
current_generation_task: Optional[asyncio.Task] = None # Add task reference
|
294
295
|
|
295
296
|
def __init__(self, initial_text: Optional[str] = None): # Keep SimpleChatApp __init__
|
296
297
|
super().__init__() # Keep SimpleChatApp __init__
|
@@ -441,26 +442,33 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
|
|
441
442
|
await self.create_new_conversation() # Keep SimpleChatApp action_new_conversation
|
442
443
|
log("action_new_conversation finished") # Added log
|
443
444
|
|
444
|
-
def action_escape(self) -> None:
|
445
|
+
def action_escape(self) -> None:
|
445
446
|
"""Handle escape key globally."""
|
446
|
-
log("action_escape triggered")
|
447
|
+
log("action_escape triggered")
|
447
448
|
settings_panel = self.query_one("#settings-panel")
|
448
|
-
log(f"Settings panel visible: {settings_panel.has_class('visible')}")
|
449
|
+
log(f"Settings panel visible: {settings_panel.has_class('visible')}")
|
450
|
+
|
449
451
|
if settings_panel.has_class("visible"):
|
450
|
-
log("Hiding settings panel")
|
451
|
-
# If settings panel is visible, hide it
|
452
|
+
log("Hiding settings panel")
|
452
453
|
settings_panel.remove_class("visible")
|
453
|
-
self.query_one("#message-input").focus()
|
454
|
+
self.query_one("#message-input").focus()
|
454
455
|
elif self.is_generating:
|
455
|
-
log("
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
456
|
+
log("Attempting to cancel generation task")
|
457
|
+
if self.current_generation_task and not self.current_generation_task.done():
|
458
|
+
log("Cancelling active generation task.")
|
459
|
+
self.current_generation_task.cancel()
|
460
|
+
# The finally block in generate_response will handle is_generating = False and UI updates
|
461
|
+
self.notify("Stopping generation...", severity="warning", timeout=2) # Notify user immediately
|
462
|
+
else:
|
463
|
+
# This case might happen if is_generating is True but the task is already done or None
|
464
|
+
log("is_generating is True, but no active task found to cancel. Resetting flag.")
|
465
|
+
self.is_generating = False # Reset flag manually if task is missing
|
466
|
+
loading = self.query_one("#loading-indicator")
|
467
|
+
loading.add_class("hidden")
|
468
|
+
else:
|
469
|
+
log("Escape pressed, but settings not visible and not actively generating.")
|
470
|
+
# Optionally add other escape behaviors here if needed for the main screen
|
471
|
+
# e.g., clear input, deselect item, etc.
|
464
472
|
|
465
473
|
def update_app_info(self) -> None:
|
466
474
|
"""Update the displayed app information."""
|
@@ -651,10 +659,10 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
|
|
651
659
|
log.error(f"Error updating UI: {str(e)}") # Use log instead of logger
|
652
660
|
|
653
661
|
# Generate the response with timeout and cleanup # Keep SimpleChatApp generate_response
|
654
|
-
|
662
|
+
self.current_generation_task = None # Clear previous task reference
|
655
663
|
try: # Keep SimpleChatApp generate_response
|
656
664
|
# Create a task for the response generation # Keep SimpleChatApp generate_response
|
657
|
-
|
665
|
+
self.current_generation_task = asyncio.create_task( # Keep SimpleChatApp generate_response
|
658
666
|
generate_streaming_response( # Keep SimpleChatApp generate_response
|
659
667
|
self, # Pass the app instance
|
660
668
|
api_messages, # Keep SimpleChatApp generate_response
|
@@ -664,13 +672,16 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
|
|
664
672
|
update_ui # Keep SimpleChatApp generate_response
|
665
673
|
) # Keep SimpleChatApp generate_response
|
666
674
|
) # Keep SimpleChatApp generate_response
|
675
|
+
generation_task = self.generation_task # Keep local reference for later checks
|
667
676
|
|
668
677
|
# Wait for response with timeout # Keep SimpleChatApp generate_response
|
669
|
-
|
678
|
+
log.info(f"Waiting for generation task {self.current_generation_task} with timeout...") # Add log
|
679
|
+
full_response = await asyncio.wait_for(self.current_generation_task, timeout=60) # Longer timeout # Keep SimpleChatApp generate_response
|
680
|
+
log.info(f"Generation task {self.current_generation_task} completed. Full response length: {len(full_response) if full_response else 0}") # Add log
|
670
681
|
|
671
|
-
# Save to database only if we got a complete response
|
672
|
-
if self.is_generating and full_response: #
|
673
|
-
log("Generation finished, saving full response to DB") # Added log
|
682
|
+
# Save to database only if we got a complete response and weren't cancelled
|
683
|
+
if self.is_generating and full_response: # Check is_generating flag here
|
684
|
+
log("Generation finished normally, saving full response to DB") # Added log
|
674
685
|
self.db.add_message( # Keep SimpleChatApp generate_response
|
675
686
|
self.current_conversation.id, # Keep SimpleChatApp generate_response
|
676
687
|
"assistant", # Keep SimpleChatApp generate_response
|
@@ -679,11 +690,24 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
|
|
679
690
|
# Force a final refresh # Keep SimpleChatApp generate_response
|
680
691
|
self.refresh(layout=True) # Keep SimpleChatApp generate_response
|
681
692
|
await asyncio.sleep(0.1) # Wait for UI to update # Keep SimpleChatApp generate_response
|
682
|
-
elif not full_response:
|
693
|
+
elif not full_response and self.is_generating: # Only log if not cancelled
|
683
694
|
log("Generation finished but full_response is empty/None") # Added log
|
695
|
+
else:
|
696
|
+
# This case handles cancellation where full_response might be partial or None
|
697
|
+
log("Generation was cancelled or finished without a full response.")
|
698
|
+
|
699
|
+
except asyncio.CancelledError: # Handle cancellation explicitly
|
700
|
+
log.warning("Generation task was cancelled.")
|
701
|
+
self.notify("Generation stopped by user.", severity="warning")
|
702
|
+
# Remove the potentially incomplete message from UI state
|
703
|
+
if self.messages and self.messages[-1].role == "assistant":
|
704
|
+
self.messages.pop()
|
705
|
+
await self.update_messages_ui() # Update UI to remove partial message
|
684
706
|
|
685
707
|
except asyncio.TimeoutError: # Keep SimpleChatApp generate_response
|
686
|
-
log.error("Response generation timed out") # Use log instead of logger
|
708
|
+
log.error(f"Response generation timed out waiting for task {self.current_generation_task}") # Use log instead of logger
|
709
|
+
# Log state at timeout
|
710
|
+
log.error(f"Timeout state: is_generating={self.is_generating}, task_done={self.current_generation_task.done() if self.current_generation_task else 'N/A'}")
|
687
711
|
error_msg = "Response generation timed out. The model may be busy or unresponsive. Please try again." # Keep SimpleChatApp generate_response
|
688
712
|
self.notify(error_msg, severity="error") # Keep SimpleChatApp generate_response
|
689
713
|
|
@@ -695,31 +719,36 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
|
|
695
719
|
await self.update_messages_ui() # Keep SimpleChatApp generate_response
|
696
720
|
|
697
721
|
finally: # Keep SimpleChatApp generate_response
|
698
|
-
# Ensure
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
await generation_task # Keep SimpleChatApp generate_response
|
705
|
-
except (asyncio.CancelledError, Exception) as e: # Keep SimpleChatApp generate_response
|
706
|
-
log.error(f"Error cleaning up generation task: {str(e)}") # Use log instead of logger
|
707
|
-
|
722
|
+
# Ensure flag is reset and task reference is cleared
|
723
|
+
log(f"Setting is_generating to False in finally block") # Added log
|
724
|
+
self.is_generating = False # Keep SimpleChatApp generate_response
|
725
|
+
self.current_generation_task = None # Clear task reference
|
726
|
+
loading = self.query_one("#loading-indicator") # Keep SimpleChatApp generate_response
|
727
|
+
loading.add_class("hidden") # Keep SimpleChatApp generate_response
|
708
728
|
# Force a final UI refresh # Keep SimpleChatApp generate_response
|
709
729
|
self.refresh(layout=True) # Keep SimpleChatApp generate_response
|
710
730
|
|
711
731
|
except Exception as e: # Keep SimpleChatApp generate_response
|
712
|
-
|
732
|
+
# Catch any other unexpected errors during generation setup/handling
|
733
|
+
log.error(f"Unexpected exception during generate_response: {str(e)}") # Added log
|
713
734
|
self.notify(f"Error generating response: {str(e)}", severity="error") # Keep SimpleChatApp generate_response
|
714
|
-
# Add error message # Keep SimpleChatApp generate_response
|
715
|
-
error_msg = f"Error
|
735
|
+
# Add error message to UI # Keep SimpleChatApp generate_response
|
736
|
+
error_msg = f"Error: {str(e)}" # Keep SimpleChatApp generate_response
|
716
737
|
self.messages.append(Message(role="assistant", content=error_msg)) # Keep SimpleChatApp generate_response
|
717
738
|
await self.update_messages_ui() # Keep SimpleChatApp generate_response
|
718
|
-
|
719
|
-
|
720
|
-
|
739
|
+
# The finally block below will handle resetting is_generating and hiding loading
|
740
|
+
|
741
|
+
finally: # Keep SimpleChatApp generate_response - This finally block now primarily handles cleanup
|
742
|
+
log(f"Ensuring is_generating is False and task is cleared in outer finally block") # Added log
|
743
|
+
self.is_generating = False # Ensure flag is always reset
|
744
|
+
self.current_generation_task = None # Ensure task ref is cleared
|
721
745
|
loading = self.query_one("#loading-indicator") # Keep SimpleChatApp generate_response
|
722
|
-
loading.add_class("hidden") #
|
746
|
+
loading.add_class("hidden") # Ensure loading indicator is hidden
|
747
|
+
# Re-focus input after generation attempt (success, failure, or cancel)
|
748
|
+
try:
|
749
|
+
self.query_one("#message-input").focus()
|
750
|
+
except Exception:
|
751
|
+
pass # Ignore if input not found
|
723
752
|
|
724
753
|
def on_model_selector_model_selected(self, event: ModelSelector.ModelSelected) -> None: # Keep SimpleChatApp on_model_selector_model_selected
|
725
754
|
"""Handle model selection""" # Keep SimpleChatApp on_model_selector_model_selected docstring
|
app/utils.py
CHANGED
@@ -86,14 +86,12 @@ 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
|
89
90
|
|
90
91
|
try:
|
92
|
+
# The cancellation is now handled by cancelling the asyncio Task in main.py
|
93
|
+
# which will raise CancelledError here, interrupting the loop.
|
91
94
|
async for chunk in client.generate_stream(messages, model, style):
|
92
|
-
# Check if generation was cancelled by the app (e.g., via escape key)
|
93
|
-
if not app.is_generating:
|
94
|
-
logger.info("Generation cancelled by app flag.")
|
95
|
-
break # Exit the loop immediately
|
96
|
-
|
97
95
|
if chunk: # Only process non-empty chunks
|
98
96
|
buffer.append(chunk)
|
99
97
|
current_time = time.time()
|
@@ -102,34 +100,37 @@ async def generate_streaming_response(app: 'SimpleChatApp', messages: List[Dict]
|
|
102
100
|
if current_time - last_update >= update_interval or len(''.join(buffer)) > 100:
|
103
101
|
new_content = ''.join(buffer)
|
104
102
|
full_response += new_content
|
105
|
-
#
|
106
|
-
if not app.is_generating:
|
107
|
-
logger.info("Generation cancelled before UI update.")
|
108
|
-
break
|
103
|
+
# No need to check app.is_generating here, rely on CancelledError
|
109
104
|
await callback(full_response)
|
110
105
|
buffer = []
|
111
106
|
last_update = current_time
|
112
107
|
|
113
108
|
# Small delay to let UI catch up
|
114
109
|
await asyncio.sleep(0.05)
|
115
|
-
|
116
|
-
# Send any remaining content if
|
117
|
-
if buffer
|
110
|
+
|
111
|
+
# Send any remaining content if the loop finished normally
|
112
|
+
if buffer:
|
118
113
|
new_content = ''.join(buffer)
|
119
114
|
full_response += new_content
|
120
115
|
await callback(full_response)
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
logger.info("Streaming response loop exited due to cancellation.")
|
126
|
-
|
116
|
+
|
117
|
+
logger.info("Streaming response loop finished normally.") # Clarify log message
|
118
|
+
# Add log before returning
|
119
|
+
logger.info(f"generate_streaming_response returning normally. Full response length: {len(full_response)}")
|
127
120
|
return full_response
|
121
|
+
except asyncio.CancelledError:
|
122
|
+
# This is expected when the user cancels via Escape
|
123
|
+
logger.info("Streaming response task cancelled.") # Clarify log message
|
124
|
+
# Add log before returning
|
125
|
+
logger.info(f"generate_streaming_response returning after cancellation. Partial response length: {len(full_response)}")
|
126
|
+
# Do not re-raise CancelledError, let the caller handle it
|
127
|
+
return full_response # Return whatever was collected so far (might be partial)
|
128
128
|
except Exception as e:
|
129
|
-
logger.error(f"Error
|
130
|
-
# Ensure the app knows generation stopped on error
|
131
|
-
|
132
|
-
|
129
|
+
logger.error(f"Error during streaming response: {str(e)}") # Clarify log message
|
130
|
+
# Ensure the app knows generation stopped on error ONLY if it wasn't cancelled
|
131
|
+
if not isinstance(e, asyncio.CancelledError):
|
132
|
+
app.is_generating = False # Reset flag on other errors
|
133
|
+
raise # Re-raise other exceptions
|
133
134
|
|
134
135
|
def ensure_ollama_running() -> bool:
|
135
136
|
"""
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: chat-console
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.2.dev1
|
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,13 +1,13 @@
|
|
1
1
|
app/__init__.py,sha256=OeqboIrx_Kjea0CY9Be8nLI-No1YWQfqbWIp-4lMOOI,131
|
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=PCc1clX9sdJh8cpzcgUlnrRyt4o8k5KCJ9UwTQ8Thas,50133
|
5
5
|
app/models.py,sha256=4-y9Lytay2exWPFi0FDlVeRL3K2-I7E-jBqNzTfokqY,2644
|
6
|
-
app/utils.py,sha256=
|
6
|
+
app/utils.py,sha256=6XSIJBcJPOXPIHnvKvRwnttdRnN9BSlodcKVj57RLeM,8861
|
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=Wm7IJPZhp4pygwEYIA4zk0mRsDaU87LkOmYUXPt7MyM,12551
|
11
11
|
app/api/openai.py,sha256=1fYgFXXL6yj_7lQ893Yj28RYG4M8d6gt_q1gzhhjcig,3641
|
12
12
|
app/ui/__init__.py,sha256=RndfbQ1Tv47qdSiuQzvWP96lPS547SDaGE-BgOtiP_w,55
|
13
13
|
app/ui/chat_interface.py,sha256=VwmVvltxS9l18DI9U7kL43t8kSPPNsrkkrrUSoGu16Q,13623
|
@@ -15,9 +15,9 @@ app/ui/chat_list.py,sha256=WQTYVNSSXlx_gQal3YqILZZKL9UiTjmNMIDX2I9pAMM,11205
|
|
15
15
|
app/ui/model_selector.py,sha256=Aj1irAs9DQMn8wfcPsFZGxWmx0JTzHjSe7pVdDMwqTQ,13182
|
16
16
|
app/ui/search.py,sha256=b-m14kG3ovqW1-i0qDQ8KnAqFJbi5b1FLM9dOnbTyIs,9763
|
17
17
|
app/ui/styles.py,sha256=04AhPuLrOd2yenfRySFRestPeuTPeMLzhmMB67NdGvw,5615
|
18
|
-
chat_console-0.2.
|
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.
|
18
|
+
chat_console-0.2.2.dev1.dist-info/licenses/LICENSE,sha256=srHZ3fvcAuZY1LHxE7P6XWju2njRCHyK6h_ftEbzxSE,1057
|
19
|
+
chat_console-0.2.2.dev1.dist-info/METADATA,sha256=HcWEG6ahXupJhz9POYMoGI90alNTHFLL0mfEPQ4xecg,2926
|
20
|
+
chat_console-0.2.2.dev1.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
21
|
+
chat_console-0.2.2.dev1.dist-info/entry_points.txt,sha256=kkVdEc22U9PAi2AeruoKklfkng_a_aHAP6VRVwrAD7c,67
|
22
|
+
chat_console-0.2.2.dev1.dist-info/top_level.txt,sha256=io9g7LCbfmTG1SFKgEOGXmCFB9uMP2H5lerm0HiHWQE,4
|
23
|
+
chat_console-0.2.2.dev1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|