chat-console 0.2.99__py3-none-any.whl → 0.3.4__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/anthropic.py +96 -72
- app/api/base.py +2 -2
- app/api/ollama.py +21 -10
- app/api/openai.py +88 -31
- app/main.py +105 -54
- app/ui/chat_interface.py +41 -7
- app/ui/model_selector.py +52 -14
- app/utils.py +130 -91
- {chat_console-0.2.99.dist-info → chat_console-0.3.4.dist-info}/METADATA +1 -1
- chat_console-0.3.4.dist-info/RECORD +24 -0
- {chat_console-0.2.99.dist-info → chat_console-0.3.4.dist-info}/WHEEL +1 -1
- chat_console-0.2.99.dist-info/RECORD +0 -24
- {chat_console-0.2.99.dist-info → chat_console-0.3.4.dist-info}/entry_points.txt +0 -0
- {chat_console-0.2.99.dist-info → chat_console-0.3.4.dist-info}/licenses/LICENSE +0 -0
- {chat_console-0.2.99.dist-info → chat_console-0.3.4.dist-info}/top_level.txt +0 -0
app/__init__.py
CHANGED
app/api/anthropic.py
CHANGED
@@ -144,86 +144,110 @@ class AnthropicClient(BaseModelClient):
|
|
144
144
|
except ImportError:
|
145
145
|
debug_log = lambda msg: None
|
146
146
|
|
147
|
+
# Always include a reliable fallback list in case API calls fail
|
148
|
+
fallback_models = [
|
149
|
+
{"id": "claude-3-opus-20240229", "name": "Claude 3 Opus"},
|
150
|
+
{"id": "claude-3-sonnet-20240229", "name": "Claude 3 Sonnet"},
|
151
|
+
{"id": "claude-3-haiku-20240307", "name": "Claude 3 Haiku"},
|
152
|
+
{"id": "claude-3-5-sonnet-20240620", "name": "Claude 3.5 Sonnet"},
|
153
|
+
{"id": "claude-3-7-sonnet-20250219", "name": "Claude 3.7 Sonnet"},
|
154
|
+
]
|
155
|
+
|
156
|
+
# If no client is initialized, return fallback immediately
|
157
|
+
if not self.client:
|
158
|
+
debug_log("Anthropic: No client initialized, using fallback models")
|
159
|
+
return fallback_models
|
160
|
+
|
147
161
|
try:
|
148
162
|
debug_log("Anthropic: Fetching models from API...")
|
149
|
-
|
150
|
-
#
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
163
|
+
|
164
|
+
# Try using the models.list method if available in newer SDK versions
|
165
|
+
if hasattr(self.client, 'models') and hasattr(self.client.models, 'list'):
|
166
|
+
try:
|
167
|
+
debug_log("Anthropic: Using client.models.list() method")
|
168
|
+
models_response = await self.client.models.list()
|
169
|
+
if hasattr(models_response, 'data') and isinstance(models_response.data, list):
|
170
|
+
formatted_models = [
|
171
|
+
{"id": model.id, "name": getattr(model, "name", model.id)}
|
172
|
+
for model in models_response.data
|
173
|
+
]
|
174
|
+
debug_log(f"Anthropic: Found {len(formatted_models)} models via SDK")
|
175
|
+
return formatted_models
|
176
|
+
except Exception as sdk_err:
|
177
|
+
debug_log(f"Anthropic: Error using models.list(): {str(sdk_err)}")
|
178
|
+
# Continue to next method
|
179
|
+
|
180
|
+
# Try direct HTTP request if client exposes the underlying HTTP client
|
157
181
|
if hasattr(self.client, '_client') and hasattr(self.client._client, 'get'):
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
182
|
+
try:
|
183
|
+
debug_log("Anthropic: Using direct HTTP request to /v1/models")
|
184
|
+
response = await self.client._client.get(
|
185
|
+
"/v1/models",
|
186
|
+
headers={"anthropic-version": "2023-06-01"}
|
187
|
+
)
|
188
|
+
response.raise_for_status()
|
189
|
+
models_data = response.json()
|
190
|
+
|
191
|
+
if 'data' in models_data and isinstance(models_data['data'], list):
|
192
|
+
formatted_models = [
|
193
|
+
{"id": model.get("id"), "name": model.get("display_name", model.get("id"))}
|
194
|
+
for model in models_data['data']
|
195
|
+
if model.get("id")
|
196
|
+
]
|
197
|
+
debug_log(f"Anthropic: Found {len(formatted_models)} models via HTTP request")
|
198
|
+
return formatted_models
|
199
|
+
else:
|
200
|
+
debug_log("Anthropic: Unexpected API response format")
|
201
|
+
except Exception as http_err:
|
202
|
+
debug_log(f"Anthropic: HTTP request error: {str(http_err)}")
|
203
|
+
# Continue to fallback
|
204
|
+
|
205
|
+
# If we reach here, both methods failed
|
206
|
+
debug_log("Anthropic: All API methods failed, using fallback models")
|
207
|
+
return fallback_models
|
183
208
|
|
184
209
|
except Exception as e:
|
185
210
|
debug_log(f"Anthropic: Failed to fetch models from API: {str(e)}")
|
186
|
-
|
187
|
-
# Include Claude 3.7 Sonnet with the correct full ID
|
188
|
-
fallback_models = [
|
189
|
-
{"id": "claude-3-opus-20240229", "name": "Claude 3 Opus"},
|
190
|
-
{"id": "claude-3-sonnet-20240229", "name": "Claude 3 Sonnet"},
|
191
|
-
{"id": "claude-3-haiku-20240307", "name": "Claude 3 Haiku"},
|
192
|
-
{"id": "claude-3-5-sonnet-20240620", "name": "Claude 3.5 Sonnet"},
|
193
|
-
{"id": "claude-3-7-sonnet-20250219", "name": "Claude 3.7 Sonnet"}, # Add Claude 3.7 Sonnet
|
194
|
-
]
|
195
|
-
debug_log("Anthropic: Using fallback model list:")
|
196
|
-
for model in fallback_models:
|
197
|
-
debug_log(f" - ID: {model['id']}, Name: {model['name']}")
|
211
|
+
debug_log("Anthropic: Using fallback model list")
|
198
212
|
return fallback_models
|
199
213
|
|
200
|
-
# Keep this synchronous for now, but make it call the async fetcher
|
201
|
-
# Note: This is slightly awkward. Ideally, config loading would be async.
|
202
|
-
# For now, we'll run the async fetcher within the sync method using asyncio.run()
|
203
|
-
# This is NOT ideal for performance but avoids larger refactoring of config loading.
|
204
214
|
def get_available_models(self) -> List[Dict[str, Any]]:
|
205
215
|
"""Get list of available Claude models by fetching from API."""
|
216
|
+
# Reliable fallback list that doesn't depend on async operations
|
217
|
+
fallback_models = [
|
218
|
+
{"id": "claude-3-opus-20240229", "name": "Claude 3 Opus"},
|
219
|
+
{"id": "claude-3-sonnet-20240229", "name": "Claude 3 Sonnet"},
|
220
|
+
{"id": "claude-3-haiku-20240307", "name": "Claude 3 Haiku"},
|
221
|
+
{"id": "claude-3-5-sonnet-20240620", "name": "Claude 3.5 Sonnet"},
|
222
|
+
{"id": "claude-3-7-sonnet-20250219", "name": "Claude 3.7 Sonnet"},
|
223
|
+
]
|
224
|
+
|
206
225
|
try:
|
207
|
-
#
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
226
|
+
# Check if we're already in an event loop
|
227
|
+
try:
|
228
|
+
loop = asyncio.get_running_loop()
|
229
|
+
in_loop = True
|
230
|
+
except RuntimeError:
|
231
|
+
in_loop = False
|
232
|
+
|
233
|
+
if in_loop:
|
234
|
+
# We're already in an event loop, create a future
|
235
|
+
try:
|
236
|
+
from app.main import debug_log
|
237
|
+
except ImportError:
|
238
|
+
debug_log = lambda msg: None
|
239
|
+
|
240
|
+
debug_log("Anthropic: Already in event loop, using fallback models")
|
241
|
+
return fallback_models
|
242
|
+
else:
|
243
|
+
# Not in an event loop, we can use asyncio.run
|
244
|
+
models = asyncio.run(self._fetch_models_from_api())
|
245
|
+
return models
|
246
|
+
except Exception as e:
|
247
|
+
try:
|
248
|
+
from app.main import debug_log
|
249
|
+
except ImportError:
|
250
|
+
debug_log = lambda msg: None
|
251
|
+
|
252
|
+
debug_log(f"Anthropic: Error in get_available_models: {str(e)}")
|
253
|
+
return fallback_models
|
app/api/base.py
CHANGED
@@ -120,8 +120,8 @@ class BaseModelClient(ABC):
|
|
120
120
|
if provider == "ollama":
|
121
121
|
return await OllamaClient.create()
|
122
122
|
elif provider == "openai":
|
123
|
-
return OpenAIClient()
|
123
|
+
return await OpenAIClient.create()
|
124
124
|
elif provider == "anthropic":
|
125
|
-
return AnthropicClient()
|
125
|
+
return await AnthropicClient.create()
|
126
126
|
else:
|
127
127
|
raise ValueError(f"Unknown provider: {provider}")
|
app/api/ollama.py
CHANGED
@@ -369,6 +369,10 @@ class OllamaClient(BaseModelClient):
|
|
369
369
|
|
370
370
|
# Use a simpler async iteration pattern that's less error-prone
|
371
371
|
debug_log("Starting to process response stream")
|
372
|
+
|
373
|
+
# Set a flag to track if we've yielded any content
|
374
|
+
has_yielded_content = False
|
375
|
+
|
372
376
|
async for line in response.content:
|
373
377
|
# Check cancellation periodically
|
374
378
|
if self._active_stream_session is None:
|
@@ -378,31 +382,38 @@ class OllamaClient(BaseModelClient):
|
|
378
382
|
try:
|
379
383
|
# Process the chunk
|
380
384
|
if line:
|
381
|
-
chunk = line.decode().strip()
|
382
385
|
chunk_str = line.decode().strip()
|
383
386
|
# Check if it looks like JSON before trying to parse
|
384
387
|
if chunk_str.startswith('{') and chunk_str.endswith('}'):
|
385
388
|
try:
|
386
389
|
data = json.loads(chunk_str)
|
387
390
|
if isinstance(data, dict) and "response" in data:
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
+
response_text = data["response"]
|
392
|
+
if response_text: # Only yield non-empty responses
|
393
|
+
has_yielded_content = True
|
394
|
+
chunk_length = len(response_text)
|
395
|
+
# Only log occasionally to reduce console spam
|
396
|
+
if chunk_length % 20 == 0:
|
397
|
+
debug_log(f"Yielding chunk of length: {chunk_length}")
|
398
|
+
yield response_text
|
391
399
|
else:
|
392
|
-
debug_log(f"JSON chunk missing 'response' key: {chunk_str}")
|
400
|
+
debug_log(f"JSON chunk missing 'response' key: {chunk_str[:100]}")
|
393
401
|
except json.JSONDecodeError:
|
394
|
-
debug_log(f"JSON decode error for chunk: {chunk_str}")
|
402
|
+
debug_log(f"JSON decode error for chunk: {chunk_str[:100]}")
|
395
403
|
else:
|
396
404
|
# Log unexpected non-JSON lines but don't process them
|
397
|
-
if chunk_str:
|
398
|
-
debug_log(f"Received unexpected non-JSON line: {chunk_str}")
|
399
|
-
# Continue processing next line regardless of parsing success/failure of current line
|
400
|
-
continue
|
405
|
+
if chunk_str and len(chunk_str) > 5: # Avoid logging empty or tiny lines
|
406
|
+
debug_log(f"Received unexpected non-JSON line: {chunk_str[:100]}")
|
401
407
|
except Exception as chunk_err:
|
402
408
|
debug_log(f"Error processing chunk: {str(chunk_err)}")
|
403
409
|
# Continue instead of breaking to try processing more chunks
|
404
410
|
continue
|
405
411
|
|
412
|
+
# If we didn't yield any content, yield a default message
|
413
|
+
if not has_yielded_content:
|
414
|
+
debug_log("No content was yielded from stream, providing fallback response")
|
415
|
+
yield "I'm sorry, but I couldn't generate a response. Please try again or try a different model."
|
416
|
+
|
406
417
|
logger.info("Streaming completed successfully")
|
407
418
|
debug_log("Streaming completed successfully")
|
408
419
|
return
|
app/api/openai.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
from openai import AsyncOpenAI
|
2
|
+
import asyncio
|
2
3
|
from typing import List, Dict, Any, Optional, Generator, AsyncGenerator
|
3
4
|
from .base import BaseModelClient
|
4
5
|
from ..config import OPENAI_API_KEY
|
@@ -84,47 +85,103 @@ class OpenAIClient(BaseModelClient):
|
|
84
85
|
debug_log(f"OpenAI: skipping invalid message: {m}")
|
85
86
|
|
86
87
|
debug_log(f"OpenAI: prepared {len(api_messages)} valid messages")
|
88
|
+
|
89
|
+
# Check for empty or very short prompts and enhance them slightly
|
90
|
+
# This helps with the "hi" case where OpenAI might not generate a meaningful response
|
91
|
+
if api_messages and len(api_messages) > 0:
|
92
|
+
last_message = api_messages[-1]
|
93
|
+
if last_message["role"] == "user" and len(last_message["content"].strip()) <= 3:
|
94
|
+
debug_log(f"OpenAI: Enhancing very short user prompt: '{last_message['content']}'")
|
95
|
+
last_message["content"] = f"{last_message['content']} - Please respond conversationally."
|
96
|
+
debug_log(f"OpenAI: Enhanced to: '{last_message['content']}'")
|
97
|
+
|
87
98
|
except Exception as msg_error:
|
88
99
|
debug_log(f"OpenAI: error preparing messages: {str(msg_error)}")
|
89
100
|
# Fallback to a simpler message format if processing fails
|
90
101
|
api_messages = [{"role": "user", "content": "Please respond to my request."}]
|
91
102
|
|
92
103
|
debug_log("OpenAI: requesting stream")
|
93
|
-
stream = await self.client.chat.completions.create(
|
94
|
-
model=model,
|
95
|
-
messages=api_messages,
|
96
|
-
temperature=temperature,
|
97
|
-
max_tokens=max_tokens,
|
98
|
-
stream=True,
|
99
|
-
)
|
100
104
|
|
101
|
-
|
102
|
-
|
105
|
+
# Use more robust error handling with retry for connection issues
|
106
|
+
max_retries = 2
|
107
|
+
retry_count = 0
|
108
|
+
|
109
|
+
while retry_count <= max_retries:
|
103
110
|
try:
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
111
|
+
stream = await self.client.chat.completions.create(
|
112
|
+
model=model,
|
113
|
+
messages=api_messages,
|
114
|
+
temperature=temperature,
|
115
|
+
max_tokens=max_tokens,
|
116
|
+
stream=True,
|
117
|
+
)
|
118
|
+
debug_log("OpenAI: stream created successfully")
|
119
|
+
|
120
|
+
# Yield a small padding token at the beginning for very short prompts
|
121
|
+
# This ensures the UI sees immediate content updates
|
122
|
+
if any(m["role"] == "user" and len(m["content"].strip()) <= 3 for m in api_messages):
|
123
|
+
debug_log("OpenAI: Adding initial padding token for short message")
|
124
|
+
yield "" # Empty string to trigger UI update cycle
|
125
|
+
|
126
|
+
# Process stream chunks
|
127
|
+
chunk_count = 0
|
128
|
+
debug_log("OpenAI: starting to process chunks")
|
119
129
|
|
130
|
+
async for chunk in stream:
|
131
|
+
chunk_count += 1
|
132
|
+
try:
|
133
|
+
if chunk.choices and hasattr(chunk.choices[0], 'delta') and hasattr(chunk.choices[0].delta, 'content'):
|
134
|
+
content = chunk.choices[0].delta.content
|
135
|
+
if content is not None:
|
136
|
+
# Ensure we're returning a string
|
137
|
+
text = str(content)
|
138
|
+
debug_log(f"OpenAI: yielding chunk {chunk_count} of length: {len(text)}")
|
139
|
+
yield text
|
140
|
+
else:
|
141
|
+
debug_log(f"OpenAI: skipping None content chunk {chunk_count}")
|
142
|
+
else:
|
143
|
+
debug_log(f"OpenAI: skipping chunk {chunk_count} with missing content")
|
144
|
+
except Exception as chunk_error:
|
145
|
+
debug_log(f"OpenAI: error processing chunk {chunk_count}: {str(chunk_error)}")
|
146
|
+
# Skip problematic chunks but continue processing
|
147
|
+
continue
|
148
|
+
|
149
|
+
debug_log(f"OpenAI: stream completed successfully with {chunk_count} chunks")
|
150
|
+
|
151
|
+
# If we reach this point, we've successfully processed the stream
|
152
|
+
break
|
153
|
+
|
154
|
+
except Exception as e:
|
155
|
+
debug_log(f"OpenAI: error in attempt {retry_count+1}/{max_retries+1}: {str(e)}")
|
156
|
+
retry_count += 1
|
157
|
+
if retry_count <= max_retries:
|
158
|
+
debug_log(f"OpenAI: retrying after error (attempt {retry_count+1})")
|
159
|
+
# Simple exponential backoff
|
160
|
+
await asyncio.sleep(1 * retry_count)
|
161
|
+
else:
|
162
|
+
debug_log("OpenAI: max retries reached, raising exception")
|
163
|
+
raise Exception(f"OpenAI streaming error after {max_retries+1} attempts: {str(e)}")
|
164
|
+
|
120
165
|
except Exception as e:
|
121
166
|
debug_log(f"OpenAI: error in generate_stream: {str(e)}")
|
167
|
+
# Yield a simple error message as a last resort to ensure UI updates
|
168
|
+
yield f"Error: {str(e)}"
|
122
169
|
raise Exception(f"OpenAI streaming error: {str(e)}")
|
123
170
|
|
124
|
-
def get_available_models(self) -> List[Dict[str, Any]]:
|
125
|
-
"""
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
171
|
+
async def get_available_models(self) -> List[Dict[str, Any]]:
|
172
|
+
"""Fetch list of available OpenAI models from the /models endpoint"""
|
173
|
+
try:
|
174
|
+
models_response = await self.client.models.list()
|
175
|
+
# Each model has an 'id' and possibly other metadata
|
176
|
+
models = []
|
177
|
+
for model in models_response.data:
|
178
|
+
# Use 'id' as both id and name for now; can enhance with more info if needed
|
179
|
+
models.append({"id": model.id, "name": model.id})
|
180
|
+
return models
|
181
|
+
except Exception as e:
|
182
|
+
# Fallback to a static list if API call fails
|
183
|
+
return [
|
184
|
+
{"id": "gpt-3.5-turbo", "name": "gpt-3.5-turbo"},
|
185
|
+
{"id": "gpt-4", "name": "gpt-4"},
|
186
|
+
{"id": "gpt-4-turbo", "name": "gpt-4-turbo"}
|
187
|
+
]
|