stratifyai 0.1.1__py3-none-any.whl → 0.1.3__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.
- api/__init__.py +3 -0
- api/main.py +763 -0
- api/static/index.html +1126 -0
- api/static/models.html +567 -0
- api/static/stratifyai_trans_logo.png +0 -0
- api/static/stratifyai_wide_logo.png +0 -0
- api/static/stratum_logo.png +0 -0
- cli/stratifyai_cli.py +574 -73
- stratifyai/api_key_helper.py +1 -1
- stratifyai/config.py +158 -24
- stratifyai/models.py +36 -1
- stratifyai/providers/anthropic.py +65 -5
- stratifyai/providers/bedrock.py +96 -9
- stratifyai/providers/grok.py +3 -2
- stratifyai/providers/openai.py +63 -8
- stratifyai/providers/openai_compatible.py +79 -7
- stratifyai/router.py +2 -2
- stratifyai/summarization.py +147 -3
- stratifyai/utils/model_selector.py +3 -3
- stratifyai/utils/provider_validator.py +4 -2
- {stratifyai-0.1.1.dist-info → stratifyai-0.1.3.dist-info}/METADATA +9 -5
- {stratifyai-0.1.1.dist-info → stratifyai-0.1.3.dist-info}/RECORD +26 -19
- {stratifyai-0.1.1.dist-info → stratifyai-0.1.3.dist-info}/top_level.txt +1 -0
- {stratifyai-0.1.1.dist-info → stratifyai-0.1.3.dist-info}/WHEEL +0 -0
- {stratifyai-0.1.1.dist-info → stratifyai-0.1.3.dist-info}/entry_points.txt +0 -0
- {stratifyai-0.1.1.dist-info → stratifyai-0.1.3.dist-info}/licenses/LICENSE +0 -0
cli/stratifyai_cli.py
CHANGED
|
@@ -32,6 +32,21 @@ app = typer.Typer(
|
|
|
32
32
|
)
|
|
33
33
|
console = Console()
|
|
34
34
|
|
|
35
|
+
# Mode-specific colors and icons
|
|
36
|
+
CHAT_COLOR = "magenta"
|
|
37
|
+
CHAT_ACCENT = "bold magenta"
|
|
38
|
+
CHAT_ICON = "💬"
|
|
39
|
+
INTERACTIVE_COLOR = "cyan"
|
|
40
|
+
INTERACTIVE_ACCENT = "bold cyan"
|
|
41
|
+
INTERACTIVE_ICON = "⚡"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def mode_prompt(text: str, mode: str = "chat") -> str:
|
|
45
|
+
"""Add mode icon prefix to prompt text."""
|
|
46
|
+
icon = CHAT_ICON if mode == "chat" else INTERACTIVE_ICON
|
|
47
|
+
color = CHAT_ACCENT if mode == "chat" else INTERACTIVE_ACCENT
|
|
48
|
+
return f"[{color}]{icon}[/{color}] {text}"
|
|
49
|
+
|
|
35
50
|
|
|
36
51
|
@app.command()
|
|
37
52
|
def chat(
|
|
@@ -122,6 +137,10 @@ def _chat_impl(
|
|
|
122
137
|
_conversation_history: Optional[List[Message]] = None,
|
|
123
138
|
):
|
|
124
139
|
"""Internal implementation of chat with conversation history support."""
|
|
140
|
+
# Show mode banner
|
|
141
|
+
console.print(f"\n[{CHAT_ACCENT}]─── 💬 CHAT MODE ───[/{CHAT_ACCENT}]")
|
|
142
|
+
console.print(f"[dim]Single message mode - use 'interactive' for conversations[/dim]\n")
|
|
143
|
+
|
|
125
144
|
try:
|
|
126
145
|
# Auto-select model based on file type if enabled
|
|
127
146
|
if auto_select and file and not (provider and model):
|
|
@@ -152,7 +171,7 @@ def _chat_impl(
|
|
|
152
171
|
# Retry loop for provider selection
|
|
153
172
|
max_attempts = 3
|
|
154
173
|
for attempt in range(max_attempts):
|
|
155
|
-
provider_choice = Prompt.ask("
|
|
174
|
+
provider_choice = Prompt.ask(mode_prompt("Choose provider", "chat"), default="1")
|
|
156
175
|
|
|
157
176
|
try:
|
|
158
177
|
provider_idx = int(provider_choice) - 1
|
|
@@ -174,25 +193,70 @@ def _chat_impl(
|
|
|
174
193
|
console.print("[yellow]Too many invalid attempts. Using default: openai[/yellow]")
|
|
175
194
|
provider = "openai"
|
|
176
195
|
|
|
196
|
+
# Model selection with vision validation loop
|
|
197
|
+
need_vision_model = file and file.suffix.lower() in {'.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'} if file else False
|
|
198
|
+
|
|
177
199
|
if not model:
|
|
178
200
|
prompted_for_model = True
|
|
179
|
-
#
|
|
201
|
+
# Validate and display models for selected provider
|
|
202
|
+
from stratifyai.utils.provider_validator import get_validated_interactive_models
|
|
203
|
+
|
|
180
204
|
if provider in MODEL_CATALOG:
|
|
181
|
-
|
|
182
|
-
|
|
205
|
+
# Show spinner while validating
|
|
206
|
+
with console.status(f"[cyan]Validating {provider} models...", spinner="dots"):
|
|
207
|
+
validation_data = get_validated_interactive_models(provider)
|
|
208
|
+
|
|
209
|
+
validation_result = validation_data["validation_result"]
|
|
210
|
+
validated_models = validation_data["models"]
|
|
211
|
+
|
|
212
|
+
# Show validation result
|
|
213
|
+
if validation_result["error"]:
|
|
214
|
+
console.print("[yellow]⚠ Default models displayed. Could not validate models.[/yellow]")
|
|
215
|
+
# Fall back to MODEL_CATALOG if validation fails
|
|
216
|
+
available_models = list(MODEL_CATALOG[provider].keys())
|
|
217
|
+
model_metadata = MODEL_CATALOG[provider]
|
|
218
|
+
else:
|
|
219
|
+
console.print(f"[green]✓ Validated {len(validated_models)} models[/green] [dim]({validation_result['validation_time_ms']}ms)[/dim]")
|
|
220
|
+
available_models = list(validated_models.keys())
|
|
221
|
+
model_metadata = validated_models
|
|
222
|
+
|
|
223
|
+
# Filter for vision models if image file provided
|
|
224
|
+
if need_vision_model:
|
|
225
|
+
vision_models = [m for m in available_models if model_metadata.get(m, {}).get("supports_vision", False)]
|
|
226
|
+
if vision_models:
|
|
227
|
+
console.print(f"\n[bold cyan]Vision-capable models for {provider}:[/bold cyan]")
|
|
228
|
+
console.print("[dim](Filtered for image file support)[/dim]")
|
|
229
|
+
available_models = vision_models
|
|
230
|
+
else:
|
|
231
|
+
console.print(f"\n[yellow]⚠ No vision-capable models available for {provider}[/yellow]")
|
|
232
|
+
console.print("[yellow]Please select a different provider or remove the image file[/yellow]")
|
|
233
|
+
raise typer.Exit(1)
|
|
234
|
+
else:
|
|
235
|
+
console.print(f"\n[bold cyan]Available {provider} models:[/bold cyan]")
|
|
236
|
+
|
|
237
|
+
# Display with friendly names, descriptions, and categories (same as interactive mode)
|
|
238
|
+
current_category = None
|
|
183
239
|
for i, m in enumerate(available_models, 1):
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
240
|
+
meta = model_metadata.get(m, {})
|
|
241
|
+
display_name = meta.get("display_name", m)
|
|
242
|
+
description = meta.get("description", "")
|
|
243
|
+
category = meta.get("category", "")
|
|
244
|
+
|
|
245
|
+
# Show category header if changed
|
|
246
|
+
if category and category != current_category:
|
|
247
|
+
console.print(f" [dim]── {category} ──[/dim]")
|
|
248
|
+
current_category = category
|
|
249
|
+
|
|
250
|
+
label = f" {i}. {display_name}"
|
|
251
|
+
if description:
|
|
252
|
+
label += f" [dim]- {description}[/dim]"
|
|
189
253
|
console.print(label)
|
|
190
|
-
|
|
254
|
+
|
|
191
255
|
# Retry loop for model selection
|
|
192
256
|
max_attempts = 3
|
|
193
257
|
model = None
|
|
194
258
|
for attempt in range(max_attempts):
|
|
195
|
-
model_choice = Prompt.ask("
|
|
259
|
+
model_choice = Prompt.ask(mode_prompt("Select model", "chat"))
|
|
196
260
|
|
|
197
261
|
try:
|
|
198
262
|
model_idx = int(model_choice) - 1
|
|
@@ -230,7 +294,7 @@ def _chat_impl(
|
|
|
230
294
|
temperature = None
|
|
231
295
|
for attempt in range(max_attempts):
|
|
232
296
|
temp_input = Prompt.ask(
|
|
233
|
-
"
|
|
297
|
+
mode_prompt("Temperature (0.0-2.0)", "chat"),
|
|
234
298
|
default="0.7"
|
|
235
299
|
)
|
|
236
300
|
|
|
@@ -260,20 +324,114 @@ def _chat_impl(
|
|
|
260
324
|
console.print(f"[dim]Attach a file to include its content in your message[/dim]")
|
|
261
325
|
console.print(f"[dim]Max file size: 5 MB | Leave blank to skip[/dim]")
|
|
262
326
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
327
|
+
# File prompt with retry loop
|
|
328
|
+
max_file_attempts = 3
|
|
329
|
+
file = None
|
|
330
|
+
for file_attempt in range(max_file_attempts):
|
|
331
|
+
file_path_input = Prompt.ask(mode_prompt("File path (or Enter to skip)", "chat"), default="")
|
|
332
|
+
|
|
333
|
+
if not file_path_input.strip():
|
|
334
|
+
# User pressed Enter to skip
|
|
335
|
+
break
|
|
336
|
+
|
|
266
337
|
file = Path(file_path_input.strip()).expanduser()
|
|
338
|
+
|
|
339
|
+
# Validate file exists and is readable
|
|
340
|
+
if not file.exists():
|
|
341
|
+
console.print(f"[red]✗ File not found: {file}[/red]")
|
|
342
|
+
if file_attempt < max_file_attempts - 1:
|
|
343
|
+
choice = Prompt.ask(
|
|
344
|
+
"[cyan]Enter 1 to retry file path or 2 to continue without file[/cyan]",
|
|
345
|
+
choices=["1", "2"],
|
|
346
|
+
default="2"
|
|
347
|
+
)
|
|
348
|
+
if choice == "2":
|
|
349
|
+
file = None
|
|
350
|
+
break
|
|
351
|
+
# Otherwise loop continues for retry
|
|
352
|
+
else:
|
|
353
|
+
console.print("[dim]Continuing without file attachment[/dim]")
|
|
354
|
+
file = None
|
|
355
|
+
elif not file.is_file():
|
|
356
|
+
console.print(f"[red]✗ Path is not a file: {file}[/red]")
|
|
357
|
+
if file_attempt < max_file_attempts - 1:
|
|
358
|
+
choice = Prompt.ask(
|
|
359
|
+
"[cyan]Enter 1 to retry file path or 2 to continue without file[/cyan]",
|
|
360
|
+
choices=["1", "2"],
|
|
361
|
+
default="2"
|
|
362
|
+
)
|
|
363
|
+
if choice == "2":
|
|
364
|
+
file = None
|
|
365
|
+
break
|
|
366
|
+
else:
|
|
367
|
+
console.print("[dim]Continuing without file attachment[/dim]")
|
|
368
|
+
file = None
|
|
369
|
+
else:
|
|
370
|
+
# File is valid, break out of retry loop
|
|
371
|
+
break
|
|
267
372
|
|
|
268
373
|
# Load content from file if provided
|
|
269
374
|
file_content = None
|
|
270
375
|
if file:
|
|
271
376
|
try:
|
|
272
|
-
|
|
273
|
-
|
|
377
|
+
# Check if file is an image
|
|
378
|
+
image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'}
|
|
379
|
+
is_image = file.suffix.lower() in image_extensions
|
|
274
380
|
|
|
275
|
-
|
|
276
|
-
|
|
381
|
+
if is_image:
|
|
382
|
+
# For image files, check if model supports vision
|
|
383
|
+
model_info = MODEL_CATALOG.get(provider, {}).get(model, {})
|
|
384
|
+
supports_vision = model_info.get("supports_vision", False)
|
|
385
|
+
|
|
386
|
+
if not supports_vision:
|
|
387
|
+
console.print(f"\n[red]✗ Vision not supported: {model} cannot process image files[/red]")
|
|
388
|
+
console.print("[yellow]⚠️ This model cannot process images. Please select a vision-capable model.[/yellow]")
|
|
389
|
+
console.print("\n[cyan]Returning to model selection...[/cyan]\n")
|
|
390
|
+
|
|
391
|
+
# Return to model selection - call chat command recursively with vision-required flag
|
|
392
|
+
# Pass message=None to force prompting for message after model selection
|
|
393
|
+
import sys
|
|
394
|
+
sys.argv = ['stratifyai', 'chat', '--provider', provider, '--file', str(file)]
|
|
395
|
+
if system:
|
|
396
|
+
sys.argv.extend(['--system', system])
|
|
397
|
+
if max_tokens:
|
|
398
|
+
sys.argv.extend(['--max-tokens', str(max_tokens)])
|
|
399
|
+
chat(provider=provider, model=None, message=None, system=system,
|
|
400
|
+
temperature=temperature, max_tokens=max_tokens, file=file,
|
|
401
|
+
stream=stream, cache_control=cache_control, chunked=chunked,
|
|
402
|
+
chunk_size=chunk_size, auto_select=auto_select)
|
|
403
|
+
return
|
|
404
|
+
|
|
405
|
+
# Read image as base64
|
|
406
|
+
import base64
|
|
407
|
+
with open(file, 'rb') as f:
|
|
408
|
+
image_data = base64.b64encode(f.read()).decode('utf-8')
|
|
409
|
+
|
|
410
|
+
# Get file size
|
|
411
|
+
file_size = file.stat().st_size
|
|
412
|
+
file_size_kb = file_size / 1024
|
|
413
|
+
file_size_mb = file_size / (1024 * 1024)
|
|
414
|
+
|
|
415
|
+
size_str = f"{file_size_kb:.1f} KB" if file_size_kb < 1024 else f"{file_size_mb:.2f} MB"
|
|
416
|
+
console.print(f"[green]✓ Loaded {file.name}[/green] [dim]({size_str}, image)[/dim]")
|
|
417
|
+
|
|
418
|
+
# Return base64 image data with metadata
|
|
419
|
+
mime_type = {
|
|
420
|
+
'.jpg': 'image/jpeg',
|
|
421
|
+
'.jpeg': 'image/jpeg',
|
|
422
|
+
'.png': 'image/png',
|
|
423
|
+
'.gif': 'image/gif',
|
|
424
|
+
'.webp': 'image/webp',
|
|
425
|
+
'.bmp': 'image/bmp'
|
|
426
|
+
}.get(file.suffix.lower(), 'image/jpeg')
|
|
427
|
+
|
|
428
|
+
file_content = f"[IMAGE:{mime_type}]\n{image_data}"
|
|
429
|
+
else:
|
|
430
|
+
# Read text file
|
|
431
|
+
with open(file, 'r', encoding='utf-8') as f:
|
|
432
|
+
file_content = f.read()
|
|
433
|
+
|
|
434
|
+
# Get file size for display
|
|
277
435
|
if isinstance(file, Path) and file.exists():
|
|
278
436
|
file_size = file.stat().st_size
|
|
279
437
|
file_size_mb = file_size / (1024 * 1024)
|
|
@@ -298,19 +456,19 @@ def _chat_impl(
|
|
|
298
456
|
|
|
299
457
|
if analysis.warning:
|
|
300
458
|
console.print(f"[yellow]⚠ {analysis.warning}[/yellow]")
|
|
301
|
-
else:
|
|
302
|
-
# Fallback for tests or non-Path objects
|
|
303
|
-
console.print(f"[dim]Loaded content from {file} ({len(file_content)} chars)[/dim]")
|
|
304
|
-
except:
|
|
305
|
-
# Fallback if stat fails (e.g., in test environments)
|
|
306
|
-
console.print(f"[dim]Loaded content from {file} ({len(file_content)} chars)[/dim]")
|
|
307
459
|
except Exception as e:
|
|
308
460
|
console.print(f"[red]Error reading file {file}: {e}[/red]")
|
|
309
461
|
raise typer.Exit(1)
|
|
310
462
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
463
|
+
# Prompt for message if not provided
|
|
464
|
+
# For image files, always prompt (user needs to provide instructions for the image)
|
|
465
|
+
# For text files, only prompt if no file content
|
|
466
|
+
is_image_file = file and file.suffix.lower() in {'.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'} if file else False
|
|
467
|
+
|
|
468
|
+
if not message:
|
|
469
|
+
if is_image_file or not file_content:
|
|
470
|
+
console.print(f"\n[{CHAT_ACCENT}]Enter your message:[/{CHAT_ACCENT}]")
|
|
471
|
+
message = Prompt.ask(mode_prompt("Message", "chat"))
|
|
314
472
|
|
|
315
473
|
# Build messages - use conversation history if this is a follow-up
|
|
316
474
|
if _conversation_history is None:
|
|
@@ -393,8 +551,8 @@ def _chat_impl(
|
|
|
393
551
|
response = client.chat_completion_sync(request)
|
|
394
552
|
response_content = response.content
|
|
395
553
|
|
|
396
|
-
# Display metadata before response
|
|
397
|
-
console.print(f"\n[bold]Provider:[/bold] [
|
|
554
|
+
# Display metadata before response (chat mode - magenta)
|
|
555
|
+
console.print(f"\n[bold]Provider:[/bold] [{CHAT_COLOR}]{provider}[/{CHAT_COLOR}] | [bold]Model:[/bold] [{CHAT_COLOR}]{model}[/{CHAT_COLOR}]")
|
|
398
556
|
|
|
399
557
|
# Build usage line with token breakdown and cache info
|
|
400
558
|
usage_parts = [
|
|
@@ -419,15 +577,15 @@ def _chat_impl(
|
|
|
419
577
|
|
|
420
578
|
console.print(f"[dim]{' | '.join(usage_parts)}[/dim]")
|
|
421
579
|
|
|
422
|
-
# Print response with
|
|
423
|
-
console.print(f"\n{response_content}", style=
|
|
580
|
+
# Print response with chat mode color (magenta)
|
|
581
|
+
console.print(f"\n{response_content}", style=CHAT_COLOR)
|
|
424
582
|
|
|
425
583
|
# Add assistant response to history for multi-turn conversation
|
|
426
584
|
messages.append(Message(role="assistant", content=response_content))
|
|
427
585
|
|
|
428
586
|
# Ask what to do next
|
|
429
587
|
console.print("\n[dim]Options: [1] Continue conversation [2] Save & continue [3] Save & exit [4] Exit[/dim]")
|
|
430
|
-
next_action = Prompt.ask("What would you like to do?", choices=["1", "2", "3", "4"], default="1")
|
|
588
|
+
next_action = Prompt.ask(mode_prompt("What would you like to do?", "chat"), choices=["1", "2", "3", "4"], default="1")
|
|
431
589
|
|
|
432
590
|
# Handle save requests
|
|
433
591
|
if next_action in ["2", "3"]:
|
|
@@ -725,14 +883,200 @@ def interactive(
|
|
|
725
883
|
|
|
726
884
|
try:
|
|
727
885
|
# Helper function to load file with size validation and intelligent extraction
|
|
728
|
-
def load_file_content(file_path: Path, warn_large: bool = True) -> Optional[str]:
|
|
729
|
-
"""Load file content with size restrictions, warnings, and intelligent extraction.
|
|
886
|
+
def load_file_content(file_path: Path, warn_large: bool = True, check_vision: bool = False) -> Optional[str]:
|
|
887
|
+
"""Load file content with size restrictions, warnings, and intelligent extraction.
|
|
888
|
+
|
|
889
|
+
Args:
|
|
890
|
+
file_path: Path to the file to load
|
|
891
|
+
warn_large: Whether to warn about large files
|
|
892
|
+
check_vision: Whether to check for vision support for image files
|
|
893
|
+
"""
|
|
894
|
+
# Declare nonlocal variables at the top before any use
|
|
895
|
+
nonlocal model, client, temperature
|
|
896
|
+
|
|
730
897
|
try:
|
|
731
898
|
# Check if file exists
|
|
732
899
|
if not file_path.exists():
|
|
733
900
|
console.print(f"[red]✗ File not found: {file_path}[/red]")
|
|
734
901
|
return None
|
|
735
902
|
|
|
903
|
+
# Check if file is an image
|
|
904
|
+
image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'}
|
|
905
|
+
is_image = file_path.suffix.lower() in image_extensions
|
|
906
|
+
|
|
907
|
+
if is_image:
|
|
908
|
+
# For image files, check if model supports vision
|
|
909
|
+
if check_vision:
|
|
910
|
+
model_info = MODEL_CATALOG.get(provider, {}).get(model, {})
|
|
911
|
+
supports_vision = model_info.get("supports_vision", False)
|
|
912
|
+
|
|
913
|
+
if not supports_vision:
|
|
914
|
+
console.print(f"\n[red]✗ Vision not supported: {model} cannot process image files[/red]")
|
|
915
|
+
console.print("[yellow]⚠️ This model cannot process images. Switching to vision-capable model...[/yellow]")
|
|
916
|
+
|
|
917
|
+
# Offer to switch to a vision model
|
|
918
|
+
choice = Prompt.ask(
|
|
919
|
+
"[cyan]Enter 1 to select a vision-capable model or 2 to continue without image[/cyan]",
|
|
920
|
+
choices=["1", "2"],
|
|
921
|
+
default="1"
|
|
922
|
+
)
|
|
923
|
+
|
|
924
|
+
if choice == "1":
|
|
925
|
+
# Show vision-capable models for current provider (like chat mode)
|
|
926
|
+
from stratifyai.utils.provider_validator import get_validated_interactive_models
|
|
927
|
+
|
|
928
|
+
# Validate and get models
|
|
929
|
+
with console.status(f"[cyan]Validating {provider} models...", spinner="dots"):
|
|
930
|
+
validation_data = get_validated_interactive_models(provider)
|
|
931
|
+
|
|
932
|
+
validation_result = validation_data["validation_result"]
|
|
933
|
+
validated_models = validation_data["models"]
|
|
934
|
+
|
|
935
|
+
# Get model metadata
|
|
936
|
+
if validation_result["error"]:
|
|
937
|
+
available_models = list(MODEL_CATALOG[provider].keys())
|
|
938
|
+
model_metadata = MODEL_CATALOG[provider]
|
|
939
|
+
else:
|
|
940
|
+
available_models = list(validated_models.keys())
|
|
941
|
+
model_metadata = validated_models
|
|
942
|
+
|
|
943
|
+
# Filter for vision models
|
|
944
|
+
vision_models = [m for m in available_models if model_metadata.get(m, {}).get("supports_vision", False)]
|
|
945
|
+
|
|
946
|
+
if not vision_models:
|
|
947
|
+
console.print(f"\n[yellow]⚠ No vision-capable models available for {provider}[/yellow]")
|
|
948
|
+
console.print("[yellow]Please use /provider to select a different provider[/yellow]\n")
|
|
949
|
+
return None
|
|
950
|
+
|
|
951
|
+
# Show vision models
|
|
952
|
+
console.print(f"\n[bold cyan]Vision-capable models for {provider}:[/bold cyan]")
|
|
953
|
+
console.print("[dim](Filtered for image file support)[/dim]")
|
|
954
|
+
|
|
955
|
+
# Display with categories and descriptions
|
|
956
|
+
current_category = None
|
|
957
|
+
for i, m in enumerate(vision_models, 1):
|
|
958
|
+
meta = model_metadata.get(m, {})
|
|
959
|
+
display_name = meta.get("display_name", m)
|
|
960
|
+
description = meta.get("description", "")
|
|
961
|
+
category = meta.get("category", "")
|
|
962
|
+
|
|
963
|
+
# Show category header if changed
|
|
964
|
+
if category and category != current_category:
|
|
965
|
+
console.print(f" [dim]── {category} ──[/dim]")
|
|
966
|
+
current_category = category
|
|
967
|
+
|
|
968
|
+
current_marker = " [green](current)[/green]" if m == model else ""
|
|
969
|
+
label = f" {i}. {display_name}{current_marker}"
|
|
970
|
+
if description:
|
|
971
|
+
label += f" [dim]- {description}[/dim]"
|
|
972
|
+
console.print(label)
|
|
973
|
+
|
|
974
|
+
# Get model selection
|
|
975
|
+
max_attempts = 3
|
|
976
|
+
new_model = None
|
|
977
|
+
for attempt in range(max_attempts):
|
|
978
|
+
model_choice = Prompt.ask("\nSelect vision model")
|
|
979
|
+
try:
|
|
980
|
+
model_idx = int(model_choice) - 1
|
|
981
|
+
if 0 <= model_idx < len(vision_models):
|
|
982
|
+
new_model = vision_models[model_idx]
|
|
983
|
+
break
|
|
984
|
+
else:
|
|
985
|
+
console.print(f"[red]✗ Invalid number.[/red] Please enter a number between 1 and {len(vision_models)}")
|
|
986
|
+
if attempt < max_attempts - 1:
|
|
987
|
+
console.print("[dim]Try again...[/dim]")
|
|
988
|
+
except ValueError:
|
|
989
|
+
console.print(f"[red]✗ Invalid input.[/red] Please enter a number")
|
|
990
|
+
if attempt < max_attempts - 1:
|
|
991
|
+
console.print("[dim]Try again...[/dim]")
|
|
992
|
+
|
|
993
|
+
if new_model:
|
|
994
|
+
# Update the outer scope model variable
|
|
995
|
+
model = new_model
|
|
996
|
+
|
|
997
|
+
# Check if new model has fixed temperature and prompt if needed
|
|
998
|
+
new_model_info = MODEL_CATALOG.get(provider, {}).get(model, {})
|
|
999
|
+
fixed_temp = new_model_info.get("fixed_temperature")
|
|
1000
|
+
|
|
1001
|
+
if fixed_temp is not None:
|
|
1002
|
+
temperature = fixed_temp
|
|
1003
|
+
console.print(f"\n[dim]Using fixed temperature: {fixed_temp} for this model[/dim]")
|
|
1004
|
+
else:
|
|
1005
|
+
# Retry loop for temperature input
|
|
1006
|
+
max_temp_attempts = 3
|
|
1007
|
+
temperature = None
|
|
1008
|
+
for temp_attempt in range(max_temp_attempts):
|
|
1009
|
+
temp_input = Prompt.ask(
|
|
1010
|
+
"\n[bold cyan]Temperature[/bold cyan] (0.0-2.0, default 0.7)",
|
|
1011
|
+
default="0.7"
|
|
1012
|
+
)
|
|
1013
|
+
|
|
1014
|
+
try:
|
|
1015
|
+
temp_value = float(temp_input)
|
|
1016
|
+
if 0.0 <= temp_value <= 2.0:
|
|
1017
|
+
temperature = temp_value
|
|
1018
|
+
break
|
|
1019
|
+
else:
|
|
1020
|
+
console.print("[red]✗ Out of range.[/red] Temperature must be between 0.0 and 2.0")
|
|
1021
|
+
if temp_attempt < max_temp_attempts - 1:
|
|
1022
|
+
console.print("[dim]Try again...[/dim]")
|
|
1023
|
+
except ValueError:
|
|
1024
|
+
console.print(f"[red]✗ Invalid input.[/red] Please enter a number (e.g., '0.7' not '{temp_input}')")
|
|
1025
|
+
if temp_attempt < max_temp_attempts - 1:
|
|
1026
|
+
console.print("[dim]Try again...[/dim]")
|
|
1027
|
+
|
|
1028
|
+
# If still no valid temperature after retries, use default
|
|
1029
|
+
if temperature is None:
|
|
1030
|
+
console.print("[yellow]Too many invalid attempts. Using default: 0.7[/yellow]")
|
|
1031
|
+
temperature = 0.7
|
|
1032
|
+
|
|
1033
|
+
# Reinitialize client with new model
|
|
1034
|
+
client = LLMClient(provider=provider)
|
|
1035
|
+
|
|
1036
|
+
# Update context window info
|
|
1037
|
+
context_window = new_model_info.get("context", "N/A")
|
|
1038
|
+
|
|
1039
|
+
console.print(f"\n[green]✓ Switched to:[/green] [cyan]{model}[/cyan] | [dim]Context: {context_window:,} tokens[/dim]")
|
|
1040
|
+
|
|
1041
|
+
# Now try loading the image again (recursively call with updated model)
|
|
1042
|
+
return load_file_content(file_path, warn_large, check_vision=True)
|
|
1043
|
+
else:
|
|
1044
|
+
console.print("[yellow]Model not changed[/yellow]")
|
|
1045
|
+
|
|
1046
|
+
return None
|
|
1047
|
+
|
|
1048
|
+
# Read image as base64
|
|
1049
|
+
import base64
|
|
1050
|
+
with open(file_path, 'rb') as f:
|
|
1051
|
+
image_data = base64.b64encode(f.read()).decode('utf-8')
|
|
1052
|
+
|
|
1053
|
+
# Get file size
|
|
1054
|
+
file_size = file_path.stat().st_size
|
|
1055
|
+
file_size_mb = file_size / (1024 * 1024)
|
|
1056
|
+
file_size_kb = file_size / 1024
|
|
1057
|
+
|
|
1058
|
+
# Check size limit
|
|
1059
|
+
if file_size > MAX_FILE_SIZE_BYTES:
|
|
1060
|
+
console.print(f"[red]✗ Image too large: {file_size_mb:.2f} MB (max {MAX_FILE_SIZE_MB} MB)[/red]")
|
|
1061
|
+
return None
|
|
1062
|
+
|
|
1063
|
+
size_str = f"{file_size_kb:.1f} KB" if file_size_kb < 1024 else f"{file_size_mb:.2f} MB"
|
|
1064
|
+
console.print(f"[green]✓ Loaded {file_path.name}[/green] [dim]({size_str}, image)[/dim]")
|
|
1065
|
+
|
|
1066
|
+
# Return base64 image data with metadata
|
|
1067
|
+
mime_type = {
|
|
1068
|
+
'.jpg': 'image/jpeg',
|
|
1069
|
+
'.jpeg': 'image/jpeg',
|
|
1070
|
+
'.png': 'image/png',
|
|
1071
|
+
'.gif': 'image/gif',
|
|
1072
|
+
'.webp': 'image/webp',
|
|
1073
|
+
'.bmp': 'image/bmp'
|
|
1074
|
+
}.get(file_path.suffix.lower(), 'image/jpeg')
|
|
1075
|
+
|
|
1076
|
+
return f"[IMAGE:{mime_type}]\n{image_data}"
|
|
1077
|
+
|
|
1078
|
+
# For non-image files, continue with existing logic
|
|
1079
|
+
|
|
736
1080
|
# Check file size
|
|
737
1081
|
file_size = file_path.stat().st_size
|
|
738
1082
|
file_size_mb = file_size / (1024 * 1024)
|
|
@@ -838,7 +1182,7 @@ def interactive(
|
|
|
838
1182
|
# Retry loop for provider selection
|
|
839
1183
|
max_attempts = 3
|
|
840
1184
|
for attempt in range(max_attempts):
|
|
841
|
-
provider_choice = Prompt.ask("
|
|
1185
|
+
provider_choice = Prompt.ask(mode_prompt("Choose provider", "interactive"), default="1")
|
|
842
1186
|
|
|
843
1187
|
try:
|
|
844
1188
|
provider_idx = int(provider_choice) - 1
|
|
@@ -873,8 +1217,7 @@ def interactive(
|
|
|
873
1217
|
|
|
874
1218
|
# Show validation result
|
|
875
1219
|
if validation_result["error"]:
|
|
876
|
-
console.print(
|
|
877
|
-
console.print("[dim]Showing curated models (availability not confirmed)[/dim]")
|
|
1220
|
+
console.print("[yellow]⚠ Default models displayed. Could not validate models.[/yellow]")
|
|
878
1221
|
else:
|
|
879
1222
|
console.print(f"[green]✓ Validated {len(validated_models)} models[/green] [dim]({validation_result['validation_time_ms']}ms)[/dim]")
|
|
880
1223
|
|
|
@@ -945,7 +1288,7 @@ def interactive(
|
|
|
945
1288
|
max_attempts = 3
|
|
946
1289
|
model = None
|
|
947
1290
|
for attempt in range(max_attempts):
|
|
948
|
-
model_choice = Prompt.ask("
|
|
1291
|
+
model_choice = Prompt.ask(mode_prompt("Select model", "interactive"))
|
|
949
1292
|
|
|
950
1293
|
try:
|
|
951
1294
|
model_idx = int(model_choice) - 1
|
|
@@ -966,12 +1309,47 @@ def interactive(
|
|
|
966
1309
|
console.print(f"[red]Too many invalid attempts. Exiting.[/red]")
|
|
967
1310
|
raise typer.Exit(1)
|
|
968
1311
|
|
|
1312
|
+
# Check if model has fixed temperature and prompt if needed
|
|
1313
|
+
model_info = MODEL_CATALOG.get(provider, {}).get(model, {})
|
|
1314
|
+
fixed_temp = model_info.get("fixed_temperature")
|
|
1315
|
+
|
|
1316
|
+
if fixed_temp is not None:
|
|
1317
|
+
temperature = fixed_temp
|
|
1318
|
+
console.print(f"\n[dim]Using fixed temperature: {fixed_temp} for this model[/dim]")
|
|
1319
|
+
else:
|
|
1320
|
+
# Retry loop for temperature input
|
|
1321
|
+
max_attempts = 3
|
|
1322
|
+
temperature = None
|
|
1323
|
+
for attempt in range(max_attempts):
|
|
1324
|
+
temp_input = Prompt.ask(
|
|
1325
|
+
mode_prompt("Temperature (0.0-2.0)", "interactive"),
|
|
1326
|
+
default="0.7"
|
|
1327
|
+
)
|
|
1328
|
+
|
|
1329
|
+
try:
|
|
1330
|
+
temp_value = float(temp_input)
|
|
1331
|
+
if 0.0 <= temp_value <= 2.0:
|
|
1332
|
+
temperature = temp_value
|
|
1333
|
+
break
|
|
1334
|
+
else:
|
|
1335
|
+
console.print("[red]✗ Out of range.[/red] Temperature must be between 0.0 and 2.0")
|
|
1336
|
+
if attempt < max_attempts - 1:
|
|
1337
|
+
console.print("[dim]Try again...[/dim]")
|
|
1338
|
+
except ValueError:
|
|
1339
|
+
console.print(f"[red]✗ Invalid input.[/red] Please enter a number (e.g., '0.7' not '{temp_input}')")
|
|
1340
|
+
if attempt < max_attempts - 1:
|
|
1341
|
+
console.print("[dim]Try again...[/dim]")
|
|
1342
|
+
|
|
1343
|
+
# If still no valid temperature after retries, use default
|
|
1344
|
+
if temperature is None:
|
|
1345
|
+
console.print("[yellow]Too many invalid attempts. Using default: 0.7[/yellow]")
|
|
1346
|
+
temperature = 0.7
|
|
1347
|
+
|
|
969
1348
|
# Initialize client
|
|
970
1349
|
client = LLMClient(provider=provider)
|
|
971
1350
|
messages: List[Message] = []
|
|
972
1351
|
|
|
973
|
-
# Get model info for context window
|
|
974
|
-
model_info = MODEL_CATALOG.get(provider, {}).get(model, {})
|
|
1352
|
+
# Get model info for context window (already retrieved above for temperature check)
|
|
975
1353
|
context_window = model_info.get("context", "N/A")
|
|
976
1354
|
|
|
977
1355
|
# Set conversation history limit (reserve 80% for history, 20% for response)
|
|
@@ -993,15 +1371,55 @@ def interactive(
|
|
|
993
1371
|
console.print(f"[dim]Load a file to provide context for the conversation[/dim]")
|
|
994
1372
|
console.print(f"[dim]Max file size: {MAX_FILE_SIZE_MB} MB | Leave blank to skip[/dim]")
|
|
995
1373
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1374
|
+
# File prompt with retry loop
|
|
1375
|
+
max_file_attempts = 3
|
|
1376
|
+
for file_attempt in range(max_file_attempts):
|
|
1377
|
+
file_path_input = Prompt.ask(mode_prompt("File path (or Enter to skip)", "interactive"), default="")
|
|
1378
|
+
|
|
1379
|
+
if not file_path_input.strip():
|
|
1380
|
+
# User pressed Enter to skip
|
|
1381
|
+
break
|
|
1382
|
+
|
|
999
1383
|
file = Path(file_path_input.strip()).expanduser()
|
|
1384
|
+
|
|
1385
|
+
# Validate file exists and is readable
|
|
1386
|
+
if not file.exists():
|
|
1387
|
+
console.print(f"[red]✗ File not found: {file}[/red]")
|
|
1388
|
+
if file_attempt < max_file_attempts - 1:
|
|
1389
|
+
choice = Prompt.ask(
|
|
1390
|
+
"[cyan]Enter 1 to retry file path or 2 to continue without file[/cyan]",
|
|
1391
|
+
choices=["1", "2"],
|
|
1392
|
+
default="2"
|
|
1393
|
+
)
|
|
1394
|
+
if choice == "2":
|
|
1395
|
+
file = None
|
|
1396
|
+
break
|
|
1397
|
+
# Otherwise loop continues for retry
|
|
1398
|
+
else:
|
|
1399
|
+
console.print("[dim]Continuing without file[/dim]")
|
|
1400
|
+
file = None
|
|
1401
|
+
elif not file.is_file():
|
|
1402
|
+
console.print(f"[red]✗ Path is not a file: {file}[/red]")
|
|
1403
|
+
if file_attempt < max_file_attempts - 1:
|
|
1404
|
+
choice = Prompt.ask(
|
|
1405
|
+
"[cyan]Enter 1 to retry file path or 2 to continue without file[/cyan]",
|
|
1406
|
+
choices=["1", "2"],
|
|
1407
|
+
default="2"
|
|
1408
|
+
)
|
|
1409
|
+
if choice == "2":
|
|
1410
|
+
file = None
|
|
1411
|
+
break
|
|
1412
|
+
else:
|
|
1413
|
+
console.print("[dim]Continuing without file[/dim]")
|
|
1414
|
+
file = None
|
|
1415
|
+
else:
|
|
1416
|
+
# File is valid, break out of retry loop
|
|
1417
|
+
break
|
|
1000
1418
|
|
|
1001
1419
|
# Load initial file if provided
|
|
1002
1420
|
if file:
|
|
1003
1421
|
console.print(f"\n[bold cyan]Loading initial context...[/bold cyan]")
|
|
1004
|
-
file_content = load_file_content(file, warn_large=True)
|
|
1422
|
+
file_content = load_file_content(file, warn_large=True, check_vision=True)
|
|
1005
1423
|
if file_content:
|
|
1006
1424
|
messages.append(Message(
|
|
1007
1425
|
role="user",
|
|
@@ -1009,14 +1427,15 @@ def interactive(
|
|
|
1009
1427
|
))
|
|
1010
1428
|
console.print(f"[dim]File loaded as initial context[/dim]")
|
|
1011
1429
|
|
|
1012
|
-
# Welcome message
|
|
1013
|
-
console.print(f"\n[
|
|
1430
|
+
# Welcome message with mode banner
|
|
1431
|
+
console.print(f"\n[{INTERACTIVE_ACCENT}]═══ ⚡ INTERACTIVE MODE ═══[/{INTERACTIVE_ACCENT}]")
|
|
1432
|
+
console.print(f"[dim]Multi-turn conversation with context preservation[/dim]\n")
|
|
1014
1433
|
|
|
1015
1434
|
# Display context info with API limit warning if applicable
|
|
1016
1435
|
if api_max_input and api_max_input < context_window:
|
|
1017
|
-
console.print(f"Provider: [
|
|
1436
|
+
console.print(f"Provider: [{INTERACTIVE_COLOR}]{provider}[/{INTERACTIVE_COLOR}] | Model: [{INTERACTIVE_COLOR}]{model}[/{INTERACTIVE_COLOR}] | Context: [{INTERACTIVE_COLOR}]{context_window:,} tokens[/{INTERACTIVE_COLOR}] [yellow](API limit: {api_max_input:,})[/yellow]")
|
|
1018
1437
|
else:
|
|
1019
|
-
console.print(f"Provider: [
|
|
1438
|
+
console.print(f"Provider: [{INTERACTIVE_COLOR}]{provider}[/{INTERACTIVE_COLOR}] | Model: [{INTERACTIVE_COLOR}]{model}[/{INTERACTIVE_COLOR}] | Context: [{INTERACTIVE_COLOR}]{context_window:,} tokens[/{INTERACTIVE_COLOR}]")
|
|
1020
1439
|
|
|
1021
1440
|
console.print("[dim]Commands: /file <path> | /attach <path> | /clear | /save [path] | /provider | /help | exit[/dim]")
|
|
1022
1441
|
console.print(f"[dim]File size limit: {MAX_FILE_SIZE_MB} MB | Ctrl+C to exit[/dim]\n")
|
|
@@ -1027,16 +1446,16 @@ def interactive(
|
|
|
1027
1446
|
last_response = None # Track last assistant response for /save command
|
|
1028
1447
|
|
|
1029
1448
|
while True:
|
|
1030
|
-
# Show staged file indicator
|
|
1031
|
-
prompt_text = "[
|
|
1449
|
+
# Show staged file indicator with interactive mode styling
|
|
1450
|
+
prompt_text = f"[{INTERACTIVE_ACCENT}]⚡ You[/{INTERACTIVE_ACCENT}]"
|
|
1032
1451
|
if staged_file_content:
|
|
1033
|
-
prompt_text = f"[
|
|
1452
|
+
prompt_text = f"[{INTERACTIVE_ACCENT}]⚡ You[/{INTERACTIVE_ACCENT}] [dim]📎 {staged_file_name}[/dim]"
|
|
1034
1453
|
|
|
1035
1454
|
# Get user input
|
|
1036
1455
|
try:
|
|
1037
1456
|
user_input = Prompt.ask(prompt_text)
|
|
1038
1457
|
except (KeyboardInterrupt, EOFError):
|
|
1039
|
-
console.print("\n[dim]Exiting...[/dim]")
|
|
1458
|
+
console.print("\n[dim]Exiting interactive mode...[/dim]")
|
|
1040
1459
|
break
|
|
1041
1460
|
|
|
1042
1461
|
# Check for exit commands
|
|
@@ -1046,24 +1465,58 @@ def interactive(
|
|
|
1046
1465
|
|
|
1047
1466
|
# Handle special commands
|
|
1048
1467
|
if user_input.startswith('/file '):
|
|
1049
|
-
# Load and send file immediately
|
|
1468
|
+
# Load and send file immediately with retry option
|
|
1050
1469
|
file_path_str = user_input[6:].strip()
|
|
1051
1470
|
file_path = Path(file_path_str).expanduser()
|
|
1052
1471
|
|
|
1053
|
-
file_content = load_file_content(file_path, warn_large=True)
|
|
1472
|
+
file_content = load_file_content(file_path, warn_large=True, check_vision=True)
|
|
1473
|
+
|
|
1474
|
+
# If file not found, offer retry
|
|
1475
|
+
if not file_content and not file_path.exists():
|
|
1476
|
+
choice = Prompt.ask(
|
|
1477
|
+
"[cyan]Enter 1 to retry file path or 2 to enter message[/cyan]",
|
|
1478
|
+
choices=["1", "2"],
|
|
1479
|
+
default="2"
|
|
1480
|
+
)
|
|
1481
|
+
if choice == "1":
|
|
1482
|
+
# Retry - prompt for new path
|
|
1483
|
+
new_path_str = Prompt.ask("File path")
|
|
1484
|
+
new_file_path = Path(new_path_str.strip()).expanduser()
|
|
1485
|
+
file_content = load_file_content(new_file_path, warn_large=True, check_vision=True)
|
|
1486
|
+
if file_content:
|
|
1487
|
+
file_path = new_file_path
|
|
1488
|
+
# If choice == "2", just continue to message prompt
|
|
1489
|
+
|
|
1054
1490
|
if file_content:
|
|
1055
1491
|
# Send file content as user message
|
|
1056
1492
|
user_input = f"[File: {file_path.name}]\n\n{file_content}"
|
|
1057
1493
|
messages.append(Message(role="user", content=user_input))
|
|
1058
1494
|
else:
|
|
1059
|
-
continue #
|
|
1495
|
+
continue # Skip to next input
|
|
1060
1496
|
|
|
1061
1497
|
elif user_input.startswith('/attach '):
|
|
1062
|
-
# Stage file for next message
|
|
1498
|
+
# Stage file for next message with retry option
|
|
1063
1499
|
file_path_str = user_input[8:].strip()
|
|
1064
1500
|
file_path = Path(file_path_str).expanduser()
|
|
1065
1501
|
|
|
1066
|
-
file_content = load_file_content(file_path, warn_large=True)
|
|
1502
|
+
file_content = load_file_content(file_path, warn_large=True, check_vision=True)
|
|
1503
|
+
|
|
1504
|
+
# If file not found, offer retry
|
|
1505
|
+
if not file_content and not file_path.exists():
|
|
1506
|
+
choice = Prompt.ask(
|
|
1507
|
+
"[cyan]Enter 1 to retry file path or 2 to enter message[/cyan]",
|
|
1508
|
+
choices=["1", "2"],
|
|
1509
|
+
default="2"
|
|
1510
|
+
)
|
|
1511
|
+
if choice == "1":
|
|
1512
|
+
# Retry - prompt for new path
|
|
1513
|
+
new_path_str = Prompt.ask("File path")
|
|
1514
|
+
new_file_path = Path(new_path_str.strip()).expanduser()
|
|
1515
|
+
file_content = load_file_content(new_file_path, warn_large=True, check_vision=True)
|
|
1516
|
+
if file_content:
|
|
1517
|
+
file_path = new_file_path
|
|
1518
|
+
# If choice == "2", continue to message prompt (don't stage anything)
|
|
1519
|
+
|
|
1067
1520
|
if file_content:
|
|
1068
1521
|
staged_file_content = file_content
|
|
1069
1522
|
staged_file_name = file_path.name
|
|
@@ -1163,19 +1616,18 @@ def interactive(
|
|
|
1163
1616
|
console.print("[yellow]Provider not changed[/yellow]")
|
|
1164
1617
|
continue
|
|
1165
1618
|
|
|
1166
|
-
# Validate and display
|
|
1619
|
+
# Validate and display curated models for the selected provider
|
|
1167
1620
|
from stratifyai.utils.provider_validator import get_validated_interactive_models
|
|
1168
1621
|
|
|
1169
1622
|
with console.status(f"[cyan]Validating {new_provider} models...", spinner="dots"):
|
|
1170
|
-
validation_data = get_validated_interactive_models(new_provider
|
|
1623
|
+
validation_data = get_validated_interactive_models(new_provider)
|
|
1171
1624
|
|
|
1172
1625
|
validation_result = validation_data["validation_result"]
|
|
1173
1626
|
validated_models = validation_data["models"]
|
|
1174
1627
|
|
|
1175
1628
|
# Show validation result
|
|
1176
1629
|
if validation_result["error"]:
|
|
1177
|
-
console.print(
|
|
1178
|
-
console.print("[dim]Showing all models (availability not confirmed)[/dim]")
|
|
1630
|
+
console.print("[yellow]⚠ Default models displayed. Could not validate models.[/yellow]")
|
|
1179
1631
|
# Fall back to MODEL_CATALOG if validation fails
|
|
1180
1632
|
available_models = list(MODEL_CATALOG[new_provider].keys())
|
|
1181
1633
|
model_metadata = MODEL_CATALOG[new_provider]
|
|
@@ -1184,15 +1636,27 @@ def interactive(
|
|
|
1184
1636
|
available_models = list(validated_models.keys())
|
|
1185
1637
|
model_metadata = validated_models
|
|
1186
1638
|
|
|
1187
|
-
# Show available models for new provider
|
|
1188
|
-
console.print(f"\n[bold cyan]
|
|
1639
|
+
# Show available models for new provider with full metadata
|
|
1640
|
+
console.print(f"\n[bold cyan]Available {new_provider} models:[/bold cyan]")
|
|
1641
|
+
|
|
1642
|
+
# Display with friendly names, descriptions, and categories (same as initial selection)
|
|
1643
|
+
current_category = None
|
|
1189
1644
|
for i, m in enumerate(available_models, 1):
|
|
1190
1645
|
meta = model_metadata.get(m, {})
|
|
1191
|
-
|
|
1646
|
+
display_name = meta.get("display_name", m)
|
|
1647
|
+
description = meta.get("description", "")
|
|
1648
|
+
category = meta.get("category", "")
|
|
1649
|
+
|
|
1650
|
+
# Show category header if changed
|
|
1651
|
+
if category and category != current_category:
|
|
1652
|
+
console.print(f" [dim]── {category} ──[/dim]")
|
|
1653
|
+
current_category = category
|
|
1654
|
+
|
|
1655
|
+
# Build label with current marker
|
|
1192
1656
|
current_marker = " [green](current)[/green]" if m == model and new_provider == provider else ""
|
|
1193
|
-
label = f" {i}. {
|
|
1194
|
-
if
|
|
1195
|
-
label += " [
|
|
1657
|
+
label = f" {i}. {display_name}{current_marker}"
|
|
1658
|
+
if description:
|
|
1659
|
+
label += f" [dim]- {description}[/dim]"
|
|
1196
1660
|
console.print(label)
|
|
1197
1661
|
|
|
1198
1662
|
# Get model selection
|
|
@@ -1220,6 +1684,43 @@ def interactive(
|
|
|
1220
1684
|
# Update provider and model
|
|
1221
1685
|
provider = new_provider
|
|
1222
1686
|
model = new_model
|
|
1687
|
+
|
|
1688
|
+
# Check if new model has fixed temperature and prompt if needed
|
|
1689
|
+
new_model_info = MODEL_CATALOG.get(provider, {}).get(model, {})
|
|
1690
|
+
fixed_temp = new_model_info.get("fixed_temperature")
|
|
1691
|
+
|
|
1692
|
+
if fixed_temp is not None:
|
|
1693
|
+
temperature = fixed_temp
|
|
1694
|
+
console.print(f"\n[dim]Using fixed temperature: {fixed_temp} for this model[/dim]")
|
|
1695
|
+
else:
|
|
1696
|
+
# Retry loop for temperature input
|
|
1697
|
+
max_attempts = 3
|
|
1698
|
+
temperature = None
|
|
1699
|
+
for attempt in range(max_attempts):
|
|
1700
|
+
temp_input = Prompt.ask(
|
|
1701
|
+
"\n[bold cyan]Temperature[/bold cyan] (0.0-2.0, default 0.7)",
|
|
1702
|
+
default="0.7"
|
|
1703
|
+
)
|
|
1704
|
+
|
|
1705
|
+
try:
|
|
1706
|
+
temp_value = float(temp_input)
|
|
1707
|
+
if 0.0 <= temp_value <= 2.0:
|
|
1708
|
+
temperature = temp_value
|
|
1709
|
+
break
|
|
1710
|
+
else:
|
|
1711
|
+
console.print("[red]✗ Out of range.[/red] Temperature must be between 0.0 and 2.0")
|
|
1712
|
+
if attempt < max_attempts - 1:
|
|
1713
|
+
console.print("[dim]Try again...[/dim]")
|
|
1714
|
+
except ValueError:
|
|
1715
|
+
console.print(f"[red]✗ Invalid input.[/red] Please enter a number (e.g., '0.7' not '{temp_input}')")
|
|
1716
|
+
if attempt < max_attempts - 1:
|
|
1717
|
+
console.print("[dim]Try again...[/dim]")
|
|
1718
|
+
|
|
1719
|
+
# If still no valid temperature after retries, use default
|
|
1720
|
+
if temperature is None:
|
|
1721
|
+
console.print("[yellow]Too many invalid attempts. Using default: 0.7[/yellow]")
|
|
1722
|
+
temperature = 0.7
|
|
1723
|
+
|
|
1223
1724
|
client = LLMClient(provider=provider) # Reinitialize client
|
|
1224
1725
|
|
|
1225
1726
|
# Update context window info
|
|
@@ -1317,7 +1818,7 @@ def interactive(
|
|
|
1317
1818
|
console.print(f"[yellow]⚠ Conversation history truncated (estimated {estimated_tokens:,} tokens)[/yellow]")
|
|
1318
1819
|
|
|
1319
1820
|
# Create request and get response
|
|
1320
|
-
request = ChatRequest(model=model, messages=messages)
|
|
1821
|
+
request = ChatRequest(model=model, messages=messages, temperature=temperature)
|
|
1321
1822
|
|
|
1322
1823
|
try:
|
|
1323
1824
|
# Show spinner while waiting for response
|
|
@@ -1330,9 +1831,9 @@ def interactive(
|
|
|
1330
1831
|
# Store last response for /save command
|
|
1331
1832
|
last_response = response
|
|
1332
1833
|
|
|
1333
|
-
# Display metadata and response
|
|
1334
|
-
console.print(f"\n[
|
|
1335
|
-
console.print(f"[bold]Provider:[/bold] [
|
|
1834
|
+
# Display metadata and response (interactive mode - cyan)
|
|
1835
|
+
console.print(f"\n[{INTERACTIVE_ACCENT}]⚡ Assistant[/{INTERACTIVE_ACCENT}]")
|
|
1836
|
+
console.print(f"[bold]Provider:[/bold] [{INTERACTIVE_COLOR}]{provider}[/{INTERACTIVE_COLOR}] | [bold]Model:[/bold] [{INTERACTIVE_COLOR}]{model}[/{INTERACTIVE_COLOR}]")
|
|
1336
1837
|
|
|
1337
1838
|
# Build usage line with token breakdown and cache info
|
|
1338
1839
|
usage_parts = [
|
|
@@ -1356,7 +1857,7 @@ def interactive(
|
|
|
1356
1857
|
usage_parts.append(f"Cache Read: {response.usage.cache_read_tokens:,}")
|
|
1357
1858
|
|
|
1358
1859
|
console.print(f"[dim]{' | '.join(usage_parts)}[/dim]")
|
|
1359
|
-
console.print(f"\n{response.content}", style=
|
|
1860
|
+
console.print(f"\n{response.content}", style=INTERACTIVE_COLOR)
|
|
1360
1861
|
console.print("[dim]💡 Tip: Use /save to save this response to a file[/dim]\n")
|
|
1361
1862
|
|
|
1362
1863
|
except AuthenticationError as e:
|