chat-console 0.3.7__py3-none-any.whl → 0.3.9__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 +180 -195
- app/api/base.py +5 -0
- app/api/ollama.py +45 -0
- app/api/openai.py +31 -0
- app/main.py +39 -9
- app/ui/chat_interface.py +49 -4
- app/utils.py +74 -11
- {chat_console-0.3.7.dist-info → chat_console-0.3.9.dist-info}/METADATA +1 -1
- chat_console-0.3.9.dist-info/RECORD +24 -0
- chat_console-0.3.7.dist-info/RECORD +0 -24
- {chat_console-0.3.7.dist-info → chat_console-0.3.9.dist-info}/WHEEL +0 -0
- {chat_console-0.3.7.dist-info → chat_console-0.3.9.dist-info}/entry_points.txt +0 -0
- {chat_console-0.3.7.dist-info → chat_console-0.3.9.dist-info}/licenses/LICENSE +0 -0
- {chat_console-0.3.7.dist-info → chat_console-0.3.9.dist-info}/top_level.txt +0 -0
app/__init__.py
CHANGED
app/api/anthropic.py
CHANGED
@@ -1,13 +1,17 @@
|
|
1
1
|
import anthropic
|
2
|
-
import asyncio
|
2
|
+
import asyncio
|
3
|
+
import logging
|
3
4
|
from typing import List, Dict, Any, Optional, Generator, AsyncGenerator
|
4
5
|
from .base import BaseModelClient
|
5
6
|
from ..config import ANTHROPIC_API_KEY
|
6
|
-
|
7
|
+
|
8
|
+
# Set up logging
|
9
|
+
logger = logging.getLogger(__name__)
|
7
10
|
|
8
11
|
class AnthropicClient(BaseModelClient):
|
9
12
|
def __init__(self):
|
10
13
|
self.client = None # Initialize in create()
|
14
|
+
self._active_stream = None # Track active stream for cancellation
|
11
15
|
|
12
16
|
@classmethod
|
13
17
|
async def create(cls) -> 'AnthropicClient':
|
@@ -17,237 +21,218 @@ class AnthropicClient(BaseModelClient):
|
|
17
21
|
return instance
|
18
22
|
|
19
23
|
def _prepare_messages(self, messages: List[Dict[str, str]], style: Optional[str] = None) -> List[Dict[str, str]]:
|
20
|
-
"""Prepare messages for
|
21
|
-
# Anthropic expects role to be 'user' or 'assistant'
|
24
|
+
"""Prepare messages for Anthropic API"""
|
22
25
|
processed_messages = []
|
23
26
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
28
47
|
processed_messages.append({
|
29
|
-
"role": "
|
30
|
-
"content":
|
48
|
+
"role": "system",
|
49
|
+
"content": message["content"]
|
31
50
|
})
|
32
51
|
else:
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
for i, msg in enumerate(processed_messages):
|
39
|
-
if msg["role"] == "user":
|
40
|
-
content = msg["content"]
|
41
|
-
if "<userStyle>" not in content:
|
42
|
-
style_instructions = self._get_style_instructions(style)
|
43
|
-
msg["content"] = f"<userStyle>{style_instructions}</userStyle>\n\n{content}"
|
44
|
-
break
|
52
|
+
# For any other role, treat as user message
|
53
|
+
processed_messages.append({
|
54
|
+
"role": "user",
|
55
|
+
"content": message["content"]
|
56
|
+
})
|
45
57
|
|
46
58
|
return processed_messages
|
47
59
|
|
48
60
|
def _get_style_instructions(self, style: str) -> str:
|
49
61
|
"""Get formatting instructions for different styles"""
|
50
62
|
styles = {
|
51
|
-
"concise": "
|
52
|
-
"detailed": "
|
53
|
-
"technical": "
|
54
|
-
"friendly": "
|
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.",
|
55
67
|
}
|
56
68
|
|
57
69
|
return styles.get(style, "")
|
58
70
|
|
59
|
-
async def generate_completion(self, messages: List[Dict[str, str]],
|
60
|
-
model: str,
|
61
|
-
style: Optional[str] = None,
|
62
|
-
temperature: float = 0.7,
|
71
|
+
async def generate_completion(self, messages: List[Dict[str, str]],
|
72
|
+
model: str,
|
73
|
+
style: Optional[str] = None,
|
74
|
+
temperature: float = 0.7,
|
63
75
|
max_tokens: Optional[int] = None) -> str:
|
64
|
-
"""Generate a text completion using
|
65
|
-
try:
|
66
|
-
from app.main import debug_log
|
67
|
-
except ImportError:
|
68
|
-
debug_log = lambda msg: None
|
69
|
-
|
70
|
-
# Resolve the model ID right before making the API call
|
71
|
-
original_model = model
|
72
|
-
resolved_model = resolve_model_id(model)
|
73
|
-
debug_log(f"Anthropic: Original model ID '{original_model}' resolved to '{resolved_model}' in generate_completion")
|
74
|
-
|
76
|
+
"""Generate a text completion using Anthropic"""
|
75
77
|
processed_messages = self._prepare_messages(messages, style)
|
76
78
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
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)}")
|
85
91
|
|
86
|
-
async def generate_stream(self, messages: List[Dict[str, str]],
|
87
|
-
model: str,
|
92
|
+
async def generate_stream(self, messages: List[Dict[str, str]],
|
93
|
+
model: str,
|
88
94
|
style: Optional[str] = None,
|
89
|
-
temperature: float = 0.7,
|
95
|
+
temperature: float = 0.7,
|
90
96
|
max_tokens: Optional[int] = None) -> AsyncGenerator[str, None]:
|
91
|
-
"""Generate a streaming text completion using
|
97
|
+
"""Generate a streaming text completion using Anthropic"""
|
92
98
|
try:
|
93
99
|
from app.main import debug_log # Import debug logging if available
|
100
|
+
debug_log(f"Anthropic: starting streaming generation with model: {model}")
|
94
101
|
except ImportError:
|
95
102
|
# If debug_log not available, create a no-op function
|
96
103
|
debug_log = lambda msg: None
|
97
104
|
|
98
|
-
# Resolve the model ID right before making the API call
|
99
|
-
original_model = model
|
100
|
-
resolved_model = resolve_model_id(model)
|
101
|
-
debug_log(f"Anthropic: Original model ID '{original_model}' resolved to '{resolved_model}'")
|
102
|
-
debug_log(f"Anthropic: starting streaming generation with model: {resolved_model}")
|
103
|
-
|
104
105
|
processed_messages = self._prepare_messages(messages, style)
|
105
106
|
|
106
107
|
try:
|
107
|
-
debug_log(f"Anthropic:
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
temperature=temperature,
|
113
|
-
max_tokens=max_tokens or 1024,
|
114
|
-
)
|
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
|
115
113
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
except Exception as chunk_error: # Restore the except block for chunk processing
|
132
|
-
debug_log(f"Anthropic: error processing chunk: {str(chunk_error)}")
|
133
|
-
# Skip problematic chunks but continue processing
|
134
|
-
continue # This continue is now correctly inside the loop and except block
|
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
|
135
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
|
+
|
136
177
|
except Exception as e:
|
137
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)}"
|
138
181
|
raise Exception(f"Anthropic streaming error: {str(e)}")
|
139
|
-
|
140
|
-
async def
|
141
|
-
"""
|
182
|
+
|
183
|
+
async def cancel_stream(self) -> None:
|
184
|
+
"""Cancel any active streaming request"""
|
185
|
+
logger.info("Cancelling active Anthropic stream")
|
142
186
|
try:
|
143
187
|
from app.main import debug_log
|
188
|
+
debug_log("Anthropic: cancelling active stream")
|
144
189
|
except ImportError:
|
145
|
-
|
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
|
-
|
161
|
-
try:
|
162
|
-
debug_log("Anthropic: Fetching models from API...")
|
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
|
190
|
+
pass
|
179
191
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
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
|
+
}
|
223
236
|
]
|
224
237
|
|
225
|
-
|
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
|
238
|
+
return models
|
app/api/base.py
CHANGED
@@ -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"""
|
app/api/ollama.py
CHANGED
@@ -266,6 +266,31 @@ class OllamaClient(BaseModelClient):
|
|
266
266
|
last_error = None
|
267
267
|
self._active_stream_session = None # Track the active session
|
268
268
|
|
269
|
+
# First check if the model exists in our available models
|
270
|
+
try:
|
271
|
+
available_models = await self.get_available_models()
|
272
|
+
model_exists = False
|
273
|
+
available_model_names = []
|
274
|
+
|
275
|
+
for m in available_models:
|
276
|
+
model_id = m.get("id", "")
|
277
|
+
available_model_names.append(model_id)
|
278
|
+
if model_id == model:
|
279
|
+
model_exists = True
|
280
|
+
break
|
281
|
+
|
282
|
+
if not model_exists:
|
283
|
+
debug_log(f"Model '{model}' not found in available models")
|
284
|
+
# Instead of failing, yield a helpful error message
|
285
|
+
yield f"Model '{model}' not found. Available models include: {', '.join(available_model_names[:5])}"
|
286
|
+
if len(available_model_names) > 5:
|
287
|
+
yield f" and {len(available_model_names) - 5} more."
|
288
|
+
yield "\n\nPlease try a different model or check your spelling."
|
289
|
+
return
|
290
|
+
except Exception as e:
|
291
|
+
debug_log(f"Error checking model availability: {str(e)}")
|
292
|
+
# Continue anyway, the main request will handle errors
|
293
|
+
|
269
294
|
while retries >= 0:
|
270
295
|
try:
|
271
296
|
# First try a quick test request to check if model is loaded
|
@@ -299,6 +324,16 @@ class OllamaClient(BaseModelClient):
|
|
299
324
|
if response.status != 200:
|
300
325
|
logger.warning(f"Model test request failed with status {response.status}")
|
301
326
|
debug_log(f"Model test request failed with status {response.status}")
|
327
|
+
|
328
|
+
# Check if this is a 404 Not Found error
|
329
|
+
if response.status == 404:
|
330
|
+
error_text = await response.text()
|
331
|
+
debug_log(f"404 error details: {error_text}")
|
332
|
+
# This is likely a model not found error
|
333
|
+
yield f"Error: Model '{model}' not found on the Ollama server."
|
334
|
+
yield "\nPlease check if the model name is correct or try pulling it first."
|
335
|
+
return
|
336
|
+
|
302
337
|
raise aiohttp.ClientError("Model not ready")
|
303
338
|
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
|
304
339
|
logger.info(f"Model cold start detected: {str(e)}")
|
@@ -326,6 +361,16 @@ class OllamaClient(BaseModelClient):
|
|
326
361
|
logger.error("Failed to pull model")
|
327
362
|
debug_log("Failed to pull model")
|
328
363
|
self._model_loading = False # Reset flag on failure
|
364
|
+
|
365
|
+
# Check if this is a 404 Not Found error
|
366
|
+
if pull_response.status == 404:
|
367
|
+
error_text = await pull_response.text()
|
368
|
+
debug_log(f"404 error details: {error_text}")
|
369
|
+
# This is likely a model not found in registry
|
370
|
+
yield f"Error: Model '{model}' not found in the Ollama registry."
|
371
|
+
yield "\nPlease check if the model name is correct or try a different model."
|
372
|
+
return
|
373
|
+
|
329
374
|
raise Exception("Failed to pull model")
|
330
375
|
logger.info("Model pulled successfully")
|
331
376
|
debug_log("Model pulled successfully")
|
app/api/openai.py
CHANGED
@@ -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:
|
app/main.py
CHANGED
@@ -940,27 +940,39 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
|
|
940
940
|
last_refresh_time = time.time() # Initialize refresh throttling timer
|
941
941
|
|
942
942
|
async def update_ui(content: str):
|
943
|
-
# This function
|
943
|
+
# This function is called by the worker with each content update
|
944
944
|
if not self.is_generating:
|
945
945
|
debug_log("update_ui called but is_generating is False, returning.")
|
946
946
|
return
|
947
947
|
|
948
948
|
async with update_lock:
|
949
949
|
try:
|
950
|
+
# Add more verbose logging
|
951
|
+
debug_log(f"update_ui called with content length: {len(content)}")
|
952
|
+
print(f"update_ui: Updating with content length {len(content)}")
|
953
|
+
|
950
954
|
# Clear thinking indicator on first content
|
951
955
|
if assistant_message.content == "Thinking...":
|
952
956
|
debug_log("First content received, clearing 'Thinking...'")
|
953
957
|
print("First content received, clearing 'Thinking...'")
|
954
|
-
|
955
|
-
|
958
|
+
# We'll let the MessageDisplay.update_content handle this special case
|
959
|
+
|
956
960
|
# Update the message object with the full content
|
957
961
|
assistant_message.content = content
|
958
962
|
|
959
|
-
# Update UI with the content
|
963
|
+
# Update UI with the content - this now has special handling for "Thinking..."
|
964
|
+
debug_log("Calling message_display.update_content")
|
960
965
|
await message_display.update_content(content)
|
961
966
|
|
962
|
-
#
|
967
|
+
# More aggressive UI refresh sequence
|
968
|
+
debug_log("Performing UI refresh sequence")
|
969
|
+
# First do a lightweight refresh
|
970
|
+
self.refresh(layout=False)
|
971
|
+
# Then scroll to end
|
972
|
+
messages_container.scroll_end(animate=False)
|
973
|
+
# Then do a full layout refresh
|
963
974
|
self.refresh(layout=True)
|
975
|
+
# Final scroll to ensure visibility
|
964
976
|
messages_container.scroll_end(animate=False)
|
965
977
|
|
966
978
|
except Exception as e:
|
@@ -1030,14 +1042,32 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
|
|
1030
1042
|
error = worker.error
|
1031
1043
|
debug_log(f"Error in generation worker: {error}")
|
1032
1044
|
log.error(f"Error in generation worker: {error}")
|
1033
|
-
|
1045
|
+
|
1046
|
+
# Sanitize error message for UI display
|
1047
|
+
error_str = str(error)
|
1048
|
+
|
1049
|
+
# Check if this is an Ollama error
|
1050
|
+
is_ollama_error = "ollama" in error_str.lower() or "404" in error_str
|
1051
|
+
|
1052
|
+
# Create a user-friendly error message
|
1053
|
+
if is_ollama_error:
|
1054
|
+
# For Ollama errors, provide a more user-friendly message
|
1055
|
+
user_error = "Unable to generate response. The selected model may not be available."
|
1056
|
+
debug_log(f"Sanitizing Ollama error to user-friendly message: {user_error}")
|
1057
|
+
# Show technical details only in notification, not in chat
|
1058
|
+
self.notify(f"Model error: {error_str}", severity="error", timeout=5)
|
1059
|
+
else:
|
1060
|
+
# For other errors, show a generic message
|
1061
|
+
user_error = f"Error generating response: {error_str}"
|
1062
|
+
self.notify(f"Generation error: {error_str}", severity="error", timeout=5)
|
1063
|
+
|
1034
1064
|
# Add error message to UI
|
1035
1065
|
if self.messages and self.messages[-1].role == "assistant":
|
1036
1066
|
debug_log("Removing thinking message")
|
1037
1067
|
self.messages.pop() # Remove thinking message
|
1038
|
-
|
1039
|
-
debug_log(f"Adding error message: {
|
1040
|
-
self.messages.append(Message(role="assistant", content=
|
1068
|
+
|
1069
|
+
debug_log(f"Adding error message: {user_error}")
|
1070
|
+
self.messages.append(Message(role="assistant", content=user_error))
|
1041
1071
|
await self.update_messages_ui()
|
1042
1072
|
|
1043
1073
|
elif worker.state == "success":
|
app/ui/chat_interface.py
CHANGED
@@ -121,11 +121,50 @@ class MessageDisplay(Static): # Inherit from Static instead of RichLog
|
|
121
121
|
|
122
122
|
async def update_content(self, content: str) -> None:
|
123
123
|
"""Update the message content using Static.update() with optimizations for streaming"""
|
124
|
+
# Use proper logging instead of print statements
|
125
|
+
import logging
|
126
|
+
logger = logging.getLogger(__name__)
|
127
|
+
logger.debug(f"MessageDisplay.update_content called with content length: {len(content)}")
|
128
|
+
|
124
129
|
# Quick unchanged content check to avoid unnecessary updates
|
125
130
|
if self.message.content == content:
|
131
|
+
logger.debug("Content unchanged, skipping update")
|
132
|
+
return
|
133
|
+
|
134
|
+
# Special handling for "Thinking..." to ensure it gets replaced
|
135
|
+
if self.message.content == "Thinking..." and content:
|
136
|
+
logger.debug("Replacing 'Thinking...' with actual content")
|
137
|
+
# Force a complete replacement rather than an append
|
138
|
+
self.message.content = ""
|
139
|
+
|
140
|
+
# Update the stored message object content
|
141
|
+
self.message.content = content
|
142
|
+
|
143
|
+
# Format with fixed-width placeholder to minimize layout shifts
|
144
|
+
formatted_content = self._format_content(content)
|
145
|
+
|
146
|
+
# Use a direct update that forces refresh - critical fix for streaming
|
147
|
+
self.update(formatted_content, refresh=True)
|
148
|
+
|
149
|
+
# Force app-level refresh and scroll to ensure visibility
|
150
|
+
try:
|
151
|
+
if self.app:
|
152
|
+
# Force a full layout refresh to ensure content is visible
|
153
|
+
self.app.refresh(layout=True)
|
154
|
+
|
155
|
+
# Find the messages container and scroll to end
|
156
|
+
containers = self.app.query("ScrollableContainer")
|
157
|
+
for container in containers:
|
158
|
+
if hasattr(container, 'scroll_end'):
|
159
|
+
container.scroll_end(animate=False)
|
160
|
+
except Exception as e:
|
161
|
+
logger.error(f"Error refreshing app: {str(e)}")
|
162
|
+
self.refresh(layout=True)
|
163
|
+
|
164
|
+
# Return early to avoid duplicate updates
|
126
165
|
return
|
127
166
|
|
128
|
-
# Update the stored message object content
|
167
|
+
# Update the stored message object content
|
129
168
|
self.message.content = content
|
130
169
|
|
131
170
|
# Format with fixed-width placeholder to minimize layout shifts
|
@@ -134,6 +173,7 @@ class MessageDisplay(Static): # Inherit from Static instead of RichLog
|
|
134
173
|
|
135
174
|
# Use a direct update that forces refresh - critical fix for streaming
|
136
175
|
# This ensures content is immediately visible
|
176
|
+
logger.debug(f"Updating widget with formatted content length: {len(formatted_content)}")
|
137
177
|
self.update(formatted_content, refresh=True)
|
138
178
|
|
139
179
|
# Force app-level refresh and scroll to ensure visibility
|
@@ -150,13 +190,18 @@ class MessageDisplay(Static): # Inherit from Static instead of RichLog
|
|
150
190
|
container.scroll_end(animate=False)
|
151
191
|
except Exception as e:
|
152
192
|
# Log the error and fallback to local refresh
|
153
|
-
|
193
|
+
logger.error(f"Error refreshing app: {str(e)}")
|
154
194
|
self.refresh(layout=True)
|
155
195
|
|
156
196
|
def _format_content(self, content: str) -> str:
|
157
197
|
"""Format message content with timestamp and handle markdown links"""
|
158
198
|
timestamp = datetime.now().strftime("%H:%M")
|
159
199
|
|
200
|
+
# Special handling for "Thinking..." to make it visually distinct
|
201
|
+
if content == "Thinking...":
|
202
|
+
# Use italic style for the thinking indicator
|
203
|
+
return f"[dim]{timestamp}[/dim] [italic]{content}[/italic]"
|
204
|
+
|
160
205
|
# Fix markdown-style links that cause markup errors
|
161
206
|
# Convert [text](url) to a safe format for Textual markup
|
162
207
|
content = re.sub(
|
@@ -170,8 +215,8 @@ class MessageDisplay(Static): # Inherit from Static instead of RichLog
|
|
170
215
|
# But keep our timestamp markup
|
171
216
|
timestamp_markup = f"[dim]{timestamp}[/dim]"
|
172
217
|
|
173
|
-
#
|
174
|
-
|
218
|
+
# Use proper logging instead of print
|
219
|
+
logger.debug(f"Formatting content: {len(content)} chars")
|
175
220
|
|
176
221
|
return f"{timestamp_markup} {content}"
|
177
222
|
|
app/utils.py
CHANGED
@@ -63,17 +63,62 @@ async def generate_conversation_title(message: str, model: str, client: Any) ->
|
|
63
63
|
|
64
64
|
# Check if client is OpenAI
|
65
65
|
is_openai = 'openai' in str(type(client)).lower()
|
66
|
-
if is_openai
|
66
|
+
if is_openai:
|
67
67
|
debug_log("Using OpenAI client for title generation")
|
68
68
|
# Use GPT-3.5 for title generation (fast and cost-effective)
|
69
69
|
title_model_id = "gpt-3.5-turbo"
|
70
70
|
debug_log(f"Using OpenAI model for title generation: {title_model_id}")
|
71
|
+
# For OpenAI, we'll always use their model, not fall back to the passed model
|
72
|
+
# This prevents trying to use Ollama models with OpenAI client
|
73
|
+
|
74
|
+
# Check if client is Ollama
|
75
|
+
is_ollama = 'ollama' in str(type(client)).lower()
|
76
|
+
if is_ollama and not title_model_id:
|
77
|
+
debug_log("Using Ollama client for title generation")
|
78
|
+
# For Ollama, check if the model exists before using it
|
79
|
+
try:
|
80
|
+
# Try a quick test request to check if model exists
|
81
|
+
debug_log(f"Testing if Ollama model exists: {model}")
|
82
|
+
import aiohttp
|
83
|
+
async with aiohttp.ClientSession() as session:
|
84
|
+
try:
|
85
|
+
base_url = "http://localhost:11434"
|
86
|
+
async with session.post(
|
87
|
+
f"{base_url}/api/generate",
|
88
|
+
json={"model": model, "prompt": "test", "stream": False},
|
89
|
+
timeout=2
|
90
|
+
) as response:
|
91
|
+
if response.status == 200:
|
92
|
+
# Model exists, use it
|
93
|
+
title_model_id = model
|
94
|
+
debug_log(f"Ollama model {model} exists, using it for title generation")
|
95
|
+
else:
|
96
|
+
debug_log(f"Ollama model {model} returned status {response.status}, falling back to default")
|
97
|
+
# Fall back to a common model
|
98
|
+
title_model_id = "llama3"
|
99
|
+
except Exception as e:
|
100
|
+
debug_log(f"Error testing Ollama model: {str(e)}, falling back to default")
|
101
|
+
# Fall back to a common model
|
102
|
+
title_model_id = "llama3"
|
103
|
+
except Exception as e:
|
104
|
+
debug_log(f"Error checking Ollama model: {str(e)}")
|
105
|
+
# Fall back to a common model
|
106
|
+
title_model_id = "llama3"
|
71
107
|
|
72
108
|
# Fallback logic if no specific model was found
|
73
109
|
if not title_model_id:
|
74
|
-
# Use
|
75
|
-
|
76
|
-
|
110
|
+
# Use a safe default based on client type
|
111
|
+
if is_openai:
|
112
|
+
title_model_id = "gpt-3.5-turbo"
|
113
|
+
elif is_anthropic:
|
114
|
+
title_model_id = "claude-3-haiku-20240307"
|
115
|
+
elif is_ollama:
|
116
|
+
title_model_id = "llama3" # Common default
|
117
|
+
else:
|
118
|
+
# Last resort - use the originally passed model
|
119
|
+
title_model_id = model
|
120
|
+
|
121
|
+
debug_log(f"No specific model found, using fallback model for title generation: {title_model_id}")
|
77
122
|
|
78
123
|
logger.info(f"Generating title for conversation using model: {title_model_id}")
|
79
124
|
debug_log(f"Final model selected for title generation: {title_model_id}")
|
@@ -325,25 +370,26 @@ async def generate_streaming_response(
|
|
325
370
|
full_response += new_content
|
326
371
|
debug_log(f"Updating UI with content length: {len(full_response)}")
|
327
372
|
|
328
|
-
#
|
329
|
-
|
330
|
-
|
331
|
-
|
373
|
+
# Enhanced debug logging
|
374
|
+
print(f"STREAM DEBUG: +{len(new_content)} chars, total: {len(full_response)}")
|
375
|
+
# Print first few characters of content for debugging
|
376
|
+
if len(full_response) < 100:
|
377
|
+
print(f"STREAM CONTENT: '{full_response}'")
|
332
378
|
|
333
379
|
try:
|
334
380
|
# Call the UI callback with the full response so far
|
381
|
+
debug_log("Calling UI callback with content")
|
335
382
|
await callback(full_response)
|
336
383
|
debug_log("UI callback completed successfully")
|
337
384
|
|
338
385
|
# Force app refresh after each update
|
339
386
|
if hasattr(app, 'refresh'):
|
387
|
+
debug_log("Forcing app refresh")
|
340
388
|
app.refresh(layout=True) # Force layout refresh
|
341
389
|
except Exception as callback_err:
|
342
390
|
debug_log(f"Error in UI callback: {str(callback_err)}")
|
343
391
|
logger.error(f"Error in UI callback: {str(callback_err)}")
|
344
|
-
|
345
|
-
if not is_openai:
|
346
|
-
print(f"Error updating UI: {str(callback_err)}")
|
392
|
+
print(f"STREAM ERROR: Error updating UI: {str(callback_err)}")
|
347
393
|
|
348
394
|
buffer = []
|
349
395
|
last_update = current_time
|
@@ -509,6 +555,23 @@ def resolve_model_id(model_id_or_name: str) -> str:
|
|
509
555
|
input_lower = model_id_or_name.lower().strip()
|
510
556
|
logger.info(f"Attempting to resolve model identifier: '{input_lower}'")
|
511
557
|
|
558
|
+
# Special case handling for common typos and model name variations
|
559
|
+
typo_corrections = {
|
560
|
+
"o4-mini": "04-mini",
|
561
|
+
"o1": "01",
|
562
|
+
"o1-mini": "01-mini",
|
563
|
+
"o1-preview": "01-preview",
|
564
|
+
"o4": "04",
|
565
|
+
"o4-preview": "04-preview",
|
566
|
+
"o4-vision": "04-vision"
|
567
|
+
}
|
568
|
+
|
569
|
+
if input_lower in typo_corrections:
|
570
|
+
corrected = typo_corrections[input_lower]
|
571
|
+
logger.info(f"Converting '{input_lower}' to '{corrected}' (letter 'o' to zero '0')")
|
572
|
+
input_lower = corrected
|
573
|
+
model_id_or_name = corrected
|
574
|
+
|
512
575
|
# First, check if this is an OpenAI model - if so, return as-is to ensure correct provider
|
513
576
|
if any(name in input_lower for name in ["gpt", "text-", "davinci"]):
|
514
577
|
logger.info(f"Input '{input_lower}' appears to be an OpenAI model, returning as-is")
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: chat-console
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.9
|
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=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,,
|
@@ -1,24 +0,0 @@
|
|
1
|
-
app/__init__.py,sha256=ZSZR6xIuPhvv1zB4p63eSeGQX8bTkhxBWk2Gn0peFaw,130
|
2
|
-
app/config.py,sha256=KawltE7cK2bR9wbe1NSlepwWIjkiFw2bg3vbLmUnP38,7626
|
3
|
-
app/database.py,sha256=nt8CVuDpy6zw8mOYqDcfUmNw611t7Ln7pz22M0b6-MI,9967
|
4
|
-
app/main.py,sha256=clcRjXwySxVjrPtqvPOIfl7r8KbHVLZ1woxyEnvl3JI,72829
|
5
|
-
app/models.py,sha256=4-y9Lytay2exWPFi0FDlVeRL3K2-I7E-jBqNzTfokqY,2644
|
6
|
-
app/utils.py,sha256=htktBl1JucYEHo1WBrWkfdip4yzRtvyVl24Aaj445xA,32421
|
7
|
-
app/api/__init__.py,sha256=A8UL84ldYlv8l7O-yKzraVFcfww86SgWfpl4p7R03-w,62
|
8
|
-
app/api/anthropic.py,sha256=UpIP3CgAOUimdVyif41MhBOCAgOyFO8mX9SFQMKRAmc,12483
|
9
|
-
app/api/base.py,sha256=eShCiZIcW3yeZLONt1xnkP0vU6v5MEaDj3YZ3xcPle8,7294
|
10
|
-
app/api/ollama.py,sha256=EBEEKXbgAYWEg_zF5PO_UKO5l_aoU3J_7tfCj9e-fqs,61699
|
11
|
-
app/api/openai.py,sha256=6ORruzuuZtIjME3WK-g7kXf7cBmM4td5Njv9JLaWh7E,9557
|
12
|
-
app/ui/__init__.py,sha256=RndfbQ1Tv47qdSiuQzvWP96lPS547SDaGE-BgOtiP_w,55
|
13
|
-
app/ui/chat_interface.py,sha256=TJlMzVmrKzr3t0JIhto0vKBvyik7gJ7UEyW3Vqbn3cE,17262
|
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.7.dist-info/licenses/LICENSE,sha256=srHZ3fvcAuZY1LHxE7P6XWju2njRCHyK6h_ftEbzxSE,1057
|
20
|
-
chat_console-0.3.7.dist-info/METADATA,sha256=eDQRUghh8Ihp8z38oAlI0___RBBDJHpLmhBGF0VgZ1w,2921
|
21
|
-
chat_console-0.3.7.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
|
22
|
-
chat_console-0.3.7.dist-info/entry_points.txt,sha256=kkVdEc22U9PAi2AeruoKklfkng_a_aHAP6VRVwrAD7c,67
|
23
|
-
chat_console-0.3.7.dist-info/top_level.txt,sha256=io9g7LCbfmTG1SFKgEOGXmCFB9uMP2H5lerm0HiHWQE,4
|
24
|
-
chat_console-0.3.7.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|