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 CHANGED
@@ -3,4 +3,4 @@ Chat CLI
3
3
  A command-line interface for chatting with various LLM providers like ChatGPT and Claude.
4
4
  """
5
5
 
6
- __version__ = "0.3.6"
6
+ __version__ = "0.3.8"
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 try Ollama for known model names or if selected from Ollama UI
51
- if (any(name in model_name_lower for name in ["llama", "mistral", "codellama", "gemma"]) or
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 try Ollama for known model names or if selected from Ollama UI
96
- if (any(name in model_name_lower for name in ["llama", "mistral", "codellama", "gemma"]) or
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"Defaulting to Ollama for unknown model: {model_name}")
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
- # IMPORTANT: Save the successful model for consistency
733
- # If the title was generated with a different model than initially selected,
734
- # update the selected_model to match so the response uses the same model
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
- # Same fallback logic as in autotitling - this ensures consistency
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
- debug_log("Falling back to OpenAI gpt-3.5-turbo")
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-instant-1.2"
818
- debug_log("Falling back to Anthropic claude-instant-1.2")
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
- debug_log(f"Falling back to Ollama model: {model}")
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
- debug_log("Final fallback to llama3")
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 remains the same, called by the worker
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
- assistant_message.content = ""
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
- # Simple refresh approach - just force a layout refresh
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
- self.notify(f"Generation error: {error}", severity="error", timeout=5)
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
- error_msg = f"Error: {error}"
1025
- debug_log(f"Adding error message: {error_msg}")
1026
- self.messages.append(Message(role="assistant", content=error_msg))
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
- # Update the stored message object content first
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
- # Prefer Haiku if Anthropic is available, otherwise fallback
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
- if client and isinstance(client, anthropic.AsyncAnthropic): # Check if the passed client is Anthropic
27
- # Check if Haiku is listed in the client's available models (more robust)
28
- available_anthropic_models = client.get_available_models()
29
- haiku_id = "claude-3-haiku-20240307"
30
- if any(m["id"] == haiku_id for m in available_anthropic_models):
31
- title_model_id = haiku_id
32
- logger.info(f"Using Anthropic Haiku for title generation: {title_model_id}")
33
- else:
34
- # If Haiku not found, try Sonnet
35
- sonnet_id = "claude-3-sonnet-20240229"
36
- if any(m["id"] == sonnet_id for m in available_anthropic_models):
37
- title_model_id = sonnet_id
38
- logger.info(f"Using Anthropic Sonnet for title generation: {title_model_id}")
39
- else:
40
- logger.warning(f"Neither Haiku nor Sonnet found in Anthropic client's list. Falling back.")
41
-
42
- # Fallback logic if no specific Anthropic model was found or client is not Anthropic
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 the originally passed model (user's selected chat model) as the final fallback
45
- title_model_id = model
46
- logger.warning(f"Falling back to originally selected model for title generation: {title_model_id}")
47
- # Consider adding fallbacks to OpenAI/Ollama here if needed based on config/availability
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
- # Generate a title using the same model but with a separate request
69
- # Assuming client has a method like generate_completion or similar
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
- title = await client.generate_completion(
73
- messages=title_prompt,
74
- model=title_model_id, # Use the chosen title model
75
- temperature=0.7,
76
- max_tokens=60 # Titles should be short
77
- )
78
- elif hasattr(client, 'generate_stream'): # Fallback or alternative method?
79
- # If generate_completion isn't available, maybe adapt generate_stream?
80
- # This part needs clarification based on the client's capabilities.
81
- # For now, let's assume a hypothetical non-streaming call or adapt stream
82
- # Simplified adaptation: collect stream chunks
83
- title_chunks = []
84
- try:
85
- # Use the chosen title model here too
86
- async for chunk in client.generate_stream(title_prompt, title_model_id, style=""):
87
- if chunk is not None: # Ensure we only process non-None chunks
88
- title_chunks.append(chunk)
89
- title = "".join(title_chunks)
90
- # If we didn't get any content, use a default
91
- if not title.strip():
92
- title = f"Conversation ({datetime.now().strftime('%Y-%m-%d %H:%M')})"
93
- except Exception as stream_error:
94
- logger.error(f"Error during title stream processing: {str(stream_error)}")
95
- title = f"Conversation ({datetime.now().strftime('%Y-%m-%d %H:%M')})"
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
- raise NotImplementedError("Client does not support a suitable method for title generation.")
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
- return title # Return successful title
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
- logger.error(f"Error generating title (tries left: {tries - 1}): {last_error}")
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: # Only sleep if there are more retries
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 last error and return a default title
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
- # Make this the worker function directly
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
- import time
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
- debug_log(f"Is Ollama client: {is_ollama}")
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
- if hasattr(client, 'is_loading_model') and not client.is_loading_model() and hasattr(app, 'query_one'):
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("Model is ready for generation, updating UI")
205
- logger.info("Model is ready for generation, updating UI")
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
- if hasattr(client, 'is_loading_model'):
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"Model loading state: {model_loading}")
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("Model loading started during streaming")
233
- logger.info("Model loading started during streaming")
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("Model loading finished during streaming")
238
- logger.info("Model loading finished during streaming")
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
- # Always update immediately for the first few chunks
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
- # Print to console for debugging
272
- print(f"Streaming update: +{len(new_content)} chars, total: {len(full_response)}")
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
- Fix: Only apply dot-to-colon conversion for Ollama models, not for OpenAI/Anthropic/custom.
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.6
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=wUvsU30dqInIPNxEtkmKfV3elJ3g5-yEF367L06eu6E,130
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=aGCaQYBTgV6PRgv6ZngC-bOYAtPl8O-9V_cMOionqbk,71245
4
+ app/main.py,sha256=KEkM7wMG7gQ4jFTRNWTTm7HQL5av6fVHFzg-uFyroZw,74654
5
5
  app/models.py,sha256=4-y9Lytay2exWPFi0FDlVeRL3K2-I7E-jBqNzTfokqY,2644
6
- app/utils.py,sha256=1eiwjQwZRJIaZvUPQVUmTpyEvWUh3iiKeX-vRRgyUGs,28925
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=bqBT4jne_W6Cvj_GoWWclV4Uk95fQvt-kkYqqZFJd8M,5769
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=TJlMzVmrKzr3t0JIhto0vKBvyik7gJ7UEyW3Vqbn3cE,17262
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.6.dist-info/licenses/LICENSE,sha256=srHZ3fvcAuZY1LHxE7P6XWju2njRCHyK6h_ftEbzxSE,1057
20
- chat_console-0.3.6.dist-info/METADATA,sha256=WZawRM5bbluU90n7HsSWqcu4sJhCHZbZJg9eLp7BK_Y,2921
21
- chat_console-0.3.6.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
22
- chat_console-0.3.6.dist-info/entry_points.txt,sha256=kkVdEc22U9PAi2AeruoKklfkng_a_aHAP6VRVwrAD7c,67
23
- chat_console-0.3.6.dist-info/top_level.txt,sha256=io9g7LCbfmTG1SFKgEOGXmCFB9uMP2H5lerm0HiHWQE,4
24
- chat_console-0.3.6.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (79.0.0)
2
+ Generator: setuptools (79.0.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5