chat-console 0.3.5__py3-none-any.whl → 0.3.7__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 +31 -58
- app/ui/chat_interface.py +1 -15
- app/utils.py +152 -92
- {chat_console-0.3.5.dist-info → chat_console-0.3.7.dist-info}/METADATA +1 -1
- {chat_console-0.3.5.dist-info → chat_console-0.3.7.dist-info}/RECORD +11 -11
- {chat_console-0.3.5.dist-info → chat_console-0.3.7.dist-info}/WHEEL +1 -1
- {chat_console-0.3.5.dist-info → chat_console-0.3.7.dist-info}/entry_points.txt +0 -0
- {chat_console-0.3.5.dist-info → chat_console-0.3.7.dist-info}/licenses/LICENSE +0 -0
- {chat_console-0.3.5.dist-info → chat_console-0.3.7.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 = []
|
@@ -930,70 +944,29 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
|
|
930
944
|
if not self.is_generating:
|
931
945
|
debug_log("update_ui called but is_generating is False, returning.")
|
932
946
|
return
|
933
|
-
|
934
|
-
# Make last_refresh_time accessible in inner scope
|
935
|
-
nonlocal last_refresh_time
|
936
947
|
|
937
948
|
async with update_lock:
|
938
949
|
try:
|
939
950
|
# Clear thinking indicator on first content
|
940
951
|
if assistant_message.content == "Thinking...":
|
941
952
|
debug_log("First content received, clearing 'Thinking...'")
|
953
|
+
print("First content received, clearing 'Thinking...'")
|
942
954
|
assistant_message.content = ""
|
943
955
|
|
944
956
|
# Update the message object with the full content
|
945
957
|
assistant_message.content = content
|
946
958
|
|
947
|
-
# Update UI with the content
|
948
|
-
# This is a critical change that ensures content is immediately visible
|
959
|
+
# Update UI with the content
|
949
960
|
await message_display.update_content(content)
|
950
961
|
|
951
|
-
#
|
952
|
-
# This ensures we don't need a second Enter press to see content
|
962
|
+
# Simple refresh approach - just force a layout refresh
|
953
963
|
self.refresh(layout=True)
|
954
|
-
|
955
|
-
# Always scroll after each update to ensure visibility
|
956
964
|
messages_container.scroll_end(animate=False)
|
957
965
|
|
958
|
-
# For longer responses, we can throttle the heavy refreshes
|
959
|
-
# to reduce visual jitter, but still do light refreshes for every update
|
960
|
-
content_length = len(content)
|
961
|
-
|
962
|
-
# Define key refresh points that require more thorough updates
|
963
|
-
new_paragraph = content.endswith("\n") and content.count("\n") > 0
|
964
|
-
code_block = "```" in content
|
965
|
-
needs_thorough_refresh = (
|
966
|
-
content_length < 30 or # Very aggressive for short responses
|
967
|
-
content_length % 16 == 0 or # More frequent periodic updates
|
968
|
-
new_paragraph or # Refresh on paragraph breaks
|
969
|
-
code_block # Refresh when code blocks are detected
|
970
|
-
)
|
971
|
-
|
972
|
-
# Check if it's been enough time since last heavy refresh
|
973
|
-
# Reduced from 200ms to 100ms for more responsive UI
|
974
|
-
current_time = time.time()
|
975
|
-
time_since_refresh = current_time - last_refresh_time
|
976
|
-
|
977
|
-
if needs_thorough_refresh and time_since_refresh > 0.1:
|
978
|
-
# Store the time we did the heavy refresh
|
979
|
-
last_refresh_time = current_time
|
980
|
-
|
981
|
-
# Ensure content is visible with an aggressive, guaranteed update sequence
|
982
|
-
# 1. Scroll to ensure visibility
|
983
|
-
messages_container.scroll_end(animate=False)
|
984
|
-
|
985
|
-
# 2. Force a comprehensive refresh with layout recalculation
|
986
|
-
self.refresh(layout=True)
|
987
|
-
|
988
|
-
# 3. Small delay for rendering
|
989
|
-
await asyncio.sleep(0.01)
|
990
|
-
|
991
|
-
# 4. Another scroll to account for any layout changes
|
992
|
-
messages_container.scroll_end(animate=False)
|
993
|
-
|
994
966
|
except Exception as e:
|
995
967
|
debug_log(f"Error updating UI: {str(e)}")
|
996
968
|
log.error(f"Error updating UI: {str(e)}")
|
969
|
+
print(f"Error updating UI: {str(e)}")
|
997
970
|
|
998
971
|
# --- Remove the inner run_generation_worker function ---
|
999
972
|
|
app/ui/chat_interface.py
CHANGED
@@ -132,9 +132,6 @@ class MessageDisplay(Static): # Inherit from Static instead of RichLog
|
|
132
132
|
# This avoids text reflowing as new tokens arrive
|
133
133
|
formatted_content = self._format_content(content)
|
134
134
|
|
135
|
-
# Print debug info to console
|
136
|
-
print(f"MessageDisplay.update_content: Updating with {len(content)} chars")
|
137
|
-
|
138
135
|
# Use a direct update that forces refresh - critical fix for streaming
|
139
136
|
# This ensures content is immediately visible
|
140
137
|
self.update(formatted_content, refresh=True)
|
@@ -143,11 +140,7 @@ class MessageDisplay(Static): # Inherit from Static instead of RichLog
|
|
143
140
|
try:
|
144
141
|
# Always force app refresh for every update
|
145
142
|
if self.app:
|
146
|
-
#
|
147
|
-
self.app.refresh(layout=False)
|
148
|
-
|
149
|
-
# Then do a full layout refresh to ensure content is visible
|
150
|
-
await asyncio.sleep(0.01)
|
143
|
+
# Force a full layout refresh to ensure content is visible
|
151
144
|
self.app.refresh(layout=True)
|
152
145
|
|
153
146
|
# Find the messages container and scroll to end
|
@@ -155,17 +148,10 @@ class MessageDisplay(Static): # Inherit from Static instead of RichLog
|
|
155
148
|
for container in containers:
|
156
149
|
if hasattr(container, 'scroll_end'):
|
157
150
|
container.scroll_end(animate=False)
|
158
|
-
|
159
|
-
# Force another refresh after scrolling
|
160
|
-
await asyncio.sleep(0.01)
|
161
|
-
self.app.refresh(layout=True)
|
162
151
|
except Exception as e:
|
163
152
|
# Log the error and fallback to local refresh
|
164
153
|
print(f"Error refreshing app: {str(e)}")
|
165
154
|
self.refresh(layout=True)
|
166
|
-
|
167
|
-
# Small delay to allow UI to update
|
168
|
-
await asyncio.sleep(0.03) # Increased delay for better rendering
|
169
155
|
|
170
156
|
def _format_content(self, content: str) -> str:
|
171
157
|
"""Format message content with timestamp and handle markdown links"""
|
app/utils.py
CHANGED
@@ -20,33 +20,63 @@ 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 and not title_model_id:
|
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
|
+
|
72
|
+
# Fallback logic if no specific model was found
|
43
73
|
if not title_model_id:
|
44
|
-
# Use the originally passed model
|
74
|
+
# Use the originally passed model as the final fallback
|
45
75
|
title_model_id = model
|
46
|
-
|
47
|
-
|
48
|
-
|
76
|
+
debug_log(f"Falling back to originally selected model for title generation: {title_model_id}")
|
77
|
+
|
49
78
|
logger.info(f"Generating title for conversation using model: {title_model_id}")
|
79
|
+
debug_log(f"Final model selected for title generation: {title_model_id}")
|
50
80
|
|
51
81
|
# Create a special prompt for title generation
|
52
82
|
title_prompt = [
|
@@ -65,36 +95,44 @@ async def generate_conversation_title(message: str, model: str, client: Any) ->
|
|
65
95
|
|
66
96
|
while tries > 0:
|
67
97
|
try:
|
68
|
-
|
69
|
-
#
|
70
|
-
# Adjust the method call based on the actual client implementation
|
98
|
+
debug_log(f"Attempt {3-tries} to generate title")
|
99
|
+
# First try generate_completion if available
|
71
100
|
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
|
-
|
101
|
+
debug_log("Using generate_completion method")
|
102
|
+
try:
|
103
|
+
title = await client.generate_completion(
|
104
|
+
messages=title_prompt,
|
105
|
+
model=title_model_id,
|
106
|
+
temperature=0.7,
|
107
|
+
max_tokens=60 # Titles should be short
|
108
|
+
)
|
109
|
+
debug_log(f"Title generated successfully: {title}")
|
110
|
+
except Exception as completion_error:
|
111
|
+
debug_log(f"Error in generate_completion: {str(completion_error)}")
|
112
|
+
raise # Re-raise to be caught by outer try/except
|
113
|
+
# Fall back to generate_stream if completion not available
|
114
|
+
elif hasattr(client, 'generate_stream'):
|
115
|
+
debug_log("Using generate_stream method")
|
116
|
+
title_chunks = []
|
117
|
+
try:
|
118
|
+
async for chunk in client.generate_stream(title_prompt, title_model_id, style=""):
|
119
|
+
if chunk is not None:
|
120
|
+
title_chunks.append(chunk)
|
121
|
+
debug_log(f"Received chunk of length: {len(chunk)}")
|
122
|
+
|
123
|
+
title = "".join(title_chunks)
|
124
|
+
debug_log(f"Combined title from chunks: {title}")
|
125
|
+
|
126
|
+
# If we didn't get any content, use a default
|
127
|
+
if not title.strip():
|
128
|
+
debug_log("Empty title received, using default")
|
129
|
+
title = f"Conversation ({datetime.now().strftime('%Y-%m-%d %H:%M')})"
|
130
|
+
except Exception as stream_error:
|
131
|
+
debug_log(f"Error during title stream processing: {str(stream_error)}")
|
132
|
+
raise # Re-raise to be caught by outer try/except
|
96
133
|
else:
|
97
|
-
|
134
|
+
debug_log("Client does not support any title generation method")
|
135
|
+
raise NotImplementedError("Client does not support a suitable method for title generation.")
|
98
136
|
|
99
137
|
# Sanitize and limit the title
|
100
138
|
title = title.strip().strip('"\'').strip()
|
@@ -102,20 +140,23 @@ async def generate_conversation_title(message: str, model: str, client: Any) ->
|
|
102
140
|
title = title[:37] + "..."
|
103
141
|
|
104
142
|
logger.info(f"Generated title: {title}")
|
105
|
-
|
143
|
+
debug_log(f"Final sanitized title: {title}")
|
144
|
+
return title # Return successful title
|
106
145
|
|
107
146
|
except Exception as e:
|
108
147
|
last_error = str(e)
|
109
|
-
|
148
|
+
debug_log(f"Error generating title (tries left: {tries-1}): {last_error}")
|
149
|
+
logger.error(f"Error generating title (tries left: {tries-1}): {last_error}")
|
110
150
|
tries -= 1
|
111
|
-
if tries > 0:
|
151
|
+
if tries > 0: # Only sleep if there are more retries
|
112
152
|
await asyncio.sleep(1) # Small delay before retry
|
113
153
|
|
114
|
-
# If all retries fail, log the
|
154
|
+
# If all retries fail, log the error and return a default title
|
155
|
+
debug_log(f"Failed to generate title after multiple retries. Using default title.")
|
115
156
|
logger.error(f"Failed to generate title after multiple retries. Last error: {last_error}")
|
116
157
|
return f"Conversation ({datetime.now().strftime('%Y-%m-%d %H:%M')})"
|
117
158
|
|
118
|
-
#
|
159
|
+
# Worker function for streaming response generation
|
119
160
|
async def generate_streaming_response(
|
120
161
|
app: 'SimpleChatApp',
|
121
162
|
messages: List[Dict],
|
@@ -136,10 +177,12 @@ async def generate_streaming_response(
|
|
136
177
|
logger.info(f"Starting streaming response with model: {model}")
|
137
178
|
debug_log(f"Starting streaming response with model: '{model}', client type: {type(client).__name__}")
|
138
179
|
|
180
|
+
# Validate messages
|
139
181
|
if not messages:
|
140
182
|
debug_log("Error: messages list is empty")
|
141
183
|
raise ValueError("Messages list cannot be empty")
|
142
184
|
|
185
|
+
# Ensure all messages have required fields
|
143
186
|
for i, msg in enumerate(messages):
|
144
187
|
try:
|
145
188
|
debug_log(f"Message {i}: role={msg.get('role', 'missing')}, content_len={len(msg.get('content', ''))}")
|
@@ -157,14 +200,14 @@ async def generate_streaming_response(
|
|
157
200
|
}
|
158
201
|
debug_log(f"Repaired message {i}")
|
159
202
|
|
160
|
-
|
161
|
-
|
203
|
+
# Initialize variables for response tracking
|
162
204
|
full_response = ""
|
163
205
|
buffer = []
|
164
206
|
last_update = time.time()
|
165
207
|
update_interval = 0.05 # Reduced interval for more frequent updates
|
166
208
|
|
167
209
|
try:
|
210
|
+
# Validate client
|
168
211
|
if client is None:
|
169
212
|
debug_log("Error: client is None, cannot proceed with streaming")
|
170
213
|
raise ValueError("Model client is None, cannot proceed with streaming")
|
@@ -173,9 +216,15 @@ async def generate_streaming_response(
|
|
173
216
|
debug_log(f"Error: client {type(client).__name__} does not have generate_stream method")
|
174
217
|
raise ValueError(f"Client {type(client).__name__} does not support streaming")
|
175
218
|
|
219
|
+
# Determine client type
|
176
220
|
is_ollama = 'ollama' in str(type(client)).lower()
|
177
|
-
|
221
|
+
is_openai = 'openai' in str(type(client)).lower()
|
222
|
+
is_anthropic = 'anthropic' in str(type(client)).lower()
|
223
|
+
|
224
|
+
debug_log(f"Client types - Ollama: {is_ollama}, OpenAI: {is_openai}, Anthropic: {is_anthropic}")
|
178
225
|
|
226
|
+
# Only show loading indicator for Ollama (which may need to load models)
|
227
|
+
# This prevents Ollama-specific UI elements from showing when using other providers
|
179
228
|
if is_ollama and hasattr(app, 'query_one'):
|
180
229
|
try:
|
181
230
|
debug_log("Showing initial model loading indicator for Ollama")
|
@@ -190,6 +239,7 @@ async def generate_streaming_response(
|
|
190
239
|
debug_log(f"Starting stream generation with messages length: {len(messages)}")
|
191
240
|
logger.info(f"Starting stream generation for model: {model}")
|
192
241
|
|
242
|
+
# Initialize stream generator
|
193
243
|
try:
|
194
244
|
debug_log("Calling client.generate_stream()")
|
195
245
|
stream_generator = client.generate_stream(messages, model, style)
|
@@ -199,10 +249,12 @@ async def generate_streaming_response(
|
|
199
249
|
logger.error(f"Error initializing stream generator: {str(stream_init_error)}")
|
200
250
|
raise
|
201
251
|
|
202
|
-
|
252
|
+
# Update UI if model is ready (Ollama specific)
|
253
|
+
# Only check is_loading_model for Ollama clients to prevent errors with other providers
|
254
|
+
if is_ollama and hasattr(client, 'is_loading_model') and not client.is_loading_model() and hasattr(app, 'query_one'):
|
203
255
|
try:
|
204
|
-
debug_log("
|
205
|
-
logger.info("
|
256
|
+
debug_log("Ollama model is ready for generation, updating UI")
|
257
|
+
logger.info("Ollama model is ready for generation, updating UI")
|
206
258
|
loading = app.query_one("#loading-indicator")
|
207
259
|
loading.remove_class("model-loading")
|
208
260
|
loading.update("▪▪▪ Generating response...")
|
@@ -210,9 +262,11 @@ async def generate_streaming_response(
|
|
210
262
|
debug_log(f"Error updating UI after stream init: {str(e)}")
|
211
263
|
logger.error(f"Error updating UI after stream init: {str(e)}")
|
212
264
|
|
265
|
+
# Process stream chunks
|
213
266
|
debug_log("Beginning to process stream chunks")
|
214
267
|
try:
|
215
268
|
async for chunk in stream_generator:
|
269
|
+
# Check for task cancellation
|
216
270
|
if asyncio.current_task().cancelled():
|
217
271
|
debug_log("Task cancellation detected during chunk processing")
|
218
272
|
logger.info("Task cancellation detected during chunk processing")
|
@@ -221,30 +275,32 @@ async def generate_streaming_response(
|
|
221
275
|
await client.cancel_stream()
|
222
276
|
raise asyncio.CancelledError()
|
223
277
|
|
224
|
-
|
278
|
+
# Handle Ollama model loading state changes - only for Ollama clients
|
279
|
+
if is_ollama and hasattr(client, 'is_loading_model'):
|
225
280
|
try:
|
226
281
|
model_loading = client.is_loading_model()
|
227
|
-
debug_log(f"
|
282
|
+
debug_log(f"Ollama model loading state: {model_loading}")
|
228
283
|
if hasattr(app, 'query_one'):
|
229
284
|
try:
|
230
285
|
loading = app.query_one("#loading-indicator")
|
231
286
|
if model_loading and hasattr(loading, 'has_class') and not loading.has_class("model-loading"):
|
232
|
-
debug_log("
|
233
|
-
logger.info("
|
287
|
+
debug_log("Ollama model loading started during streaming")
|
288
|
+
logger.info("Ollama model loading started during streaming")
|
234
289
|
loading.add_class("model-loading")
|
235
290
|
loading.update("⚙️ Loading Ollama model...")
|
236
291
|
elif not model_loading and hasattr(loading, 'has_class') and loading.has_class("model-loading"):
|
237
|
-
debug_log("
|
238
|
-
logger.info("
|
292
|
+
debug_log("Ollama model loading finished during streaming")
|
293
|
+
logger.info("Ollama model loading finished during streaming")
|
239
294
|
loading.remove_class("model-loading")
|
240
295
|
loading.update("▪▪▪ Generating response...")
|
241
296
|
except Exception as ui_e:
|
242
297
|
debug_log(f"Error updating UI elements: {str(ui_e)}")
|
243
298
|
logger.error(f"Error updating UI elements: {str(ui_e)}")
|
244
299
|
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)}")
|
300
|
+
debug_log(f"Error checking Ollama model loading state: {str(e)}")
|
301
|
+
logger.error(f"Error checking Ollama model loading state: {str(e)}")
|
247
302
|
|
303
|
+
# Process chunk content
|
248
304
|
if chunk:
|
249
305
|
if not isinstance(chunk, str):
|
250
306
|
debug_log(f"WARNING: Received non-string chunk of type: {type(chunk).__name__}")
|
@@ -259,7 +315,8 @@ async def generate_streaming_response(
|
|
259
315
|
buffer.append(chunk)
|
260
316
|
current_time = time.time()
|
261
317
|
|
262
|
-
#
|
318
|
+
# Update UI with new content
|
319
|
+
# Always update immediately for the first few chunks for better responsiveness
|
263
320
|
if (current_time - last_update >= update_interval or
|
264
321
|
len(''.join(buffer)) > 5 or # Reduced buffer size threshold
|
265
322
|
len(full_response) < 50): # More aggressive updates for early content
|
@@ -268,34 +325,25 @@ async def generate_streaming_response(
|
|
268
325
|
full_response += new_content
|
269
326
|
debug_log(f"Updating UI with content length: {len(full_response)}")
|
270
327
|
|
271
|
-
#
|
272
|
-
|
328
|
+
# Only print to console for debugging if not OpenAI
|
329
|
+
# This prevents Ollama debug output from appearing in OpenAI responses
|
330
|
+
if not is_openai:
|
331
|
+
print(f"Streaming update: +{len(new_content)} chars, total: {len(full_response)}")
|
273
332
|
|
274
333
|
try:
|
275
334
|
# Call the UI callback with the full response so far
|
276
335
|
await callback(full_response)
|
277
336
|
debug_log("UI callback completed successfully")
|
278
|
-
print("UI callback completed successfully")
|
279
337
|
|
280
|
-
# Force app refresh after each update
|
338
|
+
# Force app refresh after each update
|
281
339
|
if hasattr(app, 'refresh'):
|
282
|
-
#
|
283
|
-
app.refresh(layout=False)
|
284
|
-
# Then do a full layout refresh to ensure content is visible
|
285
|
-
await asyncio.sleep(0.01)
|
286
|
-
app.refresh(layout=True)
|
287
|
-
|
288
|
-
# Try to force scroll to end
|
289
|
-
try:
|
290
|
-
messages_container = app.query_one("#messages-container")
|
291
|
-
if messages_container and hasattr(messages_container, 'scroll_end'):
|
292
|
-
messages_container.scroll_end(animate=False)
|
293
|
-
except Exception as scroll_err:
|
294
|
-
debug_log(f"Error scrolling: {str(scroll_err)}")
|
340
|
+
app.refresh(layout=True) # Force layout refresh
|
295
341
|
except Exception as callback_err:
|
296
342
|
debug_log(f"Error in UI callback: {str(callback_err)}")
|
297
343
|
logger.error(f"Error in UI callback: {str(callback_err)}")
|
298
|
-
print
|
344
|
+
# Only print error to console if not OpenAI
|
345
|
+
if not is_openai:
|
346
|
+
print(f"Error updating UI: {str(callback_err)}")
|
299
347
|
|
300
348
|
buffer = []
|
301
349
|
last_update = current_time
|
@@ -451,8 +499,8 @@ def resolve_model_id(model_id_or_name: str) -> str:
|
|
451
499
|
"""
|
452
500
|
Resolves a potentially short model ID or display name to the full model ID
|
453
501
|
stored in the configuration. Tries multiple matching strategies.
|
454
|
-
|
455
|
-
|
502
|
+
|
503
|
+
This function is critical for ensuring models are correctly identified by provider.
|
456
504
|
"""
|
457
505
|
if not model_id_or_name:
|
458
506
|
logger.warning("resolve_model_id called with empty input, returning empty string.")
|
@@ -460,6 +508,16 @@ def resolve_model_id(model_id_or_name: str) -> str:
|
|
460
508
|
|
461
509
|
input_lower = model_id_or_name.lower().strip()
|
462
510
|
logger.info(f"Attempting to resolve model identifier: '{input_lower}'")
|
511
|
+
|
512
|
+
# First, check if this is an OpenAI model - if so, return as-is to ensure correct provider
|
513
|
+
if any(name in input_lower for name in ["gpt", "text-", "davinci"]):
|
514
|
+
logger.info(f"Input '{input_lower}' appears to be an OpenAI model, returning as-is")
|
515
|
+
return model_id_or_name
|
516
|
+
|
517
|
+
# Next, check if this is an Anthropic model - if so, return as-is to ensure correct provider
|
518
|
+
if any(name in input_lower for name in ["claude", "anthropic"]):
|
519
|
+
logger.info(f"Input '{input_lower}' appears to be an Anthropic model, returning as-is")
|
520
|
+
return model_id_or_name
|
463
521
|
|
464
522
|
available_models = CONFIG.get("available_models", {})
|
465
523
|
if not available_models:
|
@@ -470,20 +528,22 @@ def resolve_model_id(model_id_or_name: str) -> str:
|
|
470
528
|
provider = None
|
471
529
|
if input_lower in available_models:
|
472
530
|
provider = available_models[input_lower].get("provider")
|
531
|
+
logger.info(f"Found model in available_models with provider: {provider}")
|
473
532
|
else:
|
474
533
|
# Try to find by display name
|
475
534
|
for model_info in available_models.values():
|
476
535
|
if model_info.get("display_name", "").lower() == input_lower:
|
477
536
|
provider = model_info.get("provider")
|
537
|
+
logger.info(f"Found model by display name with provider: {provider}")
|
478
538
|
break
|
479
539
|
|
480
540
|
# Special case for Ollama models with version format (model:version)
|
481
|
-
if provider == "ollama" and ":" in input_lower and not input_lower.startswith("claude-"):
|
541
|
+
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-"):
|
482
542
|
logger.info(f"Input '{input_lower}' appears to be an Ollama model with version, returning as-is")
|
483
543
|
return model_id_or_name
|
484
544
|
|
485
545
|
# Only apply dot-to-colon for Ollama models
|
486
|
-
if provider == "ollama" and "." in input_lower and not input_lower.startswith("claude-"):
|
546
|
+
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-"):
|
487
547
|
logger.info(f"Input '{input_lower}' appears to be an Ollama model with dot notation")
|
488
548
|
if ":" not in input_lower:
|
489
549
|
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.7
|
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=ZSZR6xIuPhvv1zB4p63eSeGQX8bTkhxBWk2Gn0peFaw,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=clcRjXwySxVjrPtqvPOIfl7r8KbHVLZ1woxyEnvl3JI,72829
|
5
5
|
app/models.py,sha256=4-y9Lytay2exWPFi0FDlVeRL3K2-I7E-jBqNzTfokqY,2644
|
6
|
-
app/utils.py,sha256=
|
6
|
+
app/utils.py,sha256=htktBl1JucYEHo1WBrWkfdip4yzRtvyVl24Aaj445xA,32421
|
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=TJlMzVmrKzr3t0JIhto0vKBvyik7gJ7UEyW3Vqbn3cE,17262
|
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.7.dist-info/licenses/LICENSE,sha256=srHZ3fvcAuZY1LHxE7P6XWju2njRCHyK6h_ftEbzxSE,1057
|
20
|
+
chat_console-0.3.7.dist-info/METADATA,sha256=eDQRUghh8Ihp8z38oAlI0___RBBDJHpLmhBGF0VgZ1w,2921
|
21
|
+
chat_console-0.3.7.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
|
22
|
+
chat_console-0.3.7.dist-info/entry_points.txt,sha256=kkVdEc22U9PAi2AeruoKklfkng_a_aHAP6VRVwrAD7c,67
|
23
|
+
chat_console-0.3.7.dist-info/top_level.txt,sha256=io9g7LCbfmTG1SFKgEOGXmCFB9uMP2H5lerm0HiHWQE,4
|
24
|
+
chat_console-0.3.7.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|