chat-console 0.3.8__tar.gz → 0.3.91__tar.gz
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.
- {chat_console-0.3.8/chat_console.egg-info → chat_console-0.3.91}/PKG-INFO +1 -1
- {chat_console-0.3.8 → chat_console-0.3.91}/app/__init__.py +1 -1
- chat_console-0.3.91/app/api/anthropic.py +238 -0
- {chat_console-0.3.8 → chat_console-0.3.91}/app/api/base.py +5 -0
- {chat_console-0.3.8 → chat_console-0.3.91}/app/api/ollama.py +52 -0
- {chat_console-0.3.8 → chat_console-0.3.91}/app/api/openai.py +31 -0
- {chat_console-0.3.8 → chat_console-0.3.91}/app/config.py +29 -26
- {chat_console-0.3.8 → chat_console-0.3.91}/app/main.py +20 -4
- {chat_console-0.3.8 → chat_console-0.3.91}/app/ui/chat_interface.py +55 -48
- {chat_console-0.3.8 → chat_console-0.3.91}/app/utils.py +548 -210
- {chat_console-0.3.8 → chat_console-0.3.91/chat_console.egg-info}/PKG-INFO +1 -1
- chat_console-0.3.8/app/api/anthropic.py +0 -253
- {chat_console-0.3.8 → chat_console-0.3.91}/LICENSE +0 -0
- {chat_console-0.3.8 → chat_console-0.3.91}/README.md +0 -0
- {chat_console-0.3.8 → chat_console-0.3.91}/app/api/__init__.py +0 -0
- {chat_console-0.3.8 → chat_console-0.3.91}/app/database.py +0 -0
- {chat_console-0.3.8 → chat_console-0.3.91}/app/models.py +0 -0
- {chat_console-0.3.8 → chat_console-0.3.91}/app/ui/__init__.py +0 -0
- {chat_console-0.3.8 → chat_console-0.3.91}/app/ui/chat_list.py +0 -0
- {chat_console-0.3.8 → chat_console-0.3.91}/app/ui/model_browser.py +0 -0
- {chat_console-0.3.8 → chat_console-0.3.91}/app/ui/model_selector.py +0 -0
- {chat_console-0.3.8 → chat_console-0.3.91}/app/ui/search.py +0 -0
- {chat_console-0.3.8 → chat_console-0.3.91}/app/ui/styles.py +0 -0
- {chat_console-0.3.8 → chat_console-0.3.91}/chat_console.egg-info/SOURCES.txt +0 -0
- {chat_console-0.3.8 → chat_console-0.3.91}/chat_console.egg-info/dependency_links.txt +0 -0
- {chat_console-0.3.8 → chat_console-0.3.91}/chat_console.egg-info/entry_points.txt +0 -0
- {chat_console-0.3.8 → chat_console-0.3.91}/chat_console.egg-info/requires.txt +0 -0
- {chat_console-0.3.8 → chat_console-0.3.91}/chat_console.egg-info/top_level.txt +0 -0
- {chat_console-0.3.8 → chat_console-0.3.91}/setup.cfg +0 -0
- {chat_console-0.3.8 → chat_console-0.3.91}/setup.py +0 -0
@@ -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,238 @@
|
|
1
|
+
import anthropic
|
2
|
+
import asyncio
|
3
|
+
import logging
|
4
|
+
from typing import List, Dict, Any, Optional, Generator, AsyncGenerator
|
5
|
+
from .base import BaseModelClient
|
6
|
+
from ..config import ANTHROPIC_API_KEY
|
7
|
+
|
8
|
+
# Set up logging
|
9
|
+
logger = logging.getLogger(__name__)
|
10
|
+
|
11
|
+
class AnthropicClient(BaseModelClient):
|
12
|
+
def __init__(self):
|
13
|
+
self.client = None # Initialize in create()
|
14
|
+
self._active_stream = None # Track active stream for cancellation
|
15
|
+
|
16
|
+
@classmethod
|
17
|
+
async def create(cls) -> 'AnthropicClient':
|
18
|
+
"""Create a new instance with async initialization."""
|
19
|
+
instance = cls()
|
20
|
+
instance.client = anthropic.AsyncAnthropic(api_key=ANTHROPIC_API_KEY)
|
21
|
+
return instance
|
22
|
+
|
23
|
+
def _prepare_messages(self, messages: List[Dict[str, str]], style: Optional[str] = None) -> List[Dict[str, str]]:
|
24
|
+
"""Prepare messages for Anthropic API"""
|
25
|
+
processed_messages = []
|
26
|
+
|
27
|
+
# Add style instructions if provided
|
28
|
+
if style and style != "default":
|
29
|
+
style_instructions = self._get_style_instructions(style)
|
30
|
+
processed_messages.append({
|
31
|
+
"role": "system",
|
32
|
+
"content": style_instructions
|
33
|
+
})
|
34
|
+
|
35
|
+
# Add the rest of the messages
|
36
|
+
for message in messages:
|
37
|
+
# Ensure message has required fields
|
38
|
+
if "role" not in message or "content" not in message:
|
39
|
+
continue
|
40
|
+
|
41
|
+
# Map 'user' and 'assistant' roles directly
|
42
|
+
# Anthropic only supports 'user' and 'assistant' roles
|
43
|
+
if message["role"] in ["user", "assistant"]:
|
44
|
+
processed_messages.append(message)
|
45
|
+
elif message["role"] == "system":
|
46
|
+
# For system messages, we need to add them as system messages
|
47
|
+
processed_messages.append({
|
48
|
+
"role": "system",
|
49
|
+
"content": message["content"]
|
50
|
+
})
|
51
|
+
else:
|
52
|
+
# For any other role, treat as user message
|
53
|
+
processed_messages.append({
|
54
|
+
"role": "user",
|
55
|
+
"content": message["content"]
|
56
|
+
})
|
57
|
+
|
58
|
+
return processed_messages
|
59
|
+
|
60
|
+
def _get_style_instructions(self, style: str) -> str:
|
61
|
+
"""Get formatting instructions for different styles"""
|
62
|
+
styles = {
|
63
|
+
"concise": "Please provide concise, to-the-point responses without unnecessary elaboration.",
|
64
|
+
"detailed": "Please provide comprehensive responses with thorough explanations and examples.",
|
65
|
+
"technical": "Please use precise technical language and focus on accuracy and technical details.",
|
66
|
+
"friendly": "Please use a warm, conversational tone and relatable examples.",
|
67
|
+
}
|
68
|
+
|
69
|
+
return styles.get(style, "")
|
70
|
+
|
71
|
+
async def generate_completion(self, messages: List[Dict[str, str]],
|
72
|
+
model: str,
|
73
|
+
style: Optional[str] = None,
|
74
|
+
temperature: float = 0.7,
|
75
|
+
max_tokens: Optional[int] = None) -> str:
|
76
|
+
"""Generate a text completion using Anthropic"""
|
77
|
+
processed_messages = self._prepare_messages(messages, style)
|
78
|
+
|
79
|
+
try:
|
80
|
+
response = await self.client.messages.create(
|
81
|
+
model=model,
|
82
|
+
messages=processed_messages,
|
83
|
+
temperature=temperature,
|
84
|
+
max_tokens=max_tokens if max_tokens else 4096,
|
85
|
+
)
|
86
|
+
|
87
|
+
return response.content[0].text
|
88
|
+
except Exception as e:
|
89
|
+
logger.error(f"Error generating completion: {str(e)}")
|
90
|
+
raise Exception(f"Anthropic API error: {str(e)}")
|
91
|
+
|
92
|
+
async def generate_stream(self, messages: List[Dict[str, str]],
|
93
|
+
model: str,
|
94
|
+
style: Optional[str] = None,
|
95
|
+
temperature: float = 0.7,
|
96
|
+
max_tokens: Optional[int] = None) -> AsyncGenerator[str, None]:
|
97
|
+
"""Generate a streaming text completion using Anthropic"""
|
98
|
+
try:
|
99
|
+
from app.main import debug_log # Import debug logging if available
|
100
|
+
debug_log(f"Anthropic: starting streaming generation with model: {model}")
|
101
|
+
except ImportError:
|
102
|
+
# If debug_log not available, create a no-op function
|
103
|
+
debug_log = lambda msg: None
|
104
|
+
|
105
|
+
processed_messages = self._prepare_messages(messages, style)
|
106
|
+
|
107
|
+
try:
|
108
|
+
debug_log(f"Anthropic: preparing {len(processed_messages)} messages for stream")
|
109
|
+
|
110
|
+
# Use more robust error handling with retry for connection issues
|
111
|
+
max_retries = 2
|
112
|
+
retry_count = 0
|
113
|
+
|
114
|
+
while retry_count <= max_retries:
|
115
|
+
try:
|
116
|
+
debug_log(f"Anthropic: creating stream with model {model}")
|
117
|
+
|
118
|
+
# Create the stream
|
119
|
+
stream = await self.client.messages.create(
|
120
|
+
model=model,
|
121
|
+
messages=processed_messages,
|
122
|
+
temperature=temperature,
|
123
|
+
max_tokens=max_tokens if max_tokens else 4096,
|
124
|
+
stream=True
|
125
|
+
)
|
126
|
+
|
127
|
+
# Store the stream for potential cancellation
|
128
|
+
self._active_stream = stream
|
129
|
+
|
130
|
+
debug_log("Anthropic: stream created successfully")
|
131
|
+
|
132
|
+
# Process stream chunks
|
133
|
+
chunk_count = 0
|
134
|
+
debug_log("Anthropic: starting to process chunks")
|
135
|
+
|
136
|
+
async for chunk in stream:
|
137
|
+
# Check if stream has been cancelled
|
138
|
+
if self._active_stream is None:
|
139
|
+
debug_log("Anthropic: stream was cancelled, stopping generation")
|
140
|
+
break
|
141
|
+
|
142
|
+
chunk_count += 1
|
143
|
+
try:
|
144
|
+
if hasattr(chunk, 'delta') and hasattr(chunk.delta, 'text'):
|
145
|
+
content = chunk.delta.text
|
146
|
+
if content is not None:
|
147
|
+
debug_log(f"Anthropic: yielding chunk {chunk_count} of length: {len(content)}")
|
148
|
+
yield content
|
149
|
+
else:
|
150
|
+
debug_log(f"Anthropic: skipping None content chunk {chunk_count}")
|
151
|
+
else:
|
152
|
+
debug_log(f"Anthropic: skipping chunk {chunk_count} with missing content")
|
153
|
+
except Exception as chunk_error:
|
154
|
+
debug_log(f"Anthropic: error processing chunk {chunk_count}: {str(chunk_error)}")
|
155
|
+
# Skip problematic chunks but continue processing
|
156
|
+
continue
|
157
|
+
|
158
|
+
debug_log(f"Anthropic: stream completed successfully with {chunk_count} chunks")
|
159
|
+
|
160
|
+
# Clear the active stream reference when done
|
161
|
+
self._active_stream = None
|
162
|
+
|
163
|
+
# If we reach this point, we've successfully processed the stream
|
164
|
+
break
|
165
|
+
|
166
|
+
except Exception as e:
|
167
|
+
debug_log(f"Anthropic: error in attempt {retry_count+1}/{max_retries+1}: {str(e)}")
|
168
|
+
retry_count += 1
|
169
|
+
if retry_count <= max_retries:
|
170
|
+
debug_log(f"Anthropic: retrying after error (attempt {retry_count+1})")
|
171
|
+
# Simple exponential backoff
|
172
|
+
await asyncio.sleep(1 * retry_count)
|
173
|
+
else:
|
174
|
+
debug_log("Anthropic: max retries reached, raising exception")
|
175
|
+
raise Exception(f"Anthropic streaming error after {max_retries+1} attempts: {str(e)}")
|
176
|
+
|
177
|
+
except Exception as e:
|
178
|
+
debug_log(f"Anthropic: error in generate_stream: {str(e)}")
|
179
|
+
# Yield a simple error message as a last resort to ensure UI updates
|
180
|
+
yield f"Error: {str(e)}"
|
181
|
+
raise Exception(f"Anthropic streaming error: {str(e)}")
|
182
|
+
|
183
|
+
async def cancel_stream(self) -> None:
|
184
|
+
"""Cancel any active streaming request"""
|
185
|
+
logger.info("Cancelling active Anthropic stream")
|
186
|
+
try:
|
187
|
+
from app.main import debug_log
|
188
|
+
debug_log("Anthropic: cancelling active stream")
|
189
|
+
except ImportError:
|
190
|
+
pass
|
191
|
+
|
192
|
+
# Simply set the active stream to None
|
193
|
+
# This will cause the generate_stream method to stop processing chunks
|
194
|
+
self._active_stream = None
|
195
|
+
logger.info("Anthropic stream cancelled successfully")
|
196
|
+
|
197
|
+
async def get_available_models(self) -> List[Dict[str, Any]]:
|
198
|
+
"""Get list of available Anthropic models"""
|
199
|
+
# Anthropic doesn't have a models endpoint, so we return a static list
|
200
|
+
models = [
|
201
|
+
{
|
202
|
+
"id": "claude-3-opus-20240229",
|
203
|
+
"name": "Claude 3 Opus",
|
204
|
+
"description": "Most powerful model for highly complex tasks",
|
205
|
+
"context_window": 200000,
|
206
|
+
"provider": "anthropic"
|
207
|
+
},
|
208
|
+
{
|
209
|
+
"id": "claude-3-sonnet-20240229",
|
210
|
+
"name": "Claude 3 Sonnet",
|
211
|
+
"description": "Balanced model for most tasks",
|
212
|
+
"context_window": 200000,
|
213
|
+
"provider": "anthropic"
|
214
|
+
},
|
215
|
+
{
|
216
|
+
"id": "claude-3-haiku-20240307",
|
217
|
+
"name": "Claude 3 Haiku",
|
218
|
+
"description": "Fastest and most compact model",
|
219
|
+
"context_window": 200000,
|
220
|
+
"provider": "anthropic"
|
221
|
+
},
|
222
|
+
{
|
223
|
+
"id": "claude-3-5-sonnet-20240620",
|
224
|
+
"name": "Claude 3.5 Sonnet",
|
225
|
+
"description": "Latest model with improved capabilities",
|
226
|
+
"context_window": 200000,
|
227
|
+
"provider": "anthropic"
|
228
|
+
},
|
229
|
+
{
|
230
|
+
"id": "claude-3-7-sonnet-20250219",
|
231
|
+
"name": "Claude 3.7 Sonnet",
|
232
|
+
"description": "Newest model with advanced reasoning",
|
233
|
+
"context_window": 200000,
|
234
|
+
"provider": "anthropic"
|
235
|
+
}
|
236
|
+
]
|
237
|
+
|
238
|
+
return models
|
@@ -22,6 +22,11 @@ class BaseModelClient(ABC):
|
|
22
22
|
"""Generate a streaming text completion"""
|
23
23
|
yield "" # Placeholder implementation
|
24
24
|
|
25
|
+
@abstractmethod
|
26
|
+
async def cancel_stream(self) -> None:
|
27
|
+
"""Cancel any active streaming request"""
|
28
|
+
pass
|
29
|
+
|
25
30
|
@abstractmethod
|
26
31
|
def get_available_models(self) -> List[Dict[str, Any]]:
|
27
32
|
"""Get list of available models from this provider"""
|
@@ -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
|
@@ -266,6 +274,29 @@ class OllamaClient(BaseModelClient):
|
|
266
274
|
last_error = None
|
267
275
|
self._active_stream_session = None # Track the active session
|
268
276
|
|
277
|
+
# First check if the model exists in our available models
|
278
|
+
try:
|
279
|
+
available_models = await self.get_available_models()
|
280
|
+
model_exists = False
|
281
|
+
available_model_names = []
|
282
|
+
|
283
|
+
for m in available_models:
|
284
|
+
model_id = m.get("id", "")
|
285
|
+
available_model_names.append(model_id)
|
286
|
+
if model_id == model:
|
287
|
+
model_exists = True
|
288
|
+
break
|
289
|
+
|
290
|
+
if not model_exists:
|
291
|
+
error_msg = f"Model '{model}' not found in available models. Available models include: {', '.join(available_model_names[:5])}"
|
292
|
+
if len(available_model_names) > 5:
|
293
|
+
error_msg += f" and {len(available_model_names) - 5} more."
|
294
|
+
logger.error(error_msg)
|
295
|
+
raise OllamaApiError(error_msg)
|
296
|
+
except Exception as e:
|
297
|
+
debug_log(f"Error checking model availability: {str(e)}")
|
298
|
+
# Continue anyway, the main request will handle errors
|
299
|
+
|
269
300
|
while retries >= 0:
|
270
301
|
try:
|
271
302
|
# First try a quick test request to check if model is loaded
|
@@ -299,6 +330,17 @@ class OllamaClient(BaseModelClient):
|
|
299
330
|
if response.status != 200:
|
300
331
|
logger.warning(f"Model test request failed with status {response.status}")
|
301
332
|
debug_log(f"Model test request failed with status {response.status}")
|
333
|
+
|
334
|
+
# Check if this is a 404 Not Found error
|
335
|
+
if response.status == 404:
|
336
|
+
error_text = await response.text()
|
337
|
+
debug_log(f"404 error details: {error_text}")
|
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
|
343
|
+
|
302
344
|
raise aiohttp.ClientError("Model not ready")
|
303
345
|
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
|
304
346
|
logger.info(f"Model cold start detected: {str(e)}")
|
@@ -326,6 +368,16 @@ class OllamaClient(BaseModelClient):
|
|
326
368
|
logger.error("Failed to pull model")
|
327
369
|
debug_log("Failed to pull model")
|
328
370
|
self._model_loading = False # Reset flag on failure
|
371
|
+
|
372
|
+
# Check if this is a 404 Not Found error
|
373
|
+
if pull_response.status == 404:
|
374
|
+
error_text = await pull_response.text()
|
375
|
+
debug_log(f"404 error details: {error_text}")
|
376
|
+
# This is likely a model not found in registry
|
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)
|
380
|
+
|
329
381
|
raise Exception("Failed to pull model")
|
330
382
|
logger.info("Model pulled successfully")
|
331
383
|
debug_log("Model pulled successfully")
|
@@ -3,10 +3,15 @@ import asyncio
|
|
3
3
|
from typing import List, Dict, Any, Optional, Generator, AsyncGenerator
|
4
4
|
from .base import BaseModelClient
|
5
5
|
from ..config import OPENAI_API_KEY
|
6
|
+
import logging
|
7
|
+
|
8
|
+
# Set up logging
|
9
|
+
logger = logging.getLogger(__name__)
|
6
10
|
|
7
11
|
class OpenAIClient(BaseModelClient):
|
8
12
|
def __init__(self):
|
9
13
|
self.client = None # Initialize in create()
|
14
|
+
self._active_stream = None # Track active stream for cancellation
|
10
15
|
|
11
16
|
@classmethod
|
12
17
|
async def create(cls) -> 'OpenAIClient':
|
@@ -115,6 +120,10 @@ class OpenAIClient(BaseModelClient):
|
|
115
120
|
max_tokens=max_tokens,
|
116
121
|
stream=True,
|
117
122
|
)
|
123
|
+
|
124
|
+
# Store the stream for potential cancellation
|
125
|
+
self._active_stream = stream
|
126
|
+
|
118
127
|
debug_log("OpenAI: stream created successfully")
|
119
128
|
|
120
129
|
# Yield a small padding token at the beginning for very short prompts
|
@@ -128,6 +137,11 @@ class OpenAIClient(BaseModelClient):
|
|
128
137
|
debug_log("OpenAI: starting to process chunks")
|
129
138
|
|
130
139
|
async for chunk in stream:
|
140
|
+
# Check if stream has been cancelled
|
141
|
+
if self._active_stream is None:
|
142
|
+
debug_log("OpenAI: stream was cancelled, stopping generation")
|
143
|
+
break
|
144
|
+
|
131
145
|
chunk_count += 1
|
132
146
|
try:
|
133
147
|
if chunk.choices and hasattr(chunk.choices[0], 'delta') and hasattr(chunk.choices[0].delta, 'content'):
|
@@ -148,6 +162,9 @@ class OpenAIClient(BaseModelClient):
|
|
148
162
|
|
149
163
|
debug_log(f"OpenAI: stream completed successfully with {chunk_count} chunks")
|
150
164
|
|
165
|
+
# Clear the active stream reference when done
|
166
|
+
self._active_stream = None
|
167
|
+
|
151
168
|
# If we reach this point, we've successfully processed the stream
|
152
169
|
break
|
153
170
|
|
@@ -168,6 +185,20 @@ class OpenAIClient(BaseModelClient):
|
|
168
185
|
yield f"Error: {str(e)}"
|
169
186
|
raise Exception(f"OpenAI streaming error: {str(e)}")
|
170
187
|
|
188
|
+
async def cancel_stream(self) -> None:
|
189
|
+
"""Cancel any active streaming request"""
|
190
|
+
logger.info("Cancelling active OpenAI stream")
|
191
|
+
try:
|
192
|
+
from app.main import debug_log
|
193
|
+
debug_log("OpenAI: cancelling active stream")
|
194
|
+
except ImportError:
|
195
|
+
pass
|
196
|
+
|
197
|
+
# Simply set the active stream to None
|
198
|
+
# This will cause the generate_stream method to stop processing chunks
|
199
|
+
self._active_stream = None
|
200
|
+
logger.info("OpenAI stream cancelled successfully")
|
201
|
+
|
171
202
|
async def get_available_models(self) -> List[Dict[str, Any]]:
|
172
203
|
"""Fetch list of available OpenAI models from the /models endpoint"""
|
173
204
|
try:
|
@@ -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
|
@@ -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
|
@@ -120,54 +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
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
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
|
-
# This avoids text reflowing as new tokens arrive
|
145
|
-
formatted_content = self._format_content(content)
|
146
|
-
|
147
|
-
# Use a direct update that forces refresh - critical fix for streaming
|
148
|
-
# This ensures content is immediately visible
|
149
|
-
print(f"Updating widget with formatted content length: {len(formatted_content)}")
|
150
|
-
self.update(formatted_content, refresh=True)
|
151
|
-
|
152
|
-
# Force app-level refresh and scroll to ensure visibility
|
153
|
-
try:
|
154
|
-
# Always force app refresh for every update
|
155
|
-
if self.app:
|
156
|
-
# Force a full layout refresh to ensure content is visible
|
157
|
-
self.app.refresh(layout=True)
|
123
|
+
"""Update the message content."""
|
124
|
+
import logging
|
125
|
+
logger = logging.getLogger(__name__)
|
126
|
+
logger.debug(f"MessageDisplay.update_content called with content length: {len(content)}")
|
127
|
+
|
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)
|
158
139
|
|
159
|
-
#
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
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
|
164
145
|
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
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)}")
|
171
178
|
|
172
179
|
def _format_content(self, content: str) -> str:
|
173
180
|
"""Format message content with timestamp and handle markdown links"""
|
@@ -191,8 +198,8 @@ class MessageDisplay(Static): # Inherit from Static instead of RichLog
|
|
191
198
|
# But keep our timestamp markup
|
192
199
|
timestamp_markup = f"[dim]{timestamp}[/dim]"
|
193
200
|
|
194
|
-
#
|
195
|
-
|
201
|
+
# Use proper logging instead of print
|
202
|
+
logger.debug(f"Formatting content: {len(content)} chars")
|
196
203
|
|
197
204
|
return f"{timestamp_markup} {content}"
|
198
205
|
|