chat-console 0.2.0__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 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
- async with aiohttp.ClientSession() as session:
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: # Modify SimpleChatApp action_escape
445
+ def action_escape(self) -> None:
445
446
  """Handle escape key globally."""
446
- log("action_escape triggered") # Added log
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')}") # Added log
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") # Added log
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() # Focus input after closing settings
454
+ self.query_one("#message-input").focus()
454
455
  elif self.is_generating:
455
- log("Stopping generation") # Added log
456
- # Otherwise, stop generation if running
457
- self.is_generating = False # Keep SimpleChatApp action_escape
458
- self.notify("Generation stopped", severity="warning") # Keep SimpleChatApp action_escape
459
- loading = self.query_one("#loading-indicator") # Keep SimpleChatApp action_escape
460
- loading.add_class("hidden") # Keep SimpleChatApp action_escape
461
- else: # Optional: Add other escape behavior for the main screen if desired # Keep SimpleChatApp action_escape comment
462
- log("Escape pressed, but settings not visible and not generating.") # Added log
463
- # pass # Keep SimpleChatApp action_escape comment
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
- generation_task = None # Keep SimpleChatApp generate_response
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
- generation_task = asyncio.create_task( # Keep SimpleChatApp generate_response
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
- full_response = await asyncio.wait_for(generation_task, timeout=60) # Longer timeout # Keep SimpleChatApp generate_response
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 # Keep SimpleChatApp generate_response
672
- if self.is_generating and full_response: # Keep SimpleChatApp generate_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 task is properly cancelled and cleaned up # Keep SimpleChatApp generate_response
699
- if generation_task: # Keep SimpleChatApp generate_response
700
- if not generation_task.done(): # Keep SimpleChatApp generate_response
701
- log("Cancelling generation task") # Added log
702
- generation_task.cancel() # Keep SimpleChatApp generate_response
703
- try: # Keep SimpleChatApp generate_response
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
- log.error(f"Exception during generate_response: {str(e)}") # Added log
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 generating response: {str(e)}" # Keep SimpleChatApp generate_response
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
- finally: # Keep SimpleChatApp generate_response
719
- log(f"Setting is_generating to False in finally block") # Added log
720
- self.is_generating = False # Keep SimpleChatApp generate_response
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") # Keep SimpleChatApp generate_response
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
- # Check again before calling callback, in case it was cancelled during chunk processing
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 generation wasn't cancelled
117
- if buffer and app.is_generating:
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
- if app.is_generating:
123
- logger.info("Streaming response completed normally.")
124
- else:
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 in streaming response: {str(e)}")
130
- # Ensure the app knows generation stopped on error
131
- app.is_generating = False
132
- raise
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.0
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=PMx9DcD2MAVyqeYanqu6mqr43RG4oXA9jFIdBwoQFQY,48174
4
+ app/main.py,sha256=PCc1clX9sdJh8cpzcgUlnrRyt4o8k5KCJ9UwTQ8Thas,50133
5
5
  app/models.py,sha256=4-y9Lytay2exWPFi0FDlVeRL3K2-I7E-jBqNzTfokqY,2644
6
- app/utils.py,sha256=AgmLrmyikt1Y7KturNmZVK2eC6de-RfRadVbp3HmUAg,8434
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=zFZ3g2sYncvMgcvx92jTCLkigIaDvTuhILcLiCrwisc,11640
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.0.dist-info/licenses/LICENSE,sha256=srHZ3fvcAuZY1LHxE7P6XWju2njRCHyK6h_ftEbzxSE,1057
19
- chat_console-0.2.0.dist-info/METADATA,sha256=DPnX1Q1e7iTfNyyQRhJezaUXuHtq1E9mqb7k7gDe0GI,2921
20
- chat_console-0.2.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
21
- chat_console-0.2.0.dist-info/entry_points.txt,sha256=kkVdEc22U9PAi2AeruoKklfkng_a_aHAP6VRVwrAD7c,67
22
- chat_console-0.2.0.dist-info/top_level.txt,sha256=io9g7LCbfmTG1SFKgEOGXmCFB9uMP2H5lerm0HiHWQE,4
23
- chat_console-0.2.0.dist-info/RECORD,,
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,,