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 +1 -1
- app/api/ollama.py +20 -13
- app/config.py +29 -26
- app/main.py +20 -4
- app/ui/chat_interface.py +50 -67
- app/utils.py +531 -210
- {chat_console-0.3.9.dist-info → chat_console-0.3.91.dist-info}/METADATA +1 -1
- chat_console-0.3.91.dist-info/RECORD +24 -0
- chat_console-0.3.9.dist-info/RECORD +0 -24
- {chat_console-0.3.9.dist-info → chat_console-0.3.91.dist-info}/WHEEL +0 -0
- {chat_console-0.3.9.dist-info → chat_console-0.3.91.dist-info}/entry_points.txt +0 -0
- {chat_console-0.3.9.dist-info → chat_console-0.3.91.dist-info}/licenses/LICENSE +0 -0
- {chat_console-0.3.9.dist-info → chat_console-0.3.91.dist-info}/top_level.txt +0 -0
app/__init__.py
CHANGED
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
|
-
|
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
|
-
|
288
|
-
|
289
|
-
|
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
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
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
|
-
|
371
|
-
|
372
|
-
|
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
|
-
"""
|
178
|
+
"""Update the config with Anthropic models."""
|
179
179
|
if AVAILABLE_PROVIDERS["anthropic"]:
|
180
180
|
try:
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
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}")
|
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(
|
23
|
+
debug_logger = logging.getLogger() # Root logger
|
24
24
|
debug_logger.setLevel(logging.DEBUG)
|
25
25
|
debug_logger.addHandler(file_handler)
|
26
|
-
#
|
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
|
-
|
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
|
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
|
-
#
|
130
|
-
if self
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
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
|
-
#
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
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
|
-
#
|
205
|
-
async def
|
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
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
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
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
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.
|
253
|
-
|
340
|
+
update_interval = 0.03 # Responsive updates for Anthropic
|
341
|
+
|
254
342
|
try:
|
255
|
-
#
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
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
|
-
|
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
|
-
|
272
|
-
|
273
|
-
|
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
|
-
|
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
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
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
|
-
|
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
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
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
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
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
|
-
|
380
|
-
|
381
|
-
|
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
|
-
|
388
|
-
|
389
|
-
|
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
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
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
|
-
|
546
|
+
|
411
547
|
try:
|
412
|
-
|
413
|
-
|
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=
|
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
|
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
|
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
|
-
|
438
|
-
|
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
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
except Exception
|
447
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
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.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|