chat-console 0.1.95__py3-none-any.whl → 0.1.96.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/config.py +2 -1
- app/main.py +157 -8
- app/utils.py +63 -0
- {chat_console-0.1.95.dist-info → chat_console-0.1.96.dev1.dist-info}/METADATA +1 -1
- {chat_console-0.1.95.dist-info → chat_console-0.1.96.dev1.dist-info}/RECORD +9 -9
- {chat_console-0.1.95.dist-info → chat_console-0.1.96.dev1.dist-info}/WHEEL +0 -0
- {chat_console-0.1.95.dist-info → chat_console-0.1.96.dev1.dist-info}/entry_points.txt +0 -0
- {chat_console-0.1.95.dist-info → chat_console-0.1.96.dev1.dist-info}/licenses/LICENSE +0 -0
- {chat_console-0.1.95.dist-info → chat_console-0.1.96.dev1.dist-info}/top_level.txt +0 -0
app/config.py
CHANGED
app/main.py
CHANGED
@@ -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
|
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
|
-
#
|
403
|
-
|
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
|
-
#
|
406
|
-
await self.
|
434
|
+
# Update UI with user message first
|
435
|
+
await self.update_messages_ui()
|
407
436
|
|
408
|
-
#
|
409
|
-
|
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
|
app/utils.py
CHANGED
@@ -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.
|
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
|
@@ -1,9 +1,9 @@
|
|
1
1
|
app/__init__.py,sha256=OeqboIrx_Kjea0CY9Be8nLI-No1YWQfqbWIp-4lMOOI,131
|
2
|
-
app/config.py,sha256=
|
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=SfeSORMICQR5GPwniN-Hg0TH9xvhmWHvkhTdyxjbK3k,43450
|
5
5
|
app/models.py,sha256=4-y9Lytay2exWPFi0FDlVeRL3K2-I7E-jBqNzTfokqY,2644
|
6
|
-
app/utils.py,sha256=
|
6
|
+
app/utils.py,sha256=VzsdBvuj8zk7HsLlBjIvq6jvU1G2lSEE1Ml6_sbYs1I,7432
|
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
|
@@ -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.1.
|
19
|
-
chat_console-0.1.
|
20
|
-
chat_console-0.1.
|
21
|
-
chat_console-0.1.
|
22
|
-
chat_console-0.1.
|
23
|
-
chat_console-0.1.
|
18
|
+
chat_console-0.1.96.dev1.dist-info/licenses/LICENSE,sha256=srHZ3fvcAuZY1LHxE7P6XWju2njRCHyK6h_ftEbzxSE,1057
|
19
|
+
chat_console-0.1.96.dev1.dist-info/METADATA,sha256=EVvIxf_H7acsAEoSRl2Jw-LeIPdwJVLLS6ca7KcyOXY,2927
|
20
|
+
chat_console-0.1.96.dev1.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
21
|
+
chat_console-0.1.96.dev1.dist-info/entry_points.txt,sha256=kkVdEc22U9PAi2AeruoKklfkng_a_aHAP6VRVwrAD7c,67
|
22
|
+
chat_console-0.1.96.dev1.dist-info/top_level.txt,sha256=io9g7LCbfmTG1SFKgEOGXmCFB9uMP2H5lerm0HiHWQE,4
|
23
|
+
chat_console-0.1.96.dev1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|