chat-console 0.3.9__py3-none-any.whl → 0.3.91__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.9"
6
+ __version__ = "0.3.91"
app/api/ollama.py CHANGED
@@ -11,6 +11,14 @@ from .base import BaseModelClient
11
11
  # Set up logging
12
12
  logger = logging.getLogger(__name__)
13
13
 
14
+ # Custom exception for Ollama API errors
15
+ class OllamaApiError(Exception):
16
+ """Exception raised for errors in the Ollama API."""
17
+ def __init__(self, message: str, status_code: Optional[int] = None):
18
+ self.message = message
19
+ self.status_code = status_code
20
+ super().__init__(self.message)
21
+
14
22
  class OllamaClient(BaseModelClient):
15
23
  def __init__(self):
16
24
  from ..config import OLLAMA_BASE_URL
@@ -280,13 +288,11 @@ class OllamaClient(BaseModelClient):
280
288
  break
281
289
 
282
290
  if not model_exists:
283
- debug_log(f"Model '{model}' not found in available models")
284
- # Instead of failing, yield a helpful error message
285
- yield f"Model '{model}' not found. Available models include: {', '.join(available_model_names[:5])}"
291
+ error_msg = f"Model '{model}' not found in available models. Available models include: {', '.join(available_model_names[:5])}"
286
292
  if len(available_model_names) > 5:
287
- yield f" and {len(available_model_names) - 5} more."
288
- yield "\n\nPlease try a different model or check your spelling."
289
- return
293
+ error_msg += f" and {len(available_model_names) - 5} more."
294
+ logger.error(error_msg)
295
+ raise OllamaApiError(error_msg)
290
296
  except Exception as e:
291
297
  debug_log(f"Error checking model availability: {str(e)}")
292
298
  # Continue anyway, the main request will handle errors
@@ -329,10 +335,11 @@ class OllamaClient(BaseModelClient):
329
335
  if response.status == 404:
330
336
  error_text = await response.text()
331
337
  debug_log(f"404 error details: {error_text}")
332
- # This is likely a model not found error
333
- yield f"Error: Model '{model}' not found on the Ollama server."
334
- yield "\nPlease check if the model name is correct or try pulling it first."
335
- return
338
+ error_msg = f"Error: Model '{model}' not found on the Ollama server. Please check if the model name is correct or try pulling it first."
339
+ logger.error(error_msg)
340
+ # Instead of raising, yield the error message for user display
341
+ yield error_msg
342
+ return # End the generation
336
343
 
337
344
  raise aiohttp.ClientError("Model not ready")
338
345
  except (aiohttp.ClientError, asyncio.TimeoutError) as e:
@@ -367,9 +374,9 @@ class OllamaClient(BaseModelClient):
367
374
  error_text = await pull_response.text()
368
375
  debug_log(f"404 error details: {error_text}")
369
376
  # This is likely a model not found in registry
370
- yield f"Error: Model '{model}' not found in the Ollama registry."
371
- yield "\nPlease check if the model name is correct or try a different model."
372
- return
377
+ error_msg = f"Error: Model '{model}' not found in the Ollama registry. Please check if the model name is correct or try a different model."
378
+ logger.error(error_msg)
379
+ raise OllamaApiError(error_msg, status_code=404)
373
380
 
374
381
  raise Exception("Failed to pull model")
375
382
  logger.info("Model pulled successfully")
app/config.py CHANGED
@@ -175,35 +175,38 @@ CONFIG = load_config()
175
175
 
176
176
  # --- Dynamically update Anthropic models after initial load ---
177
177
  def update_anthropic_models(config):
178
- """Fetch models from Anthropic API and update the config dict."""
178
+ """Update the config with Anthropic models."""
179
179
  if AVAILABLE_PROVIDERS["anthropic"]:
180
180
  try:
181
- from app.api.anthropic import AnthropicClient # Import here to avoid circular dependency at top level
182
- client = AnthropicClient()
183
- fetched_models = client.get_available_models() # This now fetches (or uses fallback)
184
-
185
- if fetched_models:
186
- # Remove old hardcoded anthropic models first
187
- models_to_remove = [
188
- model_id for model_id, info in config["available_models"].items()
189
- if info.get("provider") == "anthropic"
190
- ]
191
- for model_id in models_to_remove:
192
- del config["available_models"][model_id]
193
-
194
- # Add fetched models
195
- for model in fetched_models:
196
- config["available_models"][model["id"]] = {
197
- "provider": "anthropic",
198
- "max_tokens": 4096, # Assign a default max_tokens
199
- "display_name": model["name"]
200
- }
201
- print(f"Updated Anthropic models in config: {[m['id'] for m in fetched_models]}") # Add print statement
202
- else:
203
- print("Could not fetch or find Anthropic models to update config.") # Add print statement
204
-
181
+ # Instead of calling an async method, use a hardcoded fallback list
182
+ # that matches what's in the AnthropicClient class
183
+ fallback_models = [
184
+ {"id": "claude-3-opus-20240229", "name": "Claude 3 Opus"},
185
+ {"id": "claude-3-sonnet-20240229", "name": "Claude 3 Sonnet"},
186
+ {"id": "claude-3-haiku-20240307", "name": "Claude 3 Haiku"},
187
+ {"id": "claude-3-5-sonnet-20240620", "name": "Claude 3.5 Sonnet"},
188
+ {"id": "claude-3-7-sonnet-20250219", "name": "Claude 3.7 Sonnet"},
189
+ ]
190
+
191
+ # Remove old models first
192
+ models_to_remove = [
193
+ model_id for model_id, info in config["available_models"].items()
194
+ if info.get("provider") == "anthropic"
195
+ ]
196
+ for model_id in models_to_remove:
197
+ del config["available_models"][model_id]
198
+
199
+ # Add the fallback models
200
+ for model in fallback_models:
201
+ config["available_models"][model["id"]] = {
202
+ "provider": "anthropic",
203
+ "max_tokens": 4096,
204
+ "display_name": model["name"]
205
+ }
206
+ print(f"Updated Anthropic models in config with fallback list")
207
+
205
208
  except Exception as e:
206
- print(f"Error updating Anthropic models in config: {e}") # Add print statement
209
+ print(f"Error updating Anthropic models in config: {e}")
207
210
  # Keep existing config if update fails
208
211
 
209
212
  return config
app/main.py CHANGED
@@ -20,10 +20,10 @@ file_handler = logging.FileHandler(debug_log_file)
20
20
  file_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
21
21
 
22
22
  # Get the logger and add the handler
23
- debug_logger = logging.getLogger("chat-cli-debug")
23
+ debug_logger = logging.getLogger() # Root logger
24
24
  debug_logger.setLevel(logging.DEBUG)
25
25
  debug_logger.addHandler(file_handler)
26
- # Prevent propagation to the root logger (which would print to console)
26
+ # CRITICAL: Force all output to the file, not stdout
27
27
  debug_logger.propagate = False
28
28
 
29
29
  # Add a convenience function to log to this file
@@ -1010,11 +1010,15 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
1010
1010
  self._loading_animation_task.cancel()
1011
1011
  self._loading_animation_task = None
1012
1012
  try:
1013
+ # Explicitly hide loading indicator
1013
1014
  loading = self.query_one("#loading-indicator")
1014
1015
  loading.add_class("hidden")
1016
+ loading.remove_class("model-loading") # Also remove model-loading class if present
1017
+ self.refresh(layout=True) # Force a refresh to ensure UI updates
1015
1018
  self.query_one("#message-input").focus()
1016
- except Exception:
1017
- pass # Ignore UI errors during cleanup
1019
+ except Exception as ui_err:
1020
+ debug_log(f"Error hiding loading indicator: {str(ui_err)}")
1021
+ log.error(f"Error hiding loading indicator: {str(ui_err)}")
1018
1022
 
1019
1023
  # Rename this method slightly to avoid potential conflicts and clarify purpose
1020
1024
  async def _handle_generation_result(self, worker: Worker[Optional[str]]) -> None:
@@ -1043,6 +1047,15 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
1043
1047
  debug_log(f"Error in generation worker: {error}")
1044
1048
  log.error(f"Error in generation worker: {error}")
1045
1049
 
1050
+ # Explicitly hide loading indicator
1051
+ try:
1052
+ loading = self.query_one("#loading-indicator")
1053
+ loading.add_class("hidden")
1054
+ loading.remove_class("model-loading") # Also remove model-loading class if present
1055
+ except Exception as ui_err:
1056
+ debug_log(f"Error hiding loading indicator: {str(ui_err)}")
1057
+ log.error(f"Error hiding loading indicator: {str(ui_err)}")
1058
+
1046
1059
  # Sanitize error message for UI display
1047
1060
  error_str = str(error)
1048
1061
 
@@ -1069,6 +1082,9 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
1069
1082
  debug_log(f"Adding error message: {user_error}")
1070
1083
  self.messages.append(Message(role="assistant", content=user_error))
1071
1084
  await self.update_messages_ui()
1085
+
1086
+ # Force a refresh to ensure UI updates
1087
+ self.refresh(layout=True)
1072
1088
 
1073
1089
  elif worker.state == "success":
1074
1090
  full_response = worker.result
app/ui/chat_interface.py CHANGED
@@ -120,78 +120,61 @@ class MessageDisplay(Static): # Inherit from Static instead of RichLog
120
120
  self.update(self._format_content(self.message.content))
121
121
 
122
122
  async def update_content(self, content: str) -> None:
123
- """Update the message content using Static.update() with optimizations for streaming"""
124
- # Use proper logging instead of print statements
123
+ """Update the message content."""
125
124
  import logging
126
125
  logger = logging.getLogger(__name__)
127
126
  logger.debug(f"MessageDisplay.update_content called with content length: {len(content)}")
128
127
 
129
- # Quick unchanged content check to avoid unnecessary updates
130
- if self.message.content == content:
131
- logger.debug("Content unchanged, skipping update")
132
- return
133
-
134
- # Special handling for "Thinking..." to ensure it gets replaced
135
- if self.message.content == "Thinking..." and content:
136
- logger.debug("Replacing 'Thinking...' with actual content")
137
- # Force a complete replacement rather than an append
138
- self.message.content = ""
139
-
140
- # Update the stored message object content
141
- self.message.content = content
142
-
143
- # Format with fixed-width placeholder to minimize layout shifts
144
- formatted_content = self._format_content(content)
145
-
146
- # Use a direct update that forces refresh - critical fix for streaming
147
- self.update(formatted_content, refresh=True)
148
-
149
- # Force app-level refresh and scroll to ensure visibility
150
- try:
151
- if self.app:
152
- # Force a full layout refresh to ensure content is visible
153
- self.app.refresh(layout=True)
154
-
155
- # Find the messages container and scroll to end
156
- containers = self.app.query("ScrollableContainer")
157
- for container in containers:
158
- if hasattr(container, 'scroll_end'):
159
- container.scroll_end(animate=False)
160
- except Exception as e:
161
- logger.error(f"Error refreshing app: {str(e)}")
162
- self.refresh(layout=True)
163
-
164
- # Return early to avoid duplicate updates
165
- return
166
-
167
- # Update the stored message object content
168
- self.message.content = content
169
-
170
- # Format with fixed-width placeholder to minimize layout shifts
171
- # This avoids text reflowing as new tokens arrive
172
- formatted_content = self._format_content(content)
173
-
174
- # Use a direct update that forces refresh - critical fix for streaming
175
- # This ensures content is immediately visible
176
- logger.debug(f"Updating widget with formatted content length: {len(formatted_content)}")
177
- self.update(formatted_content, refresh=True)
178
-
179
- # Force app-level refresh and scroll to ensure visibility
180
- try:
181
- # Always force app refresh for every update
182
- if self.app:
183
- # Force a full layout refresh to ensure content is visible
184
- self.app.refresh(layout=True)
128
+ # Use a lock to prevent race conditions during updates
129
+ if not hasattr(self, '_update_lock'):
130
+ self._update_lock = asyncio.Lock()
131
+
132
+ async with self._update_lock:
133
+ # For initial update from "Thinking..."
134
+ if self.message.content == "Thinking..." and content:
135
+ logger.debug("Replacing 'Thinking...' with initial content")
136
+ self.message.content = content # Update the stored content
137
+ formatted = self._format_content(content)
138
+ self.update(formatted, refresh=True)
185
139
 
186
- # Find the messages container and scroll to end
187
- containers = self.app.query("ScrollableContainer")
188
- for container in containers:
189
- if hasattr(container, 'scroll_end'):
190
- container.scroll_end(animate=False)
191
- except Exception as e:
192
- # Log the error and fallback to local refresh
193
- logger.error(f"Error refreshing app: {str(e)}")
194
- self.refresh(layout=True)
140
+ # Force a clean layout update
141
+ try:
142
+ if self.app:
143
+ self.app.refresh(layout=True)
144
+ await asyncio.sleep(0.05) # Small delay for layout to update
145
+
146
+ # Find container and scroll
147
+ messages_container = self.app.query_one("#messages-container")
148
+ if messages_container:
149
+ messages_container.scroll_end(animate=False)
150
+ except Exception as e:
151
+ logger.error(f"Error in initial UI update: {str(e)}")
152
+ return
153
+
154
+ # Quick unchanged content check to avoid unnecessary updates
155
+ if self.message.content == content:
156
+ logger.debug("Content unchanged, skipping update")
157
+ return
158
+
159
+ # For subsequent updates
160
+ if self.message.content != content:
161
+ self.message.content = content
162
+ formatted = self._format_content(content)
163
+ self.update(formatted, refresh=True)
164
+
165
+ # Use a more targeted refresh approach
166
+ try:
167
+ if self.app:
168
+ self.app.refresh(layout=False) # Lightweight refresh first
169
+ # Find container and scroll
170
+ messages_container = self.app.query_one("#messages-container")
171
+ if messages_container:
172
+ messages_container.scroll_end(animate=False)
173
+
174
+ # Final full refresh only at end
175
+ self.app.refresh(layout=True)
176
+ except Exception as e:
177
+ logger.error(f"Error refreshing UI: {str(e)}")
195
178
 
196
179
  def _format_content(self, content: str) -> str:
197
180
  """Format message content with timestamp and handle markdown links"""
app/utils.py CHANGED
@@ -201,284 +201,589 @@ async def generate_conversation_title(message: str, model: str, client: Any) ->
201
201
  logger.error(f"Failed to generate title after multiple retries. Last error: {last_error}")
202
202
  return f"Conversation ({datetime.now().strftime('%Y-%m-%d %H:%M')})"
203
203
 
204
- # Worker function for streaming response generation
205
- async def generate_streaming_response(
204
+ # Helper function for OpenAI streaming
205
+ async def _generate_openai_stream(
206
206
  app: 'SimpleChatApp',
207
207
  messages: List[Dict],
208
208
  model: str,
209
209
  style: str,
210
210
  client: Any,
211
- callback: Callable[[str], Awaitable[None]]
211
+ callback: Callable[[str], Awaitable[None]],
212
+ update_lock: asyncio.Lock
212
213
  ) -> Optional[str]:
213
- """
214
- Generate a streaming response from the model (as a Textual worker).
215
- Refactored to be a coroutine, not an async generator.
216
- """
214
+ """Generate streaming response using OpenAI provider."""
217
215
  try:
218
216
  from app.main import debug_log
219
217
  except ImportError:
220
218
  debug_log = lambda msg: None
221
-
222
- logger.info(f"Starting streaming response with model: {model}")
223
- debug_log(f"Starting streaming response with model: '{model}', client type: {type(client).__name__}")
224
-
225
- # Validate messages
226
- if not messages:
227
- debug_log("Error: messages list is empty")
228
- raise ValueError("Messages list cannot be empty")
229
-
230
- # Ensure all messages have required fields
231
- for i, msg in enumerate(messages):
219
+
220
+ debug_log(f"Using OpenAI-specific streaming for model: {model}")
221
+
222
+ # Initialize variables for response tracking
223
+ full_response = ""
224
+ buffer = []
225
+ last_update = time.time()
226
+ update_interval = 0.03 # Responsive updates for OpenAI
227
+
228
+ try:
229
+ # Initialize stream generator
230
+ debug_log("Initializing OpenAI stream generator")
231
+ stream_generator = client.generate_stream(messages, model, style)
232
+
233
+ # Process stream chunks
234
+ debug_log("Beginning to process OpenAI stream chunks")
235
+ async for chunk in stream_generator:
236
+ # Check for task cancellation
237
+ if asyncio.current_task().cancelled():
238
+ debug_log("Task cancellation detected during OpenAI chunk processing")
239
+ if hasattr(client, 'cancel_stream'):
240
+ await client.cancel_stream()
241
+ raise asyncio.CancelledError()
242
+
243
+ # Process chunk content
244
+ if chunk:
245
+ if not isinstance(chunk, str):
246
+ try:
247
+ chunk = str(chunk)
248
+ except Exception:
249
+ continue
250
+
251
+ buffer.append(chunk)
252
+ current_time = time.time()
253
+
254
+ # Update UI with new content
255
+ if (current_time - last_update >= update_interval or
256
+ len(''.join(buffer)) > 5 or
257
+ len(full_response) < 50):
258
+
259
+ new_content = ''.join(buffer)
260
+ full_response += new_content
261
+
262
+ try:
263
+ async with update_lock:
264
+ await callback(full_response)
265
+ if hasattr(app, 'refresh'):
266
+ app.refresh(layout=True)
267
+ except Exception as callback_err:
268
+ logger.error(f"Error in OpenAI UI callback: {str(callback_err)}")
269
+
270
+ buffer = []
271
+ last_update = current_time
272
+ await asyncio.sleep(0.02)
273
+
274
+ # Process any remaining buffer content
275
+ if buffer:
276
+ new_content = ''.join(buffer)
277
+ full_response += new_content
278
+
279
+ try:
280
+ async with update_lock:
281
+ await callback(full_response)
282
+ if hasattr(app, 'refresh'):
283
+ app.refresh(layout=True)
284
+ await asyncio.sleep(0.02)
285
+ try:
286
+ messages_container = app.query_one("#messages-container")
287
+ if messages_container:
288
+ messages_container.scroll_end(animate=False)
289
+ except Exception:
290
+ pass
291
+ except Exception as callback_err:
292
+ logger.error(f"Error in final OpenAI UI callback: {str(callback_err)}")
293
+
294
+ # Final refresh to ensure everything is displayed correctly
232
295
  try:
233
- debug_log(f"Message {i}: role={msg.get('role', 'missing')}, content_len={len(msg.get('content', ''))}")
234
- if 'role' not in msg:
235
- debug_log(f"Adding missing 'role' to message {i}")
236
- msg['role'] = 'user'
237
- if 'content' not in msg:
238
- debug_log(f"Adding missing 'content' to message {i}")
239
- msg['content'] = ''
240
- except Exception as e:
241
- debug_log(f"Error checking message {i}: {str(e)}")
242
- messages[i] = {
243
- 'role': 'user',
244
- 'content': str(msg) if msg else ''
245
- }
246
- debug_log(f"Repaired message {i}")
296
+ await asyncio.sleep(0.05)
297
+ async with update_lock:
298
+ await callback(full_response)
299
+ if hasattr(app, 'refresh'):
300
+ app.refresh(layout=True)
301
+ except Exception:
302
+ pass
303
+
304
+ return full_response
305
+
306
+ except asyncio.CancelledError:
307
+ logger.info(f"OpenAI streaming cancelled. Partial response length: {len(full_response)}")
308
+ if hasattr(client, 'cancel_stream'):
309
+ await client.cancel_stream()
310
+ return full_response
311
+
312
+ except Exception as e:
313
+ logger.error(f"Error during OpenAI streaming: {str(e)}")
314
+ if hasattr(client, 'cancel_stream'):
315
+ await client.cancel_stream()
316
+ raise
247
317
 
318
+ # Helper function for Anthropic streaming
319
+ async def _generate_anthropic_stream(
320
+ app: 'SimpleChatApp',
321
+ messages: List[Dict],
322
+ model: str,
323
+ style: str,
324
+ client: Any,
325
+ callback: Callable[[str], Awaitable[None]],
326
+ update_lock: asyncio.Lock
327
+ ) -> Optional[str]:
328
+ """Generate streaming response using Anthropic provider."""
329
+ try:
330
+ from app.main import debug_log
331
+ except ImportError:
332
+ debug_log = lambda msg: None
333
+
334
+ debug_log(f"Using Anthropic-specific streaming for model: {model}")
335
+
248
336
  # Initialize variables for response tracking
249
337
  full_response = ""
250
338
  buffer = []
251
339
  last_update = time.time()
252
- update_interval = 0.05 # Reduced interval for more frequent updates
253
-
340
+ update_interval = 0.03 # Responsive updates for Anthropic
341
+
254
342
  try:
255
- # Validate client
256
- if client is None:
257
- debug_log("Error: client is None, cannot proceed with streaming")
258
- raise ValueError("Model client is None, cannot proceed with streaming")
259
-
260
- if not hasattr(client, 'generate_stream'):
261
- debug_log(f"Error: client {type(client).__name__} does not have generate_stream method")
262
- raise ValueError(f"Client {type(client).__name__} does not support streaming")
263
-
264
- # Determine client type
265
- is_ollama = 'ollama' in str(type(client)).lower()
266
- is_openai = 'openai' in str(type(client)).lower()
267
- is_anthropic = 'anthropic' in str(type(client)).lower()
343
+ # Initialize stream generator
344
+ debug_log("Initializing Anthropic stream generator")
345
+ stream_generator = client.generate_stream(messages, model, style)
346
+
347
+ # Process stream chunks
348
+ debug_log("Beginning to process Anthropic stream chunks")
349
+ async for chunk in stream_generator:
350
+ # Check for task cancellation
351
+ if asyncio.current_task().cancelled():
352
+ debug_log("Task cancellation detected during Anthropic chunk processing")
353
+ if hasattr(client, 'cancel_stream'):
354
+ await client.cancel_stream()
355
+ raise asyncio.CancelledError()
356
+
357
+ # Process chunk content
358
+ if chunk:
359
+ if not isinstance(chunk, str):
360
+ try:
361
+ chunk = str(chunk)
362
+ except Exception:
363
+ continue
364
+
365
+ buffer.append(chunk)
366
+ current_time = time.time()
367
+
368
+ # Update UI with new content
369
+ if (current_time - last_update >= update_interval or
370
+ len(''.join(buffer)) > 5 or
371
+ len(full_response) < 50):
372
+
373
+ new_content = ''.join(buffer)
374
+ full_response += new_content
375
+
376
+ try:
377
+ async with update_lock:
378
+ await callback(full_response)
379
+ if hasattr(app, 'refresh'):
380
+ app.refresh(layout=True)
381
+ except Exception as callback_err:
382
+ logger.error(f"Error in Anthropic UI callback: {str(callback_err)}")
383
+
384
+ buffer = []
385
+ last_update = current_time
386
+ await asyncio.sleep(0.02)
387
+
388
+ # Process any remaining buffer content
389
+ if buffer:
390
+ new_content = ''.join(buffer)
391
+ full_response += new_content
392
+
393
+ try:
394
+ async with update_lock:
395
+ await callback(full_response)
396
+ if hasattr(app, 'refresh'):
397
+ app.refresh(layout=True)
398
+ await asyncio.sleep(0.02)
399
+ try:
400
+ messages_container = app.query_one("#messages-container")
401
+ if messages_container:
402
+ messages_container.scroll_end(animate=False)
403
+ except Exception:
404
+ pass
405
+ except Exception as callback_err:
406
+ logger.error(f"Error in final Anthropic UI callback: {str(callback_err)}")
407
+
408
+ # Final refresh to ensure everything is displayed correctly
409
+ try:
410
+ await asyncio.sleep(0.05)
411
+ async with update_lock:
412
+ await callback(full_response)
413
+ if hasattr(app, 'refresh'):
414
+ app.refresh(layout=True)
415
+ except Exception:
416
+ pass
417
+
418
+ return full_response
419
+
420
+ except asyncio.CancelledError:
421
+ logger.info(f"Anthropic streaming cancelled. Partial response length: {len(full_response)}")
422
+ if hasattr(client, 'cancel_stream'):
423
+ await client.cancel_stream()
424
+ return full_response
268
425
 
269
- debug_log(f"Client types - Ollama: {is_ollama}, OpenAI: {is_openai}, Anthropic: {is_anthropic}")
426
+ except Exception as e:
427
+ logger.error(f"Error during Anthropic streaming: {str(e)}")
428
+ if hasattr(client, 'cancel_stream'):
429
+ await client.cancel_stream()
430
+ raise
270
431
 
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
273
- if is_ollama and hasattr(app, 'query_one'):
432
+ # Helper function for Ollama streaming
433
+ async def _generate_ollama_stream(
434
+ app: 'SimpleChatApp',
435
+ messages: List[Dict],
436
+ model: str,
437
+ style: str,
438
+ client: Any,
439
+ callback: Callable[[str], Awaitable[None]],
440
+ update_lock: asyncio.Lock
441
+ ) -> Optional[str]:
442
+ """Generate streaming response using Ollama provider."""
443
+ try:
444
+ from app.main import debug_log
445
+ except ImportError:
446
+ debug_log = lambda msg: None
447
+
448
+ debug_log(f"Using Ollama-specific streaming for model: {model}")
449
+
450
+ # Initialize variables for response tracking
451
+ full_response = ""
452
+ buffer = []
453
+ last_update = time.time()
454
+ update_interval = 0.03 # Responsive updates for Ollama
455
+
456
+ try:
457
+ # Show loading indicator for Ollama (which may need to load models)
458
+ if hasattr(app, 'query_one'):
274
459
  try:
275
460
  debug_log("Showing initial model loading indicator for Ollama")
276
- logger.info("Showing initial model loading indicator for Ollama")
277
461
  loading = app.query_one("#loading-indicator")
278
462
  loading.add_class("model-loading")
279
463
  loading.update("⚙️ Loading Ollama model...")
280
464
  except Exception as e:
281
465
  debug_log(f"Error setting initial Ollama loading state: {str(e)}")
282
- logger.error(f"Error setting initial Ollama loading state: {str(e)}")
283
-
284
- debug_log(f"Starting stream generation with messages length: {len(messages)}")
285
- logger.info(f"Starting stream generation for model: {model}")
286
-
466
+
287
467
  # Initialize stream generator
288
- try:
289
- debug_log("Calling client.generate_stream()")
290
- stream_generator = client.generate_stream(messages, model, style)
291
- debug_log("Successfully obtained stream generator")
292
- except Exception as stream_init_error:
293
- debug_log(f"Error initializing stream generator: {str(stream_init_error)}")
294
- logger.error(f"Error initializing stream generator: {str(stream_init_error)}")
295
- raise
296
-
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'):
468
+ debug_log("Initializing Ollama stream generator")
469
+ stream_generator = client.generate_stream(messages, model, style)
470
+
471
+ # Update UI if model is ready
472
+ if hasattr(client, 'is_loading_model') and not client.is_loading_model() and hasattr(app, 'query_one'):
300
473
  try:
301
474
  debug_log("Ollama model is ready for generation, updating UI")
302
- logger.info("Ollama model is ready for generation, updating UI")
303
475
  loading = app.query_one("#loading-indicator")
304
476
  loading.remove_class("model-loading")
305
477
  loading.update("▪▪▪ Generating response...")
306
478
  except Exception as e:
307
- debug_log(f"Error updating UI after stream init: {str(e)}")
308
- logger.error(f"Error updating UI after stream init: {str(e)}")
309
-
479
+ debug_log(f"Error updating UI after Ollama stream init: {str(e)}")
480
+
310
481
  # Process stream chunks
311
- debug_log("Beginning to process stream chunks")
312
- try:
313
- async for chunk in stream_generator:
314
- # Check for task cancellation
315
- if asyncio.current_task().cancelled():
316
- debug_log("Task cancellation detected during chunk processing")
317
- logger.info("Task cancellation detected during chunk processing")
318
- if hasattr(client, 'cancel_stream'):
319
- debug_log("Calling client.cancel_stream() due to task cancellation")
320
- await client.cancel_stream()
321
- raise asyncio.CancelledError()
322
-
323
- # Handle Ollama model loading state changes - only for Ollama clients
324
- if is_ollama and hasattr(client, 'is_loading_model'):
325
- try:
326
- model_loading = client.is_loading_model()
327
- debug_log(f"Ollama model loading state: {model_loading}")
328
- if hasattr(app, 'query_one'):
329
- try:
330
- loading = app.query_one("#loading-indicator")
331
- if model_loading and hasattr(loading, 'has_class') and not loading.has_class("model-loading"):
332
- debug_log("Ollama model loading started during streaming")
333
- logger.info("Ollama model loading started during streaming")
334
- loading.add_class("model-loading")
335
- loading.update("⚙️ Loading Ollama model...")
336
- elif not model_loading and hasattr(loading, 'has_class') and loading.has_class("model-loading"):
337
- debug_log("Ollama model loading finished during streaming")
338
- logger.info("Ollama model loading finished during streaming")
339
- loading.remove_class("model-loading")
340
- loading.update("▪▪▪ Generating response...")
341
- except Exception as ui_e:
342
- debug_log(f"Error updating UI elements: {str(ui_e)}")
343
- logger.error(f"Error updating UI elements: {str(ui_e)}")
344
- except Exception as 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)}")
347
-
348
- # Process chunk content
349
- if chunk:
350
- if not isinstance(chunk, str):
351
- debug_log(f"WARNING: Received non-string chunk of type: {type(chunk).__name__}")
482
+ debug_log("Beginning to process Ollama stream chunks")
483
+ async for chunk in stream_generator:
484
+ # Check for task cancellation
485
+ if asyncio.current_task().cancelled():
486
+ debug_log("Task cancellation detected during Ollama chunk processing")
487
+ if hasattr(client, 'cancel_stream'):
488
+ await client.cancel_stream()
489
+ raise asyncio.CancelledError()
490
+
491
+ # Handle Ollama model loading state changes
492
+ if hasattr(client, 'is_loading_model'):
493
+ try:
494
+ model_loading = client.is_loading_model()
495
+ if hasattr(app, 'query_one'):
352
496
  try:
353
- chunk = str(chunk)
354
- debug_log(f"Successfully converted chunk to string, length: {len(chunk)}")
355
- except Exception as e:
356
- debug_log(f"Error converting chunk to string: {str(e)}")
357
- continue
358
-
359
- debug_log(f"Received chunk of length: {len(chunk)}")
360
- buffer.append(chunk)
361
- current_time = time.time()
362
-
363
- # Update UI with new content
364
- # Always update immediately for the first few chunks for better responsiveness
365
- if (current_time - last_update >= update_interval or
366
- len(''.join(buffer)) > 5 or # Reduced buffer size threshold
367
- len(full_response) < 50): # More aggressive updates for early content
368
-
369
- new_content = ''.join(buffer)
370
- full_response += new_content
371
- debug_log(f"Updating UI with content length: {len(full_response)}")
372
-
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}'")
497
+ loading = app.query_one("#loading-indicator")
498
+ if model_loading and hasattr(loading, 'has_class') and not loading.has_class("model-loading"):
499
+ debug_log("Ollama model loading started during streaming")
500
+ loading.add_class("model-loading")
501
+ loading.update("⚙️ Loading Ollama model...")
502
+ elif not model_loading and hasattr(loading, 'has_class') and loading.has_class("model-loading"):
503
+ debug_log("Ollama model loading finished during streaming")
504
+ loading.remove_class("model-loading")
505
+ loading.update("▪▪▪ Generating response...")
506
+ except Exception:
507
+ pass
508
+ except Exception:
509
+ pass
510
+
511
+ # Process chunk content
512
+ if chunk:
513
+ if not isinstance(chunk, str):
514
+ try:
515
+ chunk = str(chunk)
516
+ except Exception:
517
+ continue
378
518
 
379
- try:
380
- # Call the UI callback with the full response so far
381
- debug_log("Calling UI callback with content")
519
+ buffer.append(chunk)
520
+ current_time = time.time()
521
+
522
+ # Update UI with new content
523
+ if (current_time - last_update >= update_interval or
524
+ len(''.join(buffer)) > 5 or
525
+ len(full_response) < 50):
526
+
527
+ new_content = ''.join(buffer)
528
+ full_response += new_content
529
+
530
+ try:
531
+ async with update_lock:
382
532
  await callback(full_response)
383
- debug_log("UI callback completed successfully")
384
-
385
- # Force app refresh after each update
386
533
  if hasattr(app, 'refresh'):
387
- debug_log("Forcing app refresh")
388
- app.refresh(layout=True) # Force layout refresh
389
- except Exception as callback_err:
390
- debug_log(f"Error in UI callback: {str(callback_err)}")
391
- logger.error(f"Error in UI callback: {str(callback_err)}")
392
- print(f"STREAM ERROR: Error updating UI: {str(callback_err)}")
393
-
394
- buffer = []
395
- last_update = current_time
534
+ app.refresh(layout=True)
535
+ except Exception as callback_err:
536
+ logger.error(f"Error in Ollama UI callback: {str(callback_err)}")
396
537
 
397
- # Shorter sleep between updates for more responsive streaming
398
- await asyncio.sleep(0.02)
399
- except asyncio.CancelledError:
400
- debug_log("CancelledError in stream processing")
401
- raise
402
- except Exception as chunk_error:
403
- debug_log(f"Error processing stream chunks: {str(chunk_error)}")
404
- logger.error(f"Error processing stream chunks: {str(chunk_error)}")
405
- raise
406
-
538
+ buffer = []
539
+ last_update = current_time
540
+ await asyncio.sleep(0.02)
541
+
542
+ # Process any remaining buffer content
407
543
  if buffer:
408
544
  new_content = ''.join(buffer)
409
545
  full_response += new_content
410
- debug_log(f"Sending final content, total length: {len(full_response)}")
546
+
411
547
  try:
412
- await callback(full_response)
413
- debug_log("Final UI callback completed successfully")
414
-
415
- debug_log("Forcing final UI refresh sequence for all models")
416
- try:
548
+ async with update_lock:
549
+ await callback(full_response)
417
550
  if hasattr(app, 'refresh'):
418
- app.refresh(layout=False)
551
+ app.refresh(layout=True)
419
552
  await asyncio.sleep(0.02)
420
553
  try:
421
554
  messages_container = app.query_one("#messages-container")
422
- if messages_container and hasattr(messages_container, 'scroll_end'):
555
+ if messages_container:
423
556
  messages_container.scroll_end(animate=False)
424
557
  except Exception:
425
558
  pass
559
+ except Exception as callback_err:
560
+ logger.error(f"Error in final Ollama UI callback: {str(callback_err)}")
561
+
562
+ # Final refresh to ensure everything is displayed correctly
563
+ try:
564
+ await asyncio.sleep(0.05)
565
+ async with update_lock:
566
+ await callback(full_response)
567
+ if hasattr(app, 'refresh'):
568
+ app.refresh(layout=True)
569
+ except Exception:
570
+ pass
571
+
572
+ return full_response
573
+
574
+ except asyncio.CancelledError:
575
+ logger.info(f"Ollama streaming cancelled. Partial response length: {len(full_response)}")
576
+ if hasattr(client, 'cancel_stream'):
577
+ await client.cancel_stream()
578
+ return full_response
579
+
580
+ except Exception as e:
581
+ logger.error(f"Error during Ollama streaming: {str(e)}")
582
+ if hasattr(client, 'cancel_stream'):
583
+ await client.cancel_stream()
584
+ raise
585
+
586
+ # Generic fallback streaming implementation
587
+ async def _generate_generic_stream(
588
+ app: 'SimpleChatApp',
589
+ messages: List[Dict],
590
+ model: str,
591
+ style: str,
592
+ client: Any,
593
+ callback: Callable[[str], Awaitable[None]],
594
+ update_lock: asyncio.Lock
595
+ ) -> Optional[str]:
596
+ """Generic fallback implementation for streaming responses."""
597
+ try:
598
+ from app.main import debug_log
599
+ except ImportError:
600
+ debug_log = lambda msg: None
601
+
602
+ debug_log(f"Using generic streaming for model: {model}, client type: {type(client).__name__}")
603
+
604
+ # Initialize variables for response tracking
605
+ full_response = ""
606
+ buffer = []
607
+ last_update = time.time()
608
+ update_interval = 0.03 # Responsive updates
609
+
610
+ try:
611
+ # Initialize stream generator
612
+ debug_log("Initializing generic stream generator")
613
+ stream_generator = client.generate_stream(messages, model, style)
614
+
615
+ # Process stream chunks
616
+ debug_log("Beginning to process generic stream chunks")
617
+ async for chunk in stream_generator:
618
+ # Check for task cancellation
619
+ if asyncio.current_task().cancelled():
620
+ debug_log("Task cancellation detected during generic chunk processing")
621
+ if hasattr(client, 'cancel_stream'):
622
+ await client.cancel_stream()
623
+ raise asyncio.CancelledError()
624
+
625
+ # Process chunk content
626
+ if chunk:
627
+ if not isinstance(chunk, str):
628
+ try:
629
+ chunk = str(chunk)
630
+ except Exception:
631
+ continue
632
+
633
+ buffer.append(chunk)
634
+ current_time = time.time()
635
+
636
+ # Update UI with new content
637
+ if (current_time - last_update >= update_interval or
638
+ len(''.join(buffer)) > 5 or
639
+ len(full_response) < 50):
640
+
641
+ new_content = ''.join(buffer)
642
+ full_response += new_content
643
+
644
+ try:
645
+ async with update_lock:
646
+ await callback(full_response)
647
+ if hasattr(app, 'refresh'):
648
+ app.refresh(layout=True)
649
+ except Exception as callback_err:
650
+ logger.error(f"Error in generic UI callback: {str(callback_err)}")
651
+
652
+ buffer = []
653
+ last_update = current_time
654
+ await asyncio.sleep(0.02)
655
+
656
+ # Process any remaining buffer content
657
+ if buffer:
658
+ new_content = ''.join(buffer)
659
+ full_response += new_content
660
+
661
+ try:
662
+ async with update_lock:
663
+ await callback(full_response)
664
+ if hasattr(app, 'refresh'):
426
665
  app.refresh(layout=True)
427
666
  await asyncio.sleep(0.02)
428
667
  try:
429
668
  messages_container = app.query_one("#messages-container")
430
- if messages_container and hasattr(messages_container, 'scroll_end'):
669
+ if messages_container:
431
670
  messages_container.scroll_end(animate=False)
432
671
  except Exception:
433
672
  pass
434
- except Exception as refresh_err:
435
- debug_log(f"Error forcing final UI refresh: {str(refresh_err)}")
436
673
  except Exception as callback_err:
437
- debug_log(f"Error in final UI callback: {str(callback_err)}")
438
- logger.error(f"Error in final UI callback: {str(callback_err)}")
439
-
674
+ logger.error(f"Error in final generic UI callback: {str(callback_err)}")
675
+
676
+ # Final refresh to ensure everything is displayed correctly
440
677
  try:
441
678
  await asyncio.sleep(0.05)
442
- debug_log("Sending one final callback to ensure UI refresh")
443
- await callback(full_response)
444
- if hasattr(app, 'refresh'):
445
- app.refresh(layout=True)
446
- except Exception as final_err:
447
- debug_log(f"Error in final extra callback: {str(final_err)}")
448
-
449
- debug_log(f"Streaming response completed successfully. Response length: {len(full_response)}")
450
- logger.info(f"Streaming response completed successfully. Response length: {len(full_response)}")
679
+ async with update_lock:
680
+ await callback(full_response)
681
+ if hasattr(app, 'refresh'):
682
+ app.refresh(layout=True)
683
+ except Exception:
684
+ pass
685
+
451
686
  return full_response
452
-
687
+
453
688
  except asyncio.CancelledError:
454
- debug_log(f"Streaming response task cancelled. Partial response length: {len(full_response)}")
455
- logger.info(f"Streaming response task cancelled. Partial response length: {len(full_response)}")
689
+ logger.info(f"Generic streaming cancelled. Partial response length: {len(full_response)}")
456
690
  if hasattr(client, 'cancel_stream'):
457
- debug_log("Calling client.cancel_stream() after cancellation")
458
- try:
459
- await client.cancel_stream()
460
- debug_log("Successfully cancelled client stream")
461
- except Exception as cancel_err:
462
- debug_log(f"Error cancelling client stream: {str(cancel_err)}")
691
+ await client.cancel_stream()
463
692
  return full_response
464
-
693
+
465
694
  except Exception as e:
466
- debug_log(f"Error during streaming response: {str(e)}")
467
- logger.error(f"Error during streaming response: {str(e)}")
695
+ logger.error(f"Error during generic streaming: {str(e)}")
468
696
  if hasattr(client, 'cancel_stream'):
469
- debug_log("Attempting to cancel client stream after error")
470
- try:
471
- await client.cancel_stream()
472
- debug_log("Successfully cancelled client stream after error")
473
- except Exception as cancel_err:
474
- debug_log(f"Error cancelling client stream after error: {str(cancel_err)}")
697
+ await client.cancel_stream()
475
698
  raise
476
699
 
477
- finally:
478
- debug_log("generate_streaming_response worker finished or errored.")
479
- if 'full_response' in locals():
480
- return full_response
481
- return None
700
+ # Worker function for streaming response generation
701
+ async def generate_streaming_response(
702
+ app: 'SimpleChatApp',
703
+ messages: List[Dict],
704
+ model: str,
705
+ style: str,
706
+ client: Any,
707
+ callback: Callable[[str], Awaitable[None]]
708
+ ) -> Optional[str]:
709
+ """
710
+ Generate a streaming response from the model (as a Textual worker).
711
+ Refactored to be a coroutine, not an async generator.
712
+ """
713
+ try:
714
+ from app.main import debug_log
715
+ except ImportError:
716
+ debug_log = lambda msg: None
717
+
718
+ logger.info(f"Starting streaming response with model: {model}")
719
+ debug_log(f"Starting streaming response with model: '{model}', client type: {type(client).__name__}")
720
+
721
+ # Validate messages
722
+ if not messages:
723
+ debug_log("Error: messages list is empty")
724
+ raise ValueError("Messages list cannot be empty")
725
+
726
+ # Ensure all messages have required fields
727
+ for i, msg in enumerate(messages):
728
+ try:
729
+ debug_log(f"Message {i}: role={msg.get('role', 'missing')}, content_len={len(msg.get('content', ''))}")
730
+ if 'role' not in msg:
731
+ debug_log(f"Adding missing 'role' to message {i}")
732
+ msg['role'] = 'user'
733
+ if 'content' not in msg:
734
+ debug_log(f"Adding missing 'content' to message {i}")
735
+ msg['content'] = ''
736
+ except Exception as e:
737
+ debug_log(f"Error checking message {i}: {str(e)}")
738
+ messages[i] = {
739
+ 'role': 'user',
740
+ 'content': str(msg) if msg else ''
741
+ }
742
+ debug_log(f"Repaired message {i}")
743
+
744
+ # Create a lock for synchronizing UI updates
745
+ update_lock = asyncio.Lock()
746
+
747
+ # Validate client
748
+ if client is None:
749
+ debug_log("Error: client is None, cannot proceed with streaming")
750
+ raise ValueError("Model client is None, cannot proceed with streaming")
751
+
752
+ if not hasattr(client, 'generate_stream'):
753
+ debug_log(f"Error: client {type(client).__name__} does not have generate_stream method")
754
+ raise ValueError(f"Client {type(client).__name__} does not support streaming")
755
+
756
+ # Explicitly check provider type first
757
+ is_ollama = 'ollama' in str(type(client)).lower()
758
+ is_openai = 'openai' in str(type(client)).lower()
759
+ is_anthropic = 'anthropic' in str(type(client)).lower()
760
+
761
+ debug_log(f"Client types - Ollama: {is_ollama}, OpenAI: {is_openai}, Anthropic: {is_anthropic}")
762
+
763
+ # Use separate implementations for each provider
764
+ try:
765
+ if is_openai:
766
+ debug_log("Using OpenAI-specific streaming implementation")
767
+ return await _generate_openai_stream(app, messages, model, style, client, callback, update_lock)
768
+ elif is_anthropic:
769
+ debug_log("Using Anthropic-specific streaming implementation")
770
+ return await _generate_anthropic_stream(app, messages, model, style, client, callback, update_lock)
771
+ elif is_ollama:
772
+ debug_log("Using Ollama-specific streaming implementation")
773
+ return await _generate_ollama_stream(app, messages, model, style, client, callback, update_lock)
774
+ else:
775
+ # Generic fallback
776
+ debug_log("Using generic streaming implementation")
777
+ return await _generate_generic_stream(app, messages, model, style, client, callback, update_lock)
778
+ except asyncio.CancelledError:
779
+ debug_log("Task cancellation detected in main streaming function")
780
+ if hasattr(client, 'cancel_stream'):
781
+ await client.cancel_stream()
782
+ raise
783
+ except Exception as e:
784
+ debug_log(f"Error in streaming implementation: {str(e)}")
785
+ logger.error(f"Error in streaming implementation: {str(e)}")
786
+ raise
482
787
 
483
788
  async def ensure_ollama_running() -> bool:
484
789
  """
@@ -555,6 +860,22 @@ def resolve_model_id(model_id_or_name: str) -> str:
555
860
  input_lower = model_id_or_name.lower().strip()
556
861
  logger.info(f"Attempting to resolve model identifier: '{input_lower}'")
557
862
 
863
+ # Add special case handling for common OpenAI models
864
+ openai_model_aliases = {
865
+ "04-mini": "gpt-4-mini", # Fix "04-mini" typo to "gpt-4-mini"
866
+ "04": "gpt-4",
867
+ "04-vision": "gpt-4-vision",
868
+ "04-turbo": "gpt-4-turbo",
869
+ "035": "gpt-3.5-turbo",
870
+ "35-turbo": "gpt-3.5-turbo",
871
+ "35": "gpt-3.5-turbo"
872
+ }
873
+
874
+ if input_lower in openai_model_aliases:
875
+ resolved = openai_model_aliases[input_lower]
876
+ logger.info(f"Resolved '{input_lower}' to '{resolved}' via OpenAI model alias")
877
+ return resolved
878
+
558
879
  # Special case handling for common typos and model name variations
559
880
  typo_corrections = {
560
881
  "o4-mini": "04-mini",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chat-console
3
- Version: 0.3.9
3
+ Version: 0.3.91
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
@@ -0,0 +1,24 @@
1
+ app/__init__.py,sha256=1Z7Qdm9b_jcT0nisliyzs6res69PiL36mZcYaHwpzvY,131
2
+ app/config.py,sha256=xeRGXcKbNvAdQGkaJJBipM4yHZJTM1y4ZFoW764APOU,7661
3
+ app/database.py,sha256=nt8CVuDpy6zw8mOYqDcfUmNw611t7Ln7pz22M0b6-MI,9967
4
+ app/main.py,sha256=fYwnYiK4FEpTCBP8QByUBaXyAPWR1h2dG7aLdtcnkzs,75602
5
+ app/models.py,sha256=4-y9Lytay2exWPFi0FDlVeRL3K2-I7E-jBqNzTfokqY,2644
6
+ app/utils.py,sha256=DsK_Zid9HG9v6cX1y8-4uS_86AgMRTrGFjVn-fctbDM,44949
7
+ app/api/__init__.py,sha256=A8UL84ldYlv8l7O-yKzraVFcfww86SgWfpl4p7R03-w,62
8
+ app/api/anthropic.py,sha256=uInwNvGLJ_iPUs4BjdwaqXTU6NfmK1SzX7498Pt44fI,10667
9
+ app/api/base.py,sha256=Oqu674v0NkrJY91tvxGd6YWgyi6XrFvi03quzWGswg8,7425
10
+ app/api/ollama.py,sha256=zQcrs3COoS4wu9amp5oSmIsBNYK_ntilcGIPOe4wafI,64649
11
+ app/api/openai.py,sha256=hLPr955tUx_2vwRuLP8Zrl3vu7kQZgUETi4cJuaYnFE,10810
12
+ app/ui/__init__.py,sha256=RndfbQ1Tv47qdSiuQzvWP96lPS547SDaGE-BgOtiP_w,55
13
+ app/ui/chat_interface.py,sha256=prJNmigK7yD-7hb-61mgG2JXcJeDgADySYSElGEUmTg,18683
14
+ app/ui/chat_list.py,sha256=WQTYVNSSXlx_gQal3YqILZZKL9UiTjmNMIDX2I9pAMM,11205
15
+ app/ui/model_browser.py,sha256=pdblLVkdyVF0_Bo02bqbErGAtieyH-y6IfhMOPEqIso,71124
16
+ app/ui/model_selector.py,sha256=ue3rbZfjVsjli-rJN5mfSqq23Ci7NshmTb4xWS-uG5k,18685
17
+ app/ui/search.py,sha256=b-m14kG3ovqW1-i0qDQ8KnAqFJbi5b1FLM9dOnbTyIs,9763
18
+ app/ui/styles.py,sha256=04AhPuLrOd2yenfRySFRestPeuTPeMLzhmMB67NdGvw,5615
19
+ chat_console-0.3.91.dist-info/licenses/LICENSE,sha256=srHZ3fvcAuZY1LHxE7P6XWju2njRCHyK6h_ftEbzxSE,1057
20
+ chat_console-0.3.91.dist-info/METADATA,sha256=7mGtC-c-bML6p0ku5YaFSpYO0rZGbF7NZfhuUUB96u0,2922
21
+ chat_console-0.3.91.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
22
+ chat_console-0.3.91.dist-info/entry_points.txt,sha256=kkVdEc22U9PAi2AeruoKklfkng_a_aHAP6VRVwrAD7c,67
23
+ chat_console-0.3.91.dist-info/top_level.txt,sha256=io9g7LCbfmTG1SFKgEOGXmCFB9uMP2H5lerm0HiHWQE,4
24
+ chat_console-0.3.91.dist-info/RECORD,,
@@ -1,24 +0,0 @@
1
- app/__init__.py,sha256=EjqUVXPPqxbEvf8FYWy5IflGPyCeiFKVc6roYG8q77k,130
2
- app/config.py,sha256=KawltE7cK2bR9wbe1NSlepwWIjkiFw2bg3vbLmUnP38,7626
3
- app/database.py,sha256=nt8CVuDpy6zw8mOYqDcfUmNw611t7Ln7pz22M0b6-MI,9967
4
- app/main.py,sha256=KEkM7wMG7gQ4jFTRNWTTm7HQL5av6fVHFzg-uFyroZw,74654
5
- app/models.py,sha256=4-y9Lytay2exWPFi0FDlVeRL3K2-I7E-jBqNzTfokqY,2644
6
- app/utils.py,sha256=6za9f3USUiYvjTiwPDP7swPamRmlwApCYPyCKc9drNY,35228
7
- app/api/__init__.py,sha256=A8UL84ldYlv8l7O-yKzraVFcfww86SgWfpl4p7R03-w,62
8
- app/api/anthropic.py,sha256=uInwNvGLJ_iPUs4BjdwaqXTU6NfmK1SzX7498Pt44fI,10667
9
- app/api/base.py,sha256=Oqu674v0NkrJY91tvxGd6YWgyi6XrFvi03quzWGswg8,7425
10
- app/api/ollama.py,sha256=uBCdfie04zdp1UGePpz7m0XuOwMB71ynz9CulnKUDHg,64284
11
- app/api/openai.py,sha256=hLPr955tUx_2vwRuLP8Zrl3vu7kQZgUETi4cJuaYnFE,10810
12
- app/ui/__init__.py,sha256=RndfbQ1Tv47qdSiuQzvWP96lPS547SDaGE-BgOtiP_w,55
13
- app/ui/chat_interface.py,sha256=xJe3LoKbXJe1XHREevkMHL9ATpRg6y0ayu2hVGWELQM,19459
14
- app/ui/chat_list.py,sha256=WQTYVNSSXlx_gQal3YqILZZKL9UiTjmNMIDX2I9pAMM,11205
15
- app/ui/model_browser.py,sha256=pdblLVkdyVF0_Bo02bqbErGAtieyH-y6IfhMOPEqIso,71124
16
- app/ui/model_selector.py,sha256=ue3rbZfjVsjli-rJN5mfSqq23Ci7NshmTb4xWS-uG5k,18685
17
- app/ui/search.py,sha256=b-m14kG3ovqW1-i0qDQ8KnAqFJbi5b1FLM9dOnbTyIs,9763
18
- app/ui/styles.py,sha256=04AhPuLrOd2yenfRySFRestPeuTPeMLzhmMB67NdGvw,5615
19
- chat_console-0.3.9.dist-info/licenses/LICENSE,sha256=srHZ3fvcAuZY1LHxE7P6XWju2njRCHyK6h_ftEbzxSE,1057
20
- chat_console-0.3.9.dist-info/METADATA,sha256=hqzrcRA8zI4qKGLdUEp7j4Y9sLSsFA6lTyhdM4f1GHY,2921
21
- chat_console-0.3.9.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
22
- chat_console-0.3.9.dist-info/entry_points.txt,sha256=kkVdEc22U9PAi2AeruoKklfkng_a_aHAP6VRVwrAD7c,67
23
- chat_console-0.3.9.dist-info/top_level.txt,sha256=io9g7LCbfmTG1SFKgEOGXmCFB9uMP2H5lerm0HiHWQE,4
24
- chat_console-0.3.9.dist-info/RECORD,,