chat-console 0.1.95.dev1__tar.gz → 0.1.96.dev1__tar.gz

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.
Files changed (28) hide show
  1. {chat_console-0.1.95.dev1 → chat_console-0.1.96.dev1}/PKG-INFO +1 -1
  2. {chat_console-0.1.95.dev1 → chat_console-0.1.96.dev1}/app/config.py +2 -1
  3. {chat_console-0.1.95.dev1 → chat_console-0.1.96.dev1}/app/main.py +157 -8
  4. {chat_console-0.1.95.dev1 → chat_console-0.1.96.dev1}/app/utils.py +63 -0
  5. {chat_console-0.1.95.dev1 → chat_console-0.1.96.dev1}/chat_console.egg-info/PKG-INFO +1 -1
  6. {chat_console-0.1.95.dev1 → chat_console-0.1.96.dev1}/setup.py +1 -1
  7. {chat_console-0.1.95.dev1 → chat_console-0.1.96.dev1}/LICENSE +0 -0
  8. {chat_console-0.1.95.dev1 → chat_console-0.1.96.dev1}/README.md +0 -0
  9. {chat_console-0.1.95.dev1 → chat_console-0.1.96.dev1}/app/__init__.py +0 -0
  10. {chat_console-0.1.95.dev1 → chat_console-0.1.96.dev1}/app/api/__init__.py +0 -0
  11. {chat_console-0.1.95.dev1 → chat_console-0.1.96.dev1}/app/api/anthropic.py +0 -0
  12. {chat_console-0.1.95.dev1 → chat_console-0.1.96.dev1}/app/api/base.py +0 -0
  13. {chat_console-0.1.95.dev1 → chat_console-0.1.96.dev1}/app/api/ollama.py +0 -0
  14. {chat_console-0.1.95.dev1 → chat_console-0.1.96.dev1}/app/api/openai.py +0 -0
  15. {chat_console-0.1.95.dev1 → chat_console-0.1.96.dev1}/app/database.py +0 -0
  16. {chat_console-0.1.95.dev1 → chat_console-0.1.96.dev1}/app/models.py +0 -0
  17. {chat_console-0.1.95.dev1 → chat_console-0.1.96.dev1}/app/ui/__init__.py +0 -0
  18. {chat_console-0.1.95.dev1 → chat_console-0.1.96.dev1}/app/ui/chat_interface.py +0 -0
  19. {chat_console-0.1.95.dev1 → chat_console-0.1.96.dev1}/app/ui/chat_list.py +0 -0
  20. {chat_console-0.1.95.dev1 → chat_console-0.1.96.dev1}/app/ui/model_selector.py +0 -0
  21. {chat_console-0.1.95.dev1 → chat_console-0.1.96.dev1}/app/ui/search.py +0 -0
  22. {chat_console-0.1.95.dev1 → chat_console-0.1.96.dev1}/app/ui/styles.py +0 -0
  23. {chat_console-0.1.95.dev1 → chat_console-0.1.96.dev1}/chat_console.egg-info/SOURCES.txt +0 -0
  24. {chat_console-0.1.95.dev1 → chat_console-0.1.96.dev1}/chat_console.egg-info/dependency_links.txt +0 -0
  25. {chat_console-0.1.95.dev1 → chat_console-0.1.96.dev1}/chat_console.egg-info/entry_points.txt +0 -0
  26. {chat_console-0.1.95.dev1 → chat_console-0.1.96.dev1}/chat_console.egg-info/requires.txt +0 -0
  27. {chat_console-0.1.95.dev1 → chat_console-0.1.96.dev1}/chat_console.egg-info/top_level.txt +0 -0
  28. {chat_console-0.1.95.dev1 → chat_console-0.1.96.dev1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chat-console
3
- Version: 0.1.95.dev1
3
+ Version: 0.1.96.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
@@ -119,7 +119,8 @@ DEFAULT_CONFIG = {
119
119
  "default_style": "default",
120
120
  "max_history_items": 100,
121
121
  "highlight_code": True,
122
- "auto_save": True
122
+ "auto_save": True,
123
+ "generate_dynamic_titles": True
123
124
  }
124
125
 
125
126
  def validate_config(config):
@@ -13,7 +13,7 @@ from textual.containers import Container, Horizontal, Vertical, ScrollableContai
13
13
  from textual.reactive import reactive
14
14
  from textual.widgets import Button, Input, Label, Static, Header, Footer, ListView, ListItem
15
15
  from textual.binding import Binding
16
- from textual import work
16
+ from textual import work, log, on
17
17
  from textual.screen import Screen
18
18
  from openai import OpenAI
19
19
  from app.models import Message, Conversation
@@ -23,7 +23,7 @@ from app.ui.chat_interface import MessageDisplay
23
23
  from app.ui.model_selector import ModelSelector, StyleSelector
24
24
  from app.ui.chat_list import ChatList
25
25
  from app.api.base import BaseModelClient
26
- from app.utils import generate_streaming_response, save_settings_to_config # Import save function
26
+ from app.utils import generate_streaming_response, save_settings_to_config, generate_conversation_title # Import title function
27
27
 
28
28
  # --- Remove SettingsScreen class entirely ---
29
29
 
@@ -209,6 +209,33 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
209
209
  padding-top: 1;
210
210
  }
211
211
 
212
+ /* --- Title Input Modal CSS --- */
213
+ TitleInputModal {
214
+ align: center middle;
215
+ width: 60;
216
+ height: auto;
217
+ background: $surface;
218
+ border: thick $primary;
219
+ padding: 1 2;
220
+ layer: modal; /* Ensure it's above other elements */
221
+ }
222
+
223
+ #modal-label {
224
+ width: 100%;
225
+ content-align: center middle;
226
+ padding-bottom: 1;
227
+ }
228
+
229
+ #title-input {
230
+ width: 100%;
231
+ margin-bottom: 1;
232
+ }
233
+
234
+ TitleInputModal Horizontal {
235
+ width: 100%;
236
+ height: auto;
237
+ align: center middle;
238
+ }
212
239
  """
213
240
 
214
241
  BINDINGS = [ # Keep SimpleChatApp BINDINGS, ensure Enter is not globally bound for settings
@@ -219,6 +246,7 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
219
246
  Binding("ctrl+c", "quit", "Quit", show=False),
220
247
  Binding("h", "view_history", "History", show=True, key_display="h"),
221
248
  Binding("s", "settings", "Settings", show=True, key_display="s"),
249
+ Binding("t", "action_update_title", "Update Title", show=True, key_display="t"),
222
250
  ] # Keep SimpleChatApp BINDINGS end
223
251
 
224
252
  current_conversation = reactive(None) # Keep SimpleChatApp reactive var
@@ -399,14 +427,62 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
399
427
  content # Keep SimpleChatApp action_send_message
400
428
  ) # Keep SimpleChatApp action_send_message
401
429
 
402
- # Update UI # Keep SimpleChatApp action_send_message
403
- await self.update_messages_ui() # Keep SimpleChatApp action_send_message
430
+ # Check if this is the first message in the conversation
431
+ # Note: We check length *before* adding the potential assistant message
432
+ is_first_message = len(self.messages) == 1
404
433
 
405
- # Generate AI response # Keep SimpleChatApp action_send_message
406
- await self.generate_response() # Keep SimpleChatApp action_send_message
434
+ # Update UI with user message first
435
+ await self.update_messages_ui()
407
436
 
408
- # Focus back on input # Keep SimpleChatApp action_send_message
409
- input_widget.focus() # Keep SimpleChatApp action_send_message
437
+ # If this is the first message and dynamic titles are enabled, generate one
438
+ if is_first_message and self.current_conversation and CONFIG.get("generate_dynamic_titles", True):
439
+ log("First message detected, generating title...")
440
+ title_generation_in_progress = True # Use a local flag
441
+ loading = self.query_one("#loading-indicator")
442
+ loading.remove_class("hidden") # Show loading for title gen
443
+
444
+ try:
445
+ # Get appropriate client
446
+ model = self.selected_model
447
+ client = BaseModelClient.get_client_for_model(model)
448
+ if client is None:
449
+ raise Exception(f"No client available for model: {model}")
450
+
451
+ # Generate title
452
+ log(f"Calling generate_conversation_title with model: {model}")
453
+ title = await generate_conversation_title(content, model, client)
454
+ log(f"Generated title: {title}")
455
+
456
+ # Update conversation title in database
457
+ self.db.update_conversation(
458
+ self.current_conversation.id,
459
+ title=title
460
+ )
461
+
462
+ # Update UI title
463
+ title_widget = self.query_one("#conversation-title", Static)
464
+ title_widget.update(title)
465
+
466
+ # Update conversation object
467
+ self.current_conversation.title = title
468
+
469
+ self.notify(f"Conversation title set to: {title}", severity="information", timeout=3)
470
+
471
+ except Exception as e:
472
+ log.error(f"Failed to generate title: {str(e)}")
473
+ self.notify(f"Failed to generate title: {str(e)}", severity="warning")
474
+ finally:
475
+ title_generation_in_progress = False
476
+ # Hide loading indicator *only if* AI response generation isn't about to start
477
+ # This check might be redundant if generate_response always shows it anyway
478
+ if not self.is_generating:
479
+ loading.add_class("hidden")
480
+
481
+ # Generate AI response (will set self.is_generating and handle loading indicator)
482
+ await self.generate_response()
483
+
484
+ # Focus back on input
485
+ input_widget.focus()
410
486
 
411
487
  async def generate_response(self) -> None: # Keep SimpleChatApp generate_response
412
488
  """Generate an AI response.""" # Keep SimpleChatApp generate_response docstring
@@ -648,6 +724,79 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
648
724
  else:
649
725
  input_widget.focus() # Focus input when closing
650
726
 
727
+ async def action_update_title(self) -> None:
728
+ """Allow users to manually change the conversation title"""
729
+ if not self.current_conversation:
730
+ self.notify("No active conversation", severity="warning")
731
+ return
732
+
733
+ # --- Define the Modal Class ---
734
+ class TitleInputModal(Static):
735
+ def __init__(self, current_title: str):
736
+ super().__init__()
737
+ self.current_title = current_title
738
+
739
+ def compose(self) -> ComposeResult:
740
+ with Vertical(id="title-modal"):
741
+ yield Static("Enter new conversation title:", id="modal-label")
742
+ yield Input(value=self.current_title, id="title-input")
743
+ with Horizontal():
744
+ yield Button("Cancel", id="cancel-button", variant="error")
745
+ yield Button("Update", id="update-button", variant="success")
746
+
747
+ @on(Button.Pressed, "#update-button")
748
+ def update_title(self, event: Button.Pressed) -> None:
749
+ input_widget = self.query_one("#title-input", Input)
750
+ new_title = input_widget.value.strip()
751
+ if new_title:
752
+ # Call the app's update method asynchronously
753
+ asyncio.create_task(self.app.update_conversation_title(new_title))
754
+ self.remove() # Close the modal
755
+
756
+ @on(Button.Pressed, "#cancel-button")
757
+ def cancel(self, event: Button.Pressed) -> None:
758
+ self.remove() # Close the modal
759
+
760
+ def on_mount(self) -> None:
761
+ """Focus the input when the modal appears."""
762
+ self.query_one("#title-input", Input).focus()
763
+
764
+ # --- Show the modal ---
765
+ modal = TitleInputModal(self.current_conversation.title)
766
+ await self.mount(modal) # Use await for mounting
767
+
768
+ async def update_conversation_title(self, new_title: str) -> None:
769
+ """Update the current conversation title"""
770
+ if not self.current_conversation:
771
+ return
772
+
773
+ try:
774
+ # Update in database
775
+ self.db.update_conversation(
776
+ self.current_conversation.id,
777
+ title=new_title
778
+ )
779
+
780
+ # Update local object
781
+ self.current_conversation.title = new_title
782
+
783
+ # Update UI
784
+ title_widget = self.query_one("#conversation-title", Static)
785
+ title_widget.update(new_title)
786
+
787
+ # Update any chat list if visible
788
+ # Attempt to refresh ChatList if it exists
789
+ try:
790
+ chat_list = self.query_one(ChatList)
791
+ chat_list.refresh() # Call the refresh method
792
+ except Exception:
793
+ pass # Ignore if ChatList isn't found or refresh fails
794
+
795
+ self.notify("Title updated successfully", severity="information")
796
+ except Exception as e:
797
+ self.notify(f"Failed to update title: {str(e)}", severity="error")
798
+
799
+
651
800
  def main(initial_text: Optional[str] = typer.Argument(None, help="Initial text to start the chat with")): # Keep main function
652
801
  """Entry point for the chat-cli application""" # Keep main function docstring
653
802
  # When no argument is provided, typer passes the ArgumentInfo object # Keep main function
@@ -5,12 +5,75 @@ import asyncio
5
5
  import subprocess
6
6
  import logging
7
7
  from typing import Optional, Dict, Any, List
8
+ from datetime import datetime
8
9
  from .config import CONFIG, save_config
9
10
 
10
11
  # Set up logging
11
12
  logging.basicConfig(level=logging.INFO)
12
13
  logger = logging.getLogger(__name__)
13
14
 
15
+ async def generate_conversation_title(message: str, model: str, client: Any) -> str:
16
+ """Generate a descriptive title for a conversation based on the first message"""
17
+ logger.info(f"Generating title for conversation using model: {model}")
18
+
19
+ # Create a special prompt for title generation
20
+ title_prompt = [
21
+ {
22
+ "role": "system",
23
+ "content": "Generate a brief, descriptive title (maximum 40 characters) for a conversation that starts with the following message. The title should be concise and reflect the main topic or query. Return only the title text with no additional explanation or formatting."
24
+ },
25
+ {
26
+ "role": "user",
27
+ "content": message
28
+ }
29
+ ]
30
+
31
+ tries = 2 # Number of retries
32
+ last_error = None
33
+
34
+ while tries > 0:
35
+ try:
36
+ # Generate a title using the same model but with a separate request
37
+ # Assuming client has a method like generate_completion or similar
38
+ # Adjust the method call based on the actual client implementation
39
+ if hasattr(client, 'generate_completion'):
40
+ title = await client.generate_completion(
41
+ messages=title_prompt,
42
+ model=model,
43
+ temperature=0.7,
44
+ max_tokens=60 # Titles should be short
45
+ )
46
+ elif hasattr(client, 'generate_stream'): # Fallback or alternative method?
47
+ # If generate_completion isn't available, maybe adapt generate_stream?
48
+ # This part needs clarification based on the client's capabilities.
49
+ # For now, let's assume a hypothetical non-streaming call or adapt stream
50
+ # Simplified adaptation: collect stream chunks
51
+ title_chunks = []
52
+ async for chunk in client.generate_stream(title_prompt, model, style=""): # Assuming style might not apply or needs default
53
+ title_chunks.append(chunk)
54
+ title = "".join(title_chunks)
55
+ else:
56
+ raise NotImplementedError("Client does not support a suitable method for title generation.")
57
+
58
+ # Sanitize and limit the title
59
+ title = title.strip().strip('"\'').strip()
60
+ if len(title) > 40: # Set a maximum title length
61
+ title = title[:37] + "..."
62
+
63
+ logger.info(f"Generated title: {title}")
64
+ return title # Return successful title
65
+
66
+ except Exception as e:
67
+ last_error = str(e)
68
+ logger.error(f"Error generating title (tries left: {tries - 1}): {last_error}")
69
+ tries -= 1
70
+ if tries > 0: # Only sleep if there are more retries
71
+ await asyncio.sleep(1) # Small delay before retry
72
+
73
+ # If all retries fail, log the last error and return a default title
74
+ logger.error(f"Failed to generate title after multiple retries. Last error: {last_error}")
75
+ return f"Conversation ({datetime.now().strftime('%Y-%m-%d %H:%M')})"
76
+
14
77
  async def generate_streaming_response(messages: List[Dict], model: str, style: str, client: Any, callback: Any) -> str:
15
78
  """Generate a streaming response from the model"""
16
79
  logger.info(f"Starting streaming response with model: {model}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chat-console
3
- Version: 0.1.95.dev1
3
+ Version: 0.1.96.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
@@ -14,7 +14,7 @@ with open(os.path.join("app", "__init__.py"), "r", encoding="utf-8") as f:
14
14
 
15
15
  setup(
16
16
  name="chat-console",
17
- version="0.1.95.dev1",
17
+ version="0.1.96.dev1",
18
18
  author="Johnathan Greenaway",
19
19
  author_email="john@fimbriata.dev",
20
20
  description="A command-line interface for chatting with LLMs, storing chats and (future) rag interactions",