chat-console 0.3.6__py3-none-any.whl → 0.3.8__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/base.py +42 -16
- app/main.py +66 -22
- app/ui/chat_interface.py +22 -1
- app/utils.py +197 -82
- {chat_console-0.3.6.dist-info → chat_console-0.3.8.dist-info}/METADATA +1 -1
- {chat_console-0.3.6.dist-info → chat_console-0.3.8.dist-info}/RECORD +11 -11
- {chat_console-0.3.6.dist-info → chat_console-0.3.8.dist-info}/WHEEL +1 -1
- {chat_console-0.3.6.dist-info → chat_console-0.3.8.dist-info}/entry_points.txt +0 -0
- {chat_console-0.3.6.dist-info → chat_console-0.3.8.dist-info}/licenses/LICENSE +0 -0
- {chat_console-0.3.6.dist-info → chat_console-0.3.8.dist-info}/top_level.txt +0 -0
app/__init__.py
CHANGED
app/api/base.py
CHANGED
@@ -38,27 +38,41 @@ class BaseModelClient(ABC):
|
|
38
38
|
|
39
39
|
logger = logging.getLogger(__name__)
|
40
40
|
|
41
|
+
# Safety check for None or empty string
|
42
|
+
if not model_name:
|
43
|
+
logger.warning("Empty model name passed to get_client_type_for_model")
|
44
|
+
return None
|
45
|
+
|
41
46
|
# Get model info and provider
|
42
47
|
model_info = CONFIG["available_models"].get(model_name)
|
43
48
|
model_name_lower = model_name.lower()
|
44
49
|
|
50
|
+
# Debug log the model name
|
51
|
+
logger.info(f"Getting client type for model: {model_name}")
|
52
|
+
|
45
53
|
# If model is in config, use its provider
|
46
54
|
if model_info:
|
47
55
|
provider = model_info["provider"]
|
56
|
+
logger.info(f"Found model in config with provider: {provider}")
|
48
57
|
# For custom models, try to infer provider
|
49
58
|
else:
|
50
|
-
# First
|
51
|
-
if
|
52
|
-
model_name in [m["id"] for m in CONFIG.get("ollama_models", [])]):
|
53
|
-
provider = "ollama"
|
54
|
-
# Then try other providers
|
55
|
-
elif any(name in model_name_lower for name in ["gpt", "text-", "davinci"]):
|
59
|
+
# First check for OpenAI models - these should ALWAYS use OpenAI client
|
60
|
+
if any(name in model_name_lower for name in ["gpt", "text-", "davinci"]):
|
56
61
|
provider = "openai"
|
62
|
+
logger.info(f"Identified as OpenAI model: {model_name}")
|
63
|
+
# Then check for Anthropic models - these should ALWAYS use Anthropic client
|
57
64
|
elif any(name in model_name_lower for name in ["claude", "anthropic"]):
|
58
65
|
provider = "anthropic"
|
66
|
+
logger.info(f"Identified as Anthropic model: {model_name}")
|
67
|
+
# Then try Ollama for known model names or if selected from Ollama UI
|
68
|
+
elif (any(name in model_name_lower for name in ["llama", "mistral", "codellama", "gemma"]) or
|
69
|
+
model_name in [m["id"] for m in CONFIG.get("ollama_models", [])]):
|
70
|
+
provider = "ollama"
|
71
|
+
logger.info(f"Identified as Ollama model: {model_name}")
|
59
72
|
else:
|
60
73
|
# Default to Ollama for unknown models
|
61
74
|
provider = "ollama"
|
75
|
+
logger.info(f"Unknown model type, defaulting to Ollama: {model_name}")
|
62
76
|
|
63
77
|
# Return appropriate client class
|
64
78
|
if provider == "ollama":
|
@@ -81,6 +95,14 @@ class BaseModelClient(ABC):
|
|
81
95
|
|
82
96
|
logger = logging.getLogger(__name__)
|
83
97
|
|
98
|
+
# Safety check for None or empty string
|
99
|
+
if not model_name:
|
100
|
+
logger.warning("Empty model name passed to get_client_for_model")
|
101
|
+
raise ValueError("Model name cannot be empty")
|
102
|
+
|
103
|
+
# Log the model name we're getting a client for
|
104
|
+
logger.info(f"Getting client for model: {model_name}")
|
105
|
+
|
84
106
|
# Get model info and provider
|
85
107
|
model_info = CONFIG["available_models"].get(model_name)
|
86
108
|
model_name_lower = model_name.lower()
|
@@ -88,31 +110,35 @@ class BaseModelClient(ABC):
|
|
88
110
|
# If model is in config, use its provider
|
89
111
|
if model_info:
|
90
112
|
provider = model_info["provider"]
|
113
|
+
logger.info(f"Found model in config with provider: {provider}")
|
91
114
|
if not AVAILABLE_PROVIDERS[provider]:
|
92
115
|
raise Exception(f"Provider '{provider}' is not available. Please check your configuration.")
|
93
116
|
# For custom models, try to infer provider
|
94
117
|
else:
|
95
|
-
# First
|
96
|
-
if
|
97
|
-
model_name in [m["id"] for m in CONFIG.get("ollama_models", [])]):
|
98
|
-
if not AVAILABLE_PROVIDERS["ollama"]:
|
99
|
-
raise Exception("Ollama server is not running. Please start Ollama and try again.")
|
100
|
-
provider = "ollama"
|
101
|
-
logger.info(f"Using Ollama for model: {model_name}")
|
102
|
-
# Then try other providers if they're available
|
103
|
-
elif any(name in model_name_lower for name in ["gpt", "text-", "davinci"]):
|
118
|
+
# First check for OpenAI models - these should ALWAYS use OpenAI client
|
119
|
+
if any(name in model_name_lower for name in ["gpt", "text-", "davinci"]):
|
104
120
|
if not AVAILABLE_PROVIDERS["openai"]:
|
105
121
|
raise Exception("OpenAI API key not found. Please set OPENAI_API_KEY environment variable.")
|
106
122
|
provider = "openai"
|
123
|
+
logger.info(f"Identified as OpenAI model: {model_name}")
|
124
|
+
# Then check for Anthropic models - these should ALWAYS use Anthropic client
|
107
125
|
elif any(name in model_name_lower for name in ["claude", "anthropic"]):
|
108
126
|
if not AVAILABLE_PROVIDERS["anthropic"]:
|
109
127
|
raise Exception("Anthropic API key not found. Please set ANTHROPIC_API_KEY environment variable.")
|
110
128
|
provider = "anthropic"
|
129
|
+
logger.info(f"Identified as Anthropic model: {model_name}")
|
130
|
+
# Then try Ollama for known model names or if selected from Ollama UI
|
131
|
+
elif (any(name in model_name_lower for name in ["llama", "mistral", "codellama", "gemma"]) or
|
132
|
+
model_name in [m["id"] for m in CONFIG.get("ollama_models", [])]):
|
133
|
+
if not AVAILABLE_PROVIDERS["ollama"]:
|
134
|
+
raise Exception("Ollama server is not running. Please start Ollama and try again.")
|
135
|
+
provider = "ollama"
|
136
|
+
logger.info(f"Identified as Ollama model: {model_name}")
|
111
137
|
else:
|
112
138
|
# Default to Ollama for unknown models
|
113
139
|
if AVAILABLE_PROVIDERS["ollama"]:
|
114
140
|
provider = "ollama"
|
115
|
-
logger.info(f"
|
141
|
+
logger.info(f"Unknown model type, defaulting to Ollama: {model_name}")
|
116
142
|
else:
|
117
143
|
raise Exception(f"Unknown model: {model_name}")
|
118
144
|
|
app/main.py
CHANGED
@@ -707,10 +707,18 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
|
|
707
707
|
else:
|
708
708
|
raise Exception("No valid API clients available for title generation")
|
709
709
|
|
710
|
-
# Generate title
|
710
|
+
# Generate title - make sure we're using the right client for the model
|
711
711
|
print(f"Calling generate_conversation_title with model: {model}")
|
712
712
|
log(f"Calling generate_conversation_title with model: {model}")
|
713
|
-
debug_log(f"Calling generate_conversation_title with model: {model}")
|
713
|
+
debug_log(f"Calling generate_conversation_title with model: {model}, client type: {type(client).__name__}")
|
714
|
+
|
715
|
+
# Double-check that we're using the right client for this model
|
716
|
+
expected_client_type = BaseModelClient.get_client_type_for_model(model)
|
717
|
+
if expected_client_type and not isinstance(client, expected_client_type):
|
718
|
+
debug_log(f"Warning: Client type mismatch. Expected {expected_client_type.__name__}, got {type(client).__name__}")
|
719
|
+
debug_log("Creating new client with correct type")
|
720
|
+
client = await BaseModelClient.get_client_for_model(model)
|
721
|
+
|
714
722
|
title = await generate_conversation_title(content, model, client)
|
715
723
|
debug_log(f"Generated title: {title}")
|
716
724
|
log(f"Generated title: {title}")
|
@@ -729,11 +737,9 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
|
|
729
737
|
# Update conversation object
|
730
738
|
self.current_conversation.title = title
|
731
739
|
|
732
|
-
#
|
733
|
-
#
|
734
|
-
|
735
|
-
debug_log(f"Using same model for chat response: '{model}'")
|
736
|
-
self.selected_model = model
|
740
|
+
# DO NOT update the selected model here - keep the user's original selection
|
741
|
+
# This was causing issues with model mixing
|
742
|
+
debug_log(f"Keeping original selected model: '{self.selected_model}'")
|
737
743
|
|
738
744
|
self.notify(f"Conversation title set to: {title}", severity="information", timeout=3)
|
739
745
|
|
@@ -805,17 +811,23 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
|
|
805
811
|
style = self.selected_style
|
806
812
|
|
807
813
|
debug_log(f"Using model: '{model}', style: '{style}'")
|
814
|
+
|
815
|
+
# Determine the expected client type for this model
|
816
|
+
expected_client_type = BaseModelClient.get_client_type_for_model(model)
|
817
|
+
debug_log(f"Expected client type for {model}: {expected_client_type.__name__ if expected_client_type else 'None'}")
|
808
818
|
|
809
819
|
# Ensure we have a valid model
|
810
820
|
if not model:
|
811
821
|
debug_log("Model is empty, selecting a default model")
|
812
|
-
#
|
822
|
+
# Check which providers are available and select an appropriate default
|
813
823
|
if OPENAI_API_KEY:
|
814
824
|
model = "gpt-3.5-turbo"
|
815
|
-
|
825
|
+
expected_client_type = BaseModelClient.get_client_type_for_model(model)
|
826
|
+
debug_log(f"Falling back to OpenAI gpt-3.5-turbo with client type {expected_client_type.__name__ if expected_client_type else 'None'}")
|
816
827
|
elif ANTHROPIC_API_KEY:
|
817
|
-
model = "claude-
|
818
|
-
|
828
|
+
model = "claude-3-haiku-20240307" # Updated to newer Claude model
|
829
|
+
expected_client_type = BaseModelClient.get_client_type_for_model(model)
|
830
|
+
debug_log(f"Falling back to Anthropic Claude 3 Haiku with client type {expected_client_type.__name__ if expected_client_type else 'None'}")
|
819
831
|
else:
|
820
832
|
# Check for a common Ollama model
|
821
833
|
try:
|
@@ -826,11 +838,13 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
|
|
826
838
|
model = models[0].get("id", "llama3")
|
827
839
|
else:
|
828
840
|
model = "llama3" # Common default
|
829
|
-
|
841
|
+
expected_client_type = BaseModelClient.get_client_type_for_model(model)
|
842
|
+
debug_log(f"Falling back to Ollama model: {model} with client type {expected_client_type.__name__ if expected_client_type else 'None'}")
|
830
843
|
except Exception as ollama_err:
|
831
844
|
debug_log(f"Error getting Ollama models: {str(ollama_err)}")
|
832
845
|
model = "llama3" # Final fallback
|
833
|
-
|
846
|
+
expected_client_type = BaseModelClient.get_client_type_for_model(model)
|
847
|
+
debug_log(f"Final fallback to llama3 with client type {expected_client_type.__name__ if expected_client_type else 'None'}")
|
834
848
|
|
835
849
|
# Convert messages to API format with enhanced error checking
|
836
850
|
api_messages = []
|
@@ -926,27 +940,39 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
|
|
926
940
|
last_refresh_time = time.time() # Initialize refresh throttling timer
|
927
941
|
|
928
942
|
async def update_ui(content: str):
|
929
|
-
# This function
|
943
|
+
# This function is called by the worker with each content update
|
930
944
|
if not self.is_generating:
|
931
945
|
debug_log("update_ui called but is_generating is False, returning.")
|
932
946
|
return
|
933
947
|
|
934
948
|
async with update_lock:
|
935
949
|
try:
|
950
|
+
# Add more verbose logging
|
951
|
+
debug_log(f"update_ui called with content length: {len(content)}")
|
952
|
+
print(f"update_ui: Updating with content length {len(content)}")
|
953
|
+
|
936
954
|
# Clear thinking indicator on first content
|
937
955
|
if assistant_message.content == "Thinking...":
|
938
956
|
debug_log("First content received, clearing 'Thinking...'")
|
939
957
|
print("First content received, clearing 'Thinking...'")
|
940
|
-
|
941
|
-
|
958
|
+
# We'll let the MessageDisplay.update_content handle this special case
|
959
|
+
|
942
960
|
# Update the message object with the full content
|
943
961
|
assistant_message.content = content
|
944
962
|
|
945
|
-
# Update UI with the content
|
963
|
+
# Update UI with the content - this now has special handling for "Thinking..."
|
964
|
+
debug_log("Calling message_display.update_content")
|
946
965
|
await message_display.update_content(content)
|
947
966
|
|
948
|
-
#
|
967
|
+
# More aggressive UI refresh sequence
|
968
|
+
debug_log("Performing UI refresh sequence")
|
969
|
+
# First do a lightweight refresh
|
970
|
+
self.refresh(layout=False)
|
971
|
+
# Then scroll to end
|
972
|
+
messages_container.scroll_end(animate=False)
|
973
|
+
# Then do a full layout refresh
|
949
974
|
self.refresh(layout=True)
|
975
|
+
# Final scroll to ensure visibility
|
950
976
|
messages_container.scroll_end(animate=False)
|
951
977
|
|
952
978
|
except Exception as e:
|
@@ -1016,14 +1042,32 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
|
|
1016
1042
|
error = worker.error
|
1017
1043
|
debug_log(f"Error in generation worker: {error}")
|
1018
1044
|
log.error(f"Error in generation worker: {error}")
|
1019
|
-
|
1045
|
+
|
1046
|
+
# Sanitize error message for UI display
|
1047
|
+
error_str = str(error)
|
1048
|
+
|
1049
|
+
# Check if this is an Ollama error
|
1050
|
+
is_ollama_error = "ollama" in error_str.lower() or "404" in error_str
|
1051
|
+
|
1052
|
+
# Create a user-friendly error message
|
1053
|
+
if is_ollama_error:
|
1054
|
+
# For Ollama errors, provide a more user-friendly message
|
1055
|
+
user_error = "Unable to generate response. The selected model may not be available."
|
1056
|
+
debug_log(f"Sanitizing Ollama error to user-friendly message: {user_error}")
|
1057
|
+
# Show technical details only in notification, not in chat
|
1058
|
+
self.notify(f"Model error: {error_str}", severity="error", timeout=5)
|
1059
|
+
else:
|
1060
|
+
# For other errors, show a generic message
|
1061
|
+
user_error = f"Error generating response: {error_str}"
|
1062
|
+
self.notify(f"Generation error: {error_str}", severity="error", timeout=5)
|
1063
|
+
|
1020
1064
|
# Add error message to UI
|
1021
1065
|
if self.messages and self.messages[-1].role == "assistant":
|
1022
1066
|
debug_log("Removing thinking message")
|
1023
1067
|
self.messages.pop() # Remove thinking message
|
1024
|
-
|
1025
|
-
debug_log(f"Adding error message: {
|
1026
|
-
self.messages.append(Message(role="assistant", content=
|
1068
|
+
|
1069
|
+
debug_log(f"Adding error message: {user_error}")
|
1070
|
+
self.messages.append(Message(role="assistant", content=user_error))
|
1027
1071
|
await self.update_messages_ui()
|
1028
1072
|
|
1029
1073
|
elif worker.state == "success":
|
app/ui/chat_interface.py
CHANGED
@@ -121,11 +121,23 @@ class MessageDisplay(Static): # Inherit from Static instead of RichLog
|
|
121
121
|
|
122
122
|
async def update_content(self, content: str) -> None:
|
123
123
|
"""Update the message content using Static.update() with optimizations for streaming"""
|
124
|
+
# Debug print to verify method is being called with content
|
125
|
+
print(f"MessageDisplay.update_content called with content length: {len(content)}")
|
126
|
+
|
124
127
|
# Quick unchanged content check to avoid unnecessary updates
|
125
128
|
if self.message.content == content:
|
129
|
+
print("Content unchanged, skipping update")
|
126
130
|
return
|
127
131
|
|
128
|
-
#
|
132
|
+
# Special handling for "Thinking..." to ensure it gets replaced
|
133
|
+
if self.message.content == "Thinking..." and content:
|
134
|
+
print("Replacing 'Thinking...' with actual content")
|
135
|
+
# Force a complete replacement rather than an append
|
136
|
+
self.message.content = ""
|
137
|
+
# Add a debug print to confirm this branch is executed
|
138
|
+
print("CRITICAL FIX: Replacing 'Thinking...' placeholder with actual content")
|
139
|
+
|
140
|
+
# Update the stored message object content
|
129
141
|
self.message.content = content
|
130
142
|
|
131
143
|
# Format with fixed-width placeholder to minimize layout shifts
|
@@ -134,6 +146,7 @@ class MessageDisplay(Static): # Inherit from Static instead of RichLog
|
|
134
146
|
|
135
147
|
# Use a direct update that forces refresh - critical fix for streaming
|
136
148
|
# This ensures content is immediately visible
|
149
|
+
print(f"Updating widget with formatted content length: {len(formatted_content)}")
|
137
150
|
self.update(formatted_content, refresh=True)
|
138
151
|
|
139
152
|
# Force app-level refresh and scroll to ensure visibility
|
@@ -148,6 +161,9 @@ class MessageDisplay(Static): # Inherit from Static instead of RichLog
|
|
148
161
|
for container in containers:
|
149
162
|
if hasattr(container, 'scroll_end'):
|
150
163
|
container.scroll_end(animate=False)
|
164
|
+
|
165
|
+
# Add an additional refresh after scrolling
|
166
|
+
self.app.refresh(layout=True)
|
151
167
|
except Exception as e:
|
152
168
|
# Log the error and fallback to local refresh
|
153
169
|
print(f"Error refreshing app: {str(e)}")
|
@@ -157,6 +173,11 @@ class MessageDisplay(Static): # Inherit from Static instead of RichLog
|
|
157
173
|
"""Format message content with timestamp and handle markdown links"""
|
158
174
|
timestamp = datetime.now().strftime("%H:%M")
|
159
175
|
|
176
|
+
# Special handling for "Thinking..." to make it visually distinct
|
177
|
+
if content == "Thinking...":
|
178
|
+
# Use italic style for the thinking indicator
|
179
|
+
return f"[dim]{timestamp}[/dim] [italic]{content}[/italic]"
|
180
|
+
|
160
181
|
# Fix markdown-style links that cause markup errors
|
161
182
|
# Convert [text](url) to a safe format for Textual markup
|
162
183
|
content = re.sub(
|
app/utils.py
CHANGED
@@ -20,33 +20,108 @@ logger = logging.getLogger(__name__)
|
|
20
20
|
|
21
21
|
async def generate_conversation_title(message: str, model: str, client: Any) -> str:
|
22
22
|
"""Generate a descriptive title for a conversation based on the first message"""
|
23
|
+
try:
|
24
|
+
from app.main import debug_log
|
25
|
+
except ImportError:
|
26
|
+
debug_log = lambda msg: None
|
27
|
+
|
28
|
+
debug_log(f"Starting title generation with model: {model}, client type: {type(client).__name__}")
|
29
|
+
|
23
30
|
# --- Choose a specific, reliable model for title generation ---
|
24
|
-
#
|
31
|
+
# First, determine if we have a valid client
|
32
|
+
if client is None:
|
33
|
+
debug_log("Client is None, will use default title")
|
34
|
+
return f"Conversation ({datetime.now().strftime('%Y-%m-%d %H:%M')})"
|
35
|
+
|
36
|
+
# Determine the best model to use for title generation
|
25
37
|
title_model_id = None
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
38
|
+
|
39
|
+
# Check if client is Anthropic
|
40
|
+
is_anthropic = 'anthropic' in str(type(client)).lower()
|
41
|
+
if is_anthropic:
|
42
|
+
debug_log("Using Anthropic client for title generation")
|
43
|
+
# Try to get available models safely
|
44
|
+
try:
|
45
|
+
available_anthropic_models = client.get_available_models()
|
46
|
+
debug_log(f"Found {len(available_anthropic_models)} Anthropic models")
|
47
|
+
|
48
|
+
# Try Claude 3 Haiku first (fastest)
|
49
|
+
haiku_id = "claude-3-haiku-20240307"
|
50
|
+
if any(m.get("id") == haiku_id for m in available_anthropic_models):
|
51
|
+
title_model_id = haiku_id
|
52
|
+
debug_log(f"Using Anthropic Haiku for title generation: {title_model_id}")
|
53
|
+
else:
|
54
|
+
# If Haiku not found, try Sonnet
|
55
|
+
sonnet_id = "claude-3-sonnet-20240229"
|
56
|
+
if any(m.get("id") == sonnet_id for m in available_anthropic_models):
|
57
|
+
title_model_id = sonnet_id
|
58
|
+
debug_log(f"Using Anthropic Sonnet for title generation: {title_model_id}")
|
59
|
+
else:
|
60
|
+
debug_log("Neither Haiku nor Sonnet found in Anthropic models list")
|
61
|
+
except Exception as e:
|
62
|
+
debug_log(f"Error getting Anthropic models: {str(e)}")
|
63
|
+
|
64
|
+
# Check if client is OpenAI
|
65
|
+
is_openai = 'openai' in str(type(client)).lower()
|
66
|
+
if is_openai:
|
67
|
+
debug_log("Using OpenAI client for title generation")
|
68
|
+
# Use GPT-3.5 for title generation (fast and cost-effective)
|
69
|
+
title_model_id = "gpt-3.5-turbo"
|
70
|
+
debug_log(f"Using OpenAI model for title generation: {title_model_id}")
|
71
|
+
# For OpenAI, we'll always use their model, not fall back to the passed model
|
72
|
+
# This prevents trying to use Ollama models with OpenAI client
|
73
|
+
|
74
|
+
# Check if client is Ollama
|
75
|
+
is_ollama = 'ollama' in str(type(client)).lower()
|
76
|
+
if is_ollama and not title_model_id:
|
77
|
+
debug_log("Using Ollama client for title generation")
|
78
|
+
# For Ollama, check if the model exists before using it
|
79
|
+
try:
|
80
|
+
# Try a quick test request to check if model exists
|
81
|
+
debug_log(f"Testing if Ollama model exists: {model}")
|
82
|
+
import aiohttp
|
83
|
+
async with aiohttp.ClientSession() as session:
|
84
|
+
try:
|
85
|
+
base_url = "http://localhost:11434"
|
86
|
+
async with session.post(
|
87
|
+
f"{base_url}/api/generate",
|
88
|
+
json={"model": model, "prompt": "test", "stream": False},
|
89
|
+
timeout=2
|
90
|
+
) as response:
|
91
|
+
if response.status == 200:
|
92
|
+
# Model exists, use it
|
93
|
+
title_model_id = model
|
94
|
+
debug_log(f"Ollama model {model} exists, using it for title generation")
|
95
|
+
else:
|
96
|
+
debug_log(f"Ollama model {model} returned status {response.status}, falling back to default")
|
97
|
+
# Fall back to a common model
|
98
|
+
title_model_id = "llama3"
|
99
|
+
except Exception as e:
|
100
|
+
debug_log(f"Error testing Ollama model: {str(e)}, falling back to default")
|
101
|
+
# Fall back to a common model
|
102
|
+
title_model_id = "llama3"
|
103
|
+
except Exception as e:
|
104
|
+
debug_log(f"Error checking Ollama model: {str(e)}")
|
105
|
+
# Fall back to a common model
|
106
|
+
title_model_id = "llama3"
|
107
|
+
|
108
|
+
# Fallback logic if no specific model was found
|
43
109
|
if not title_model_id:
|
44
|
-
# Use
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
110
|
+
# Use a safe default based on client type
|
111
|
+
if is_openai:
|
112
|
+
title_model_id = "gpt-3.5-turbo"
|
113
|
+
elif is_anthropic:
|
114
|
+
title_model_id = "claude-3-haiku-20240307"
|
115
|
+
elif is_ollama:
|
116
|
+
title_model_id = "llama3" # Common default
|
117
|
+
else:
|
118
|
+
# Last resort - use the originally passed model
|
119
|
+
title_model_id = model
|
120
|
+
|
121
|
+
debug_log(f"No specific model found, using fallback model for title generation: {title_model_id}")
|
122
|
+
|
49
123
|
logger.info(f"Generating title for conversation using model: {title_model_id}")
|
124
|
+
debug_log(f"Final model selected for title generation: {title_model_id}")
|
50
125
|
|
51
126
|
# Create a special prompt for title generation
|
52
127
|
title_prompt = [
|
@@ -65,36 +140,44 @@ async def generate_conversation_title(message: str, model: str, client: Any) ->
|
|
65
140
|
|
66
141
|
while tries > 0:
|
67
142
|
try:
|
68
|
-
|
69
|
-
#
|
70
|
-
# Adjust the method call based on the actual client implementation
|
143
|
+
debug_log(f"Attempt {3-tries} to generate title")
|
144
|
+
# First try generate_completion if available
|
71
145
|
if hasattr(client, 'generate_completion'):
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
146
|
+
debug_log("Using generate_completion method")
|
147
|
+
try:
|
148
|
+
title = await client.generate_completion(
|
149
|
+
messages=title_prompt,
|
150
|
+
model=title_model_id,
|
151
|
+
temperature=0.7,
|
152
|
+
max_tokens=60 # Titles should be short
|
153
|
+
)
|
154
|
+
debug_log(f"Title generated successfully: {title}")
|
155
|
+
except Exception as completion_error:
|
156
|
+
debug_log(f"Error in generate_completion: {str(completion_error)}")
|
157
|
+
raise # Re-raise to be caught by outer try/except
|
158
|
+
# Fall back to generate_stream if completion not available
|
159
|
+
elif hasattr(client, 'generate_stream'):
|
160
|
+
debug_log("Using generate_stream method")
|
161
|
+
title_chunks = []
|
162
|
+
try:
|
163
|
+
async for chunk in client.generate_stream(title_prompt, title_model_id, style=""):
|
164
|
+
if chunk is not None:
|
165
|
+
title_chunks.append(chunk)
|
166
|
+
debug_log(f"Received chunk of length: {len(chunk)}")
|
167
|
+
|
168
|
+
title = "".join(title_chunks)
|
169
|
+
debug_log(f"Combined title from chunks: {title}")
|
170
|
+
|
171
|
+
# If we didn't get any content, use a default
|
172
|
+
if not title.strip():
|
173
|
+
debug_log("Empty title received, using default")
|
174
|
+
title = f"Conversation ({datetime.now().strftime('%Y-%m-%d %H:%M')})"
|
175
|
+
except Exception as stream_error:
|
176
|
+
debug_log(f"Error during title stream processing: {str(stream_error)}")
|
177
|
+
raise # Re-raise to be caught by outer try/except
|
96
178
|
else:
|
97
|
-
|
179
|
+
debug_log("Client does not support any title generation method")
|
180
|
+
raise NotImplementedError("Client does not support a suitable method for title generation.")
|
98
181
|
|
99
182
|
# Sanitize and limit the title
|
100
183
|
title = title.strip().strip('"\'').strip()
|
@@ -102,20 +185,23 @@ async def generate_conversation_title(message: str, model: str, client: Any) ->
|
|
102
185
|
title = title[:37] + "..."
|
103
186
|
|
104
187
|
logger.info(f"Generated title: {title}")
|
105
|
-
|
188
|
+
debug_log(f"Final sanitized title: {title}")
|
189
|
+
return title # Return successful title
|
106
190
|
|
107
191
|
except Exception as e:
|
108
192
|
last_error = str(e)
|
109
|
-
|
193
|
+
debug_log(f"Error generating title (tries left: {tries-1}): {last_error}")
|
194
|
+
logger.error(f"Error generating title (tries left: {tries-1}): {last_error}")
|
110
195
|
tries -= 1
|
111
|
-
if tries > 0:
|
196
|
+
if tries > 0: # Only sleep if there are more retries
|
112
197
|
await asyncio.sleep(1) # Small delay before retry
|
113
198
|
|
114
|
-
# If all retries fail, log the
|
199
|
+
# If all retries fail, log the error and return a default title
|
200
|
+
debug_log(f"Failed to generate title after multiple retries. Using default title.")
|
115
201
|
logger.error(f"Failed to generate title after multiple retries. Last error: {last_error}")
|
116
202
|
return f"Conversation ({datetime.now().strftime('%Y-%m-%d %H:%M')})"
|
117
203
|
|
118
|
-
#
|
204
|
+
# Worker function for streaming response generation
|
119
205
|
async def generate_streaming_response(
|
120
206
|
app: 'SimpleChatApp',
|
121
207
|
messages: List[Dict],
|
@@ -136,10 +222,12 @@ async def generate_streaming_response(
|
|
136
222
|
logger.info(f"Starting streaming response with model: {model}")
|
137
223
|
debug_log(f"Starting streaming response with model: '{model}', client type: {type(client).__name__}")
|
138
224
|
|
225
|
+
# Validate messages
|
139
226
|
if not messages:
|
140
227
|
debug_log("Error: messages list is empty")
|
141
228
|
raise ValueError("Messages list cannot be empty")
|
142
229
|
|
230
|
+
# Ensure all messages have required fields
|
143
231
|
for i, msg in enumerate(messages):
|
144
232
|
try:
|
145
233
|
debug_log(f"Message {i}: role={msg.get('role', 'missing')}, content_len={len(msg.get('content', ''))}")
|
@@ -157,14 +245,14 @@ async def generate_streaming_response(
|
|
157
245
|
}
|
158
246
|
debug_log(f"Repaired message {i}")
|
159
247
|
|
160
|
-
|
161
|
-
|
248
|
+
# Initialize variables for response tracking
|
162
249
|
full_response = ""
|
163
250
|
buffer = []
|
164
251
|
last_update = time.time()
|
165
252
|
update_interval = 0.05 # Reduced interval for more frequent updates
|
166
253
|
|
167
254
|
try:
|
255
|
+
# Validate client
|
168
256
|
if client is None:
|
169
257
|
debug_log("Error: client is None, cannot proceed with streaming")
|
170
258
|
raise ValueError("Model client is None, cannot proceed with streaming")
|
@@ -173,9 +261,15 @@ async def generate_streaming_response(
|
|
173
261
|
debug_log(f"Error: client {type(client).__name__} does not have generate_stream method")
|
174
262
|
raise ValueError(f"Client {type(client).__name__} does not support streaming")
|
175
263
|
|
264
|
+
# Determine client type
|
176
265
|
is_ollama = 'ollama' in str(type(client)).lower()
|
177
|
-
|
266
|
+
is_openai = 'openai' in str(type(client)).lower()
|
267
|
+
is_anthropic = 'anthropic' in str(type(client)).lower()
|
268
|
+
|
269
|
+
debug_log(f"Client types - Ollama: {is_ollama}, OpenAI: {is_openai}, Anthropic: {is_anthropic}")
|
178
270
|
|
271
|
+
# Only show loading indicator for Ollama (which may need to load models)
|
272
|
+
# This prevents Ollama-specific UI elements from showing when using other providers
|
179
273
|
if is_ollama and hasattr(app, 'query_one'):
|
180
274
|
try:
|
181
275
|
debug_log("Showing initial model loading indicator for Ollama")
|
@@ -190,6 +284,7 @@ async def generate_streaming_response(
|
|
190
284
|
debug_log(f"Starting stream generation with messages length: {len(messages)}")
|
191
285
|
logger.info(f"Starting stream generation for model: {model}")
|
192
286
|
|
287
|
+
# Initialize stream generator
|
193
288
|
try:
|
194
289
|
debug_log("Calling client.generate_stream()")
|
195
290
|
stream_generator = client.generate_stream(messages, model, style)
|
@@ -199,10 +294,12 @@ async def generate_streaming_response(
|
|
199
294
|
logger.error(f"Error initializing stream generator: {str(stream_init_error)}")
|
200
295
|
raise
|
201
296
|
|
202
|
-
|
297
|
+
# Update UI if model is ready (Ollama specific)
|
298
|
+
# Only check is_loading_model for Ollama clients to prevent errors with other providers
|
299
|
+
if is_ollama and hasattr(client, 'is_loading_model') and not client.is_loading_model() and hasattr(app, 'query_one'):
|
203
300
|
try:
|
204
|
-
debug_log("
|
205
|
-
logger.info("
|
301
|
+
debug_log("Ollama model is ready for generation, updating UI")
|
302
|
+
logger.info("Ollama model is ready for generation, updating UI")
|
206
303
|
loading = app.query_one("#loading-indicator")
|
207
304
|
loading.remove_class("model-loading")
|
208
305
|
loading.update("▪▪▪ Generating response...")
|
@@ -210,9 +307,11 @@ async def generate_streaming_response(
|
|
210
307
|
debug_log(f"Error updating UI after stream init: {str(e)}")
|
211
308
|
logger.error(f"Error updating UI after stream init: {str(e)}")
|
212
309
|
|
310
|
+
# Process stream chunks
|
213
311
|
debug_log("Beginning to process stream chunks")
|
214
312
|
try:
|
215
313
|
async for chunk in stream_generator:
|
314
|
+
# Check for task cancellation
|
216
315
|
if asyncio.current_task().cancelled():
|
217
316
|
debug_log("Task cancellation detected during chunk processing")
|
218
317
|
logger.info("Task cancellation detected during chunk processing")
|
@@ -221,30 +320,32 @@ async def generate_streaming_response(
|
|
221
320
|
await client.cancel_stream()
|
222
321
|
raise asyncio.CancelledError()
|
223
322
|
|
224
|
-
|
323
|
+
# Handle Ollama model loading state changes - only for Ollama clients
|
324
|
+
if is_ollama and hasattr(client, 'is_loading_model'):
|
225
325
|
try:
|
226
326
|
model_loading = client.is_loading_model()
|
227
|
-
debug_log(f"
|
327
|
+
debug_log(f"Ollama model loading state: {model_loading}")
|
228
328
|
if hasattr(app, 'query_one'):
|
229
329
|
try:
|
230
330
|
loading = app.query_one("#loading-indicator")
|
231
331
|
if model_loading and hasattr(loading, 'has_class') and not loading.has_class("model-loading"):
|
232
|
-
debug_log("
|
233
|
-
logger.info("
|
332
|
+
debug_log("Ollama model loading started during streaming")
|
333
|
+
logger.info("Ollama model loading started during streaming")
|
234
334
|
loading.add_class("model-loading")
|
235
335
|
loading.update("⚙️ Loading Ollama model...")
|
236
336
|
elif not model_loading and hasattr(loading, 'has_class') and loading.has_class("model-loading"):
|
237
|
-
debug_log("
|
238
|
-
logger.info("
|
337
|
+
debug_log("Ollama model loading finished during streaming")
|
338
|
+
logger.info("Ollama model loading finished during streaming")
|
239
339
|
loading.remove_class("model-loading")
|
240
340
|
loading.update("▪▪▪ Generating response...")
|
241
341
|
except Exception as ui_e:
|
242
342
|
debug_log(f"Error updating UI elements: {str(ui_e)}")
|
243
343
|
logger.error(f"Error updating UI elements: {str(ui_e)}")
|
244
344
|
except Exception as e:
|
245
|
-
debug_log(f"Error checking model loading state: {str(e)}")
|
246
|
-
logger.error(f"Error checking model loading state: {str(e)}")
|
345
|
+
debug_log(f"Error checking Ollama model loading state: {str(e)}")
|
346
|
+
logger.error(f"Error checking Ollama model loading state: {str(e)}")
|
247
347
|
|
348
|
+
# Process chunk content
|
248
349
|
if chunk:
|
249
350
|
if not isinstance(chunk, str):
|
250
351
|
debug_log(f"WARNING: Received non-string chunk of type: {type(chunk).__name__}")
|
@@ -259,7 +360,8 @@ async def generate_streaming_response(
|
|
259
360
|
buffer.append(chunk)
|
260
361
|
current_time = time.time()
|
261
362
|
|
262
|
-
#
|
363
|
+
# Update UI with new content
|
364
|
+
# Always update immediately for the first few chunks for better responsiveness
|
263
365
|
if (current_time - last_update >= update_interval or
|
264
366
|
len(''.join(buffer)) > 5 or # Reduced buffer size threshold
|
265
367
|
len(full_response) < 50): # More aggressive updates for early content
|
@@ -268,25 +370,26 @@ async def generate_streaming_response(
|
|
268
370
|
full_response += new_content
|
269
371
|
debug_log(f"Updating UI with content length: {len(full_response)}")
|
270
372
|
|
271
|
-
#
|
272
|
-
print(f"
|
373
|
+
# Enhanced debug logging
|
374
|
+
print(f"STREAM DEBUG: +{len(new_content)} chars, total: {len(full_response)}")
|
375
|
+
# Print first few characters of content for debugging
|
376
|
+
if len(full_response) < 100:
|
377
|
+
print(f"STREAM CONTENT: '{full_response}'")
|
273
378
|
|
274
379
|
try:
|
275
380
|
# Call the UI callback with the full response so far
|
381
|
+
debug_log("Calling UI callback with content")
|
276
382
|
await callback(full_response)
|
277
383
|
debug_log("UI callback completed successfully")
|
278
384
|
|
279
385
|
# Force app refresh after each update
|
280
386
|
if hasattr(app, 'refresh'):
|
387
|
+
debug_log("Forcing app refresh")
|
281
388
|
app.refresh(layout=True) # Force layout refresh
|
282
389
|
except Exception as callback_err:
|
283
390
|
debug_log(f"Error in UI callback: {str(callback_err)}")
|
284
391
|
logger.error(f"Error in UI callback: {str(callback_err)}")
|
285
|
-
print(f"Error updating UI: {str(callback_err)}")
|
286
|
-
except Exception as callback_err:
|
287
|
-
debug_log(f"Error in UI callback: {str(callback_err)}")
|
288
|
-
logger.error(f"Error in UI callback: {str(callback_err)}")
|
289
|
-
print(f"Error updating UI: {str(callback_err)}")
|
392
|
+
print(f"STREAM ERROR: Error updating UI: {str(callback_err)}")
|
290
393
|
|
291
394
|
buffer = []
|
292
395
|
last_update = current_time
|
@@ -442,8 +545,8 @@ def resolve_model_id(model_id_or_name: str) -> str:
|
|
442
545
|
"""
|
443
546
|
Resolves a potentially short model ID or display name to the full model ID
|
444
547
|
stored in the configuration. Tries multiple matching strategies.
|
445
|
-
|
446
|
-
|
548
|
+
|
549
|
+
This function is critical for ensuring models are correctly identified by provider.
|
447
550
|
"""
|
448
551
|
if not model_id_or_name:
|
449
552
|
logger.warning("resolve_model_id called with empty input, returning empty string.")
|
@@ -451,6 +554,16 @@ def resolve_model_id(model_id_or_name: str) -> str:
|
|
451
554
|
|
452
555
|
input_lower = model_id_or_name.lower().strip()
|
453
556
|
logger.info(f"Attempting to resolve model identifier: '{input_lower}'")
|
557
|
+
|
558
|
+
# First, check if this is an OpenAI model - if so, return as-is to ensure correct provider
|
559
|
+
if any(name in input_lower for name in ["gpt", "text-", "davinci"]):
|
560
|
+
logger.info(f"Input '{input_lower}' appears to be an OpenAI model, returning as-is")
|
561
|
+
return model_id_or_name
|
562
|
+
|
563
|
+
# Next, check if this is an Anthropic model - if so, return as-is to ensure correct provider
|
564
|
+
if any(name in input_lower for name in ["claude", "anthropic"]):
|
565
|
+
logger.info(f"Input '{input_lower}' appears to be an Anthropic model, returning as-is")
|
566
|
+
return model_id_or_name
|
454
567
|
|
455
568
|
available_models = CONFIG.get("available_models", {})
|
456
569
|
if not available_models:
|
@@ -461,20 +574,22 @@ def resolve_model_id(model_id_or_name: str) -> str:
|
|
461
574
|
provider = None
|
462
575
|
if input_lower in available_models:
|
463
576
|
provider = available_models[input_lower].get("provider")
|
577
|
+
logger.info(f"Found model in available_models with provider: {provider}")
|
464
578
|
else:
|
465
579
|
# Try to find by display name
|
466
580
|
for model_info in available_models.values():
|
467
581
|
if model_info.get("display_name", "").lower() == input_lower:
|
468
582
|
provider = model_info.get("provider")
|
583
|
+
logger.info(f"Found model by display name with provider: {provider}")
|
469
584
|
break
|
470
585
|
|
471
586
|
# Special case for Ollama models with version format (model:version)
|
472
|
-
if provider == "ollama" and ":" in input_lower and not input_lower.startswith("claude-"):
|
587
|
+
if (provider == "ollama" or any(name in input_lower for name in ["llama", "mistral", "codellama", "gemma"])) and ":" in input_lower and not input_lower.startswith("claude-"):
|
473
588
|
logger.info(f"Input '{input_lower}' appears to be an Ollama model with version, returning as-is")
|
474
589
|
return model_id_or_name
|
475
590
|
|
476
591
|
# Only apply dot-to-colon for Ollama models
|
477
|
-
if provider == "ollama" and "." in input_lower and not input_lower.startswith("claude-"):
|
592
|
+
if (provider == "ollama" or any(name in input_lower for name in ["llama", "mistral", "codellama", "gemma"])) and "." in input_lower and not input_lower.startswith("claude-"):
|
478
593
|
logger.info(f"Input '{input_lower}' appears to be an Ollama model with dot notation")
|
479
594
|
if ":" not in input_lower:
|
480
595
|
parts = input_lower.split(".")
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: chat-console
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.8
|
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=GakIrISWzWywKWxN4zYsAXUif2gEnmhI_aZfkWRzDJI,130
|
2
2
|
app/config.py,sha256=KawltE7cK2bR9wbe1NSlepwWIjkiFw2bg3vbLmUnP38,7626
|
3
3
|
app/database.py,sha256=nt8CVuDpy6zw8mOYqDcfUmNw611t7Ln7pz22M0b6-MI,9967
|
4
|
-
app/main.py,sha256=
|
4
|
+
app/main.py,sha256=KEkM7wMG7gQ4jFTRNWTTm7HQL5av6fVHFzg-uFyroZw,74654
|
5
5
|
app/models.py,sha256=4-y9Lytay2exWPFi0FDlVeRL3K2-I7E-jBqNzTfokqY,2644
|
6
|
-
app/utils.py,sha256=
|
6
|
+
app/utils.py,sha256=u4Og-N9EKHuhI81PHlPQkdpetIR1zX3UBTZ5XssvowI,34659
|
7
7
|
app/api/__init__.py,sha256=A8UL84ldYlv8l7O-yKzraVFcfww86SgWfpl4p7R03-w,62
|
8
8
|
app/api/anthropic.py,sha256=UpIP3CgAOUimdVyif41MhBOCAgOyFO8mX9SFQMKRAmc,12483
|
9
|
-
app/api/base.py,sha256=
|
9
|
+
app/api/base.py,sha256=eShCiZIcW3yeZLONt1xnkP0vU6v5MEaDj3YZ3xcPle8,7294
|
10
10
|
app/api/ollama.py,sha256=EBEEKXbgAYWEg_zF5PO_UKO5l_aoU3J_7tfCj9e-fqs,61699
|
11
11
|
app/api/openai.py,sha256=6ORruzuuZtIjME3WK-g7kXf7cBmM4td5Njv9JLaWh7E,9557
|
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=0TNtzl_11cAVfh_V8OO-nRbQh0615rOc1N7ri39xxkQ,18428
|
14
14
|
app/ui/chat_list.py,sha256=WQTYVNSSXlx_gQal3YqILZZKL9UiTjmNMIDX2I9pAMM,11205
|
15
15
|
app/ui/model_browser.py,sha256=pdblLVkdyVF0_Bo02bqbErGAtieyH-y6IfhMOPEqIso,71124
|
16
16
|
app/ui/model_selector.py,sha256=ue3rbZfjVsjli-rJN5mfSqq23Ci7NshmTb4xWS-uG5k,18685
|
17
17
|
app/ui/search.py,sha256=b-m14kG3ovqW1-i0qDQ8KnAqFJbi5b1FLM9dOnbTyIs,9763
|
18
18
|
app/ui/styles.py,sha256=04AhPuLrOd2yenfRySFRestPeuTPeMLzhmMB67NdGvw,5615
|
19
|
-
chat_console-0.3.
|
20
|
-
chat_console-0.3.
|
21
|
-
chat_console-0.3.
|
22
|
-
chat_console-0.3.
|
23
|
-
chat_console-0.3.
|
24
|
-
chat_console-0.3.
|
19
|
+
chat_console-0.3.8.dist-info/licenses/LICENSE,sha256=srHZ3fvcAuZY1LHxE7P6XWju2njRCHyK6h_ftEbzxSE,1057
|
20
|
+
chat_console-0.3.8.dist-info/METADATA,sha256=1JGTapio5cA1J_3I9-ujTOcUiFcl29GRy73konykJvE,2921
|
21
|
+
chat_console-0.3.8.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
|
22
|
+
chat_console-0.3.8.dist-info/entry_points.txt,sha256=kkVdEc22U9PAi2AeruoKklfkng_a_aHAP6VRVwrAD7c,67
|
23
|
+
chat_console-0.3.8.dist-info/top_level.txt,sha256=io9g7LCbfmTG1SFKgEOGXmCFB9uMP2H5lerm0HiHWQE,4
|
24
|
+
chat_console-0.3.8.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|