New functionality++ Verision bump
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -33,3 +33,4 @@ build*
|
||||
compiled/
|
||||
images/oai-iOS-Default-1024x1024@1x.png
|
||||
images/oai.icon/
|
||||
b0.sh
|
||||
339
oai.py
339
oai.py
@@ -20,11 +20,17 @@ import re
|
||||
import sqlite3
|
||||
import json
|
||||
import datetime
|
||||
import logging # Added missing import for logging
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler # Added for log rotation
|
||||
from prompt_toolkit import PromptSession
|
||||
from prompt_toolkit.history import FileHistory
|
||||
from rich.logging import RichHandler
|
||||
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
||||
from packaging import version as pkg_version
|
||||
import io # Added for custom handler
|
||||
|
||||
# App version. Changes by author with new releases.
|
||||
version = '1.9.5'
|
||||
|
||||
app = typer.Typer()
|
||||
|
||||
@@ -71,27 +77,9 @@ LOW_CREDIT_RATIO = 0.1 # Warn if credits left < 10% of total
|
||||
LOW_CREDIT_AMOUNT = 1.0 # Warn if credits left < $1 in absolute terms
|
||||
HIGH_COST_WARNING = "cost_warning_threshold" # Configurable key for cost threshold, default $0.01
|
||||
|
||||
# Setup Rich-powered logging to file as per [rich.readthedocs.io](https://rich.readthedocs.io/en/latest/logging.html)
|
||||
log_console = Console(file=open(log_file, 'a'), width=120) # Append to log file, wider for tracebacks
|
||||
handler = RichHandler(
|
||||
console=log_console,
|
||||
level=logging.INFO,
|
||||
rich_tracebacks=True,
|
||||
tracebacks_suppress=['requests', 'openrouter', 'urllib3', 'httpx', 'openai'] # Suppress irrelevant tracebacks for cleaner logs
|
||||
)
|
||||
logging.basicConfig(
|
||||
level=logging.NOTSET, # Let handler control what gets written
|
||||
format="%(message)s", # Rich formats it
|
||||
datefmt="[%X]",
|
||||
handlers=[handler]
|
||||
)
|
||||
app_logger = logging.getLogger("oai_app")
|
||||
app_logger.setLevel(logging.INFO)
|
||||
|
||||
# DB configuration
|
||||
database = config_dir / 'oai_config.db'
|
||||
DB_FILE = str(database)
|
||||
version = '1.9'
|
||||
|
||||
def create_table_if_not_exists():
|
||||
"""Ensure the config and conversation_sessions tables exist."""
|
||||
@@ -122,6 +110,124 @@ def set_config(key: str, value: str):
|
||||
conn.execute('INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)', (key, value))
|
||||
conn.commit()
|
||||
|
||||
# ============================================================================
|
||||
# ROTATING RICH HANDLER - Combines RotatingFileHandler with Rich formatting
|
||||
# ============================================================================
|
||||
class RotatingRichHandler(RotatingFileHandler):
|
||||
"""Custom handler that combines RotatingFileHandler with Rich formatting."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Create a Rich console that writes to a string buffer
|
||||
self.rich_console = Console(file=io.StringIO(), width=120, force_terminal=False)
|
||||
self.rich_handler = RichHandler(
|
||||
console=self.rich_console,
|
||||
show_time=True,
|
||||
show_path=True,
|
||||
rich_tracebacks=True,
|
||||
tracebacks_suppress=['requests', 'openrouter', 'urllib3', 'httpx', 'openai']
|
||||
)
|
||||
|
||||
def emit(self, record):
|
||||
try:
|
||||
# Let RichHandler format the record
|
||||
self.rich_handler.emit(record)
|
||||
|
||||
# Get the formatted output from the string buffer
|
||||
output = self.rich_console.file.getvalue()
|
||||
|
||||
# Clear the buffer for next use
|
||||
self.rich_console.file.seek(0)
|
||||
self.rich_console.file.truncate(0)
|
||||
|
||||
# Write the Rich-formatted output to our rotating file
|
||||
if output:
|
||||
self.stream.write(output)
|
||||
self.flush()
|
||||
|
||||
except Exception:
|
||||
self.handleError(record)
|
||||
|
||||
# Get log configuration from DB
|
||||
LOG_MAX_SIZE_MB = int(get_config('log_max_size_mb') or "10")
|
||||
LOG_BACKUP_COUNT = int(get_config('log_backup_count') or "2")
|
||||
|
||||
# Create the custom rotating handler
|
||||
app_handler = RotatingRichHandler(
|
||||
filename=str(log_file),
|
||||
maxBytes=LOG_MAX_SIZE_MB * 1024 * 1024, # Convert MB to bytes
|
||||
backupCount=LOG_BACKUP_COUNT,
|
||||
encoding='utf-8'
|
||||
)
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.NOTSET,
|
||||
format="%(message)s", # Rich formats it
|
||||
datefmt="[%X]",
|
||||
handlers=[app_handler]
|
||||
)
|
||||
|
||||
app_logger = logging.getLogger("oai_app")
|
||||
app_logger.setLevel(logging.INFO)
|
||||
|
||||
# ============================================================================
|
||||
# END OF LOGGING SETUP
|
||||
# ============================================================================
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def check_for_updates(current_version: str) -> str:
|
||||
"""
|
||||
Check if a new version is available using semantic versioning.
|
||||
|
||||
Returns:
|
||||
Formatted status string for display
|
||||
"""
|
||||
try:
|
||||
response = requests.get(
|
||||
'https://gitlab.pm/api/v1/repos/rune/oai/releases/latest',
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=1.0,
|
||||
allow_redirects=True
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
version_online = data.get('tag_name', '').lstrip('v')
|
||||
|
||||
if not version_online:
|
||||
logger.warning("No version found in API response")
|
||||
return f"[bold green]oAI version {current_version}[/]"
|
||||
|
||||
current = pkg_version.parse(current_version)
|
||||
latest = pkg_version.parse(version_online)
|
||||
|
||||
if latest > current:
|
||||
logger.info(f"Update available: {current_version} → {version_online}")
|
||||
return f"[bold green]oAI version {current_version} [/][bold red](Update available: {current_version} → {version_online})[/]"
|
||||
else:
|
||||
logger.debug(f"Already up to date: {current_version}")
|
||||
return f"[bold green]oAI version {current_version} (up to date)[/]"
|
||||
|
||||
except requests.exceptions.HTTPError as e:
|
||||
logger.warning(f"HTTP error checking for updates: {e.response.status_code}")
|
||||
return f"[bold green]oAI version {current_version}[/]"
|
||||
except requests.exceptions.ConnectionError:
|
||||
logger.warning("Network error checking for updates (offline?)")
|
||||
return f"[bold green]oAI version {current_version}[/]"
|
||||
except requests.exceptions.Timeout:
|
||||
logger.warning("Timeout checking for updates")
|
||||
return f"[bold green]oAI version {current_version}[/]"
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.warning(f"Request error checking for updates: {type(e).__name__}")
|
||||
return f"[bold green]oAI version {current_version}[/]"
|
||||
except (KeyError, ValueError) as e:
|
||||
logger.warning(f"Invalid API response checking for updates: {e}")
|
||||
return f"[bold green]oAI version {current_version}[/]"
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error checking for updates: {e}")
|
||||
return f"[bold green]oAI version {current_version}[/]"
|
||||
|
||||
def save_conversation(name: str, data: List[Dict[str, str]]):
|
||||
"""Save conversation history to DB."""
|
||||
timestamp = datetime.datetime.now().isoformat()
|
||||
@@ -309,6 +415,7 @@ STREAM_ENABLED = get_config('stream_enabled') or "on"
|
||||
DEFAULT_MODEL_ID = get_config('default_model')
|
||||
MAX_TOKEN = int(get_config('max_token') or "100000")
|
||||
COST_WARNING_THRESHOLD = float(get_config(HIGH_COST_WARNING) or "0.01") # Configurable cost threshold for alerts
|
||||
DEFAULT_ONLINE_MODE = get_config('default_online_mode') or "off" # New: Default online mode setting
|
||||
|
||||
# Fetch models with app identification headers
|
||||
models_data = []
|
||||
@@ -416,16 +523,15 @@ def display_paginated_table(table: Table, title: str):
|
||||
return
|
||||
|
||||
# Separate header from data rows
|
||||
# Typically the first 3 lines are: top border, header row, separator
|
||||
header_lines = []
|
||||
data_lines = []
|
||||
|
||||
# Find where the header ends (usually after the first horizontal line after header text)
|
||||
# Find where the header ends
|
||||
header_end_index = 0
|
||||
found_header_text = False
|
||||
|
||||
for i, line_segments in enumerate(all_lines):
|
||||
# Check if this line contains header-style text (bold/magenta usually)
|
||||
# Check if this line contains header-style text
|
||||
has_header_style = any(
|
||||
seg.style and ('bold' in str(seg.style) or 'magenta' in str(seg.style))
|
||||
for seg in line_segments
|
||||
@@ -437,21 +543,20 @@ def display_paginated_table(table: Table, title: str):
|
||||
# After finding header text, the next line with box-drawing chars is the separator
|
||||
if found_header_text and i > 0:
|
||||
line_text = ''.join(seg.text for seg in line_segments)
|
||||
# Check for horizontal line characters (─ ━ ╌ etc.)
|
||||
if any(char in line_text for char in ['─', '━', '┼', '╪', '┤', '├']):
|
||||
header_end_index = i
|
||||
break
|
||||
|
||||
# If we found a header separator, split there
|
||||
if header_end_index > 0:
|
||||
header_lines = all_lines[:header_end_index + 1] # Include the separator
|
||||
header_lines = all_lines[:header_end_index + 1]
|
||||
data_lines = all_lines[header_end_index + 1:]
|
||||
else:
|
||||
# Fallback: assume first 3 lines are header
|
||||
header_lines = all_lines[:min(3, len(all_lines))]
|
||||
data_lines = all_lines[min(3, len(all_lines)):]
|
||||
|
||||
# Calculate how many data lines fit per page (accounting for header)
|
||||
# Calculate how many data lines fit per page
|
||||
lines_per_page = terminal_height - len(header_lines)
|
||||
|
||||
# Display with pagination
|
||||
@@ -469,7 +574,7 @@ def display_paginated_table(table: Table, title: str):
|
||||
for line_segments in header_lines:
|
||||
for segment in line_segments:
|
||||
console.print(segment.text, style=segment.style, end="")
|
||||
console.print() # New line after each row
|
||||
console.print()
|
||||
|
||||
# Calculate how many data lines to show on this page
|
||||
end_line = min(current_line + lines_per_page, len(data_lines))
|
||||
@@ -478,7 +583,7 @@ def display_paginated_table(table: Table, title: str):
|
||||
for line_segments in data_lines[current_line:end_line]:
|
||||
for segment in line_segments:
|
||||
console.print(segment.text, style=segment.style, end="")
|
||||
console.print() # New line after each row
|
||||
console.print()
|
||||
|
||||
# Update position
|
||||
current_line = end_line
|
||||
@@ -492,20 +597,16 @@ def display_paginated_table(table: Table, title: str):
|
||||
import tty
|
||||
import termios
|
||||
|
||||
# Save terminal settings
|
||||
fd = sys.stdin.fileno()
|
||||
old_settings = termios.tcgetattr(fd)
|
||||
|
||||
try:
|
||||
# Set terminal to raw mode to read single character
|
||||
tty.setraw(fd)
|
||||
char = sys.stdin.read(1)
|
||||
|
||||
# If not space, break pagination
|
||||
if char != ' ':
|
||||
break
|
||||
finally:
|
||||
# Restore terminal settings
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||
except:
|
||||
# Fallback for Windows or if termios not available
|
||||
@@ -513,12 +614,11 @@ def display_paginated_table(table: Table, title: str):
|
||||
if input_char != '':
|
||||
break
|
||||
else:
|
||||
# No more content
|
||||
break
|
||||
|
||||
@app.command()
|
||||
def chat():
|
||||
global API_KEY, OPENROUTER_BASE_URL, STREAM_ENABLED, MAX_TOKEN, COST_WARNING_THRESHOLD
|
||||
global API_KEY, OPENROUTER_BASE_URL, STREAM_ENABLED, MAX_TOKEN, COST_WARNING_THRESHOLD, DEFAULT_ONLINE_MODE, LOG_MAX_SIZE_MB, LOG_BACKUP_COUNT
|
||||
session_max_token = 0
|
||||
session_system_prompt = ""
|
||||
session_history = []
|
||||
@@ -531,9 +631,9 @@ def chat():
|
||||
conversation_memory_enabled = True # Memory ON by default
|
||||
memory_start_index = 0 # Track when memory was last enabled
|
||||
saved_conversations_cache = [] # Cache for /list results to use with /load by number
|
||||
online_mode_enabled = False # Online mode (web search) disabled by default
|
||||
online_mode_enabled = DEFAULT_ONLINE_MODE == "on" # Initialize from config
|
||||
|
||||
app_logger.info("Starting new chat session with memory enabled") # Log session start
|
||||
app_logger.info("Starting new chat session with memory enabled")
|
||||
|
||||
if not API_KEY:
|
||||
console.print("[bold red]API key not found. Use '/config api'.[/]")
|
||||
@@ -567,7 +667,8 @@ def chat():
|
||||
client = OpenRouter(api_key=API_KEY)
|
||||
|
||||
if selected_model:
|
||||
console.print(f"[bold blue]Welcome to oAI![/] [bold red]Active model: {selected_model['name']}[/]")
|
||||
online_status = "enabled" if online_mode_enabled else "disabled"
|
||||
console.print(f"[bold blue]Welcome to oAI![/] [bold red]Active model: {selected_model['name']}[/] [dim cyan](Online mode: {online_status})[/]")
|
||||
else:
|
||||
console.print("[bold blue]Welcome to oAI![/] [italic blue]Select a model with '/model'.[/]")
|
||||
|
||||
@@ -584,14 +685,11 @@ def chat():
|
||||
# Handle // escape sequence - convert to single / and treat as regular text
|
||||
if user_input.startswith("//"):
|
||||
user_input = user_input[1:] # Remove first slash, keep the rest
|
||||
# Don't process as command, jump to message processing
|
||||
|
||||
# Check for unknown commands (starts with / but not a valid command)
|
||||
# Check for unknown commands
|
||||
elif user_input.startswith("/") and user_input.lower() not in ["exit", "quit", "bye"]:
|
||||
# Extract command (first word after /)
|
||||
command_word = user_input.split()[0].lower() if user_input.split() else user_input.lower()
|
||||
|
||||
# Check if it's a valid command or partial match
|
||||
if not any(command_word.startswith(cmd) for cmd in VALID_COMMANDS):
|
||||
console.print(f"[bold red]Unknown command: {command_word}[/]")
|
||||
console.print("[bold yellow]Type /help to see all available commands.[/]")
|
||||
@@ -600,7 +698,7 @@ def chat():
|
||||
|
||||
if user_input.lower() in ["exit", "quit", "bye"]:
|
||||
total_tokens = total_input_tokens + total_output_tokens
|
||||
app_logger.info(f"Session ended. Total messages: {message_count}, Total tokens: {total_tokens}, Total cost: ${total_cost:.4f}") # Log session summary
|
||||
app_logger.info(f"Session ended. Total messages: {message_count}, Total tokens: {total_tokens}, Total cost: ${total_cost:.4f}")
|
||||
console.print("[bold yellow]Goodbye![/]")
|
||||
return
|
||||
|
||||
@@ -618,7 +716,9 @@ def chat():
|
||||
args = user_input[8:].strip()
|
||||
if not args:
|
||||
status = "enabled" if online_mode_enabled else "disabled"
|
||||
default_status = "enabled" if DEFAULT_ONLINE_MODE == "on" else "disabled"
|
||||
console.print(f"[bold blue]Online mode (web search) {status}.[/]")
|
||||
console.print(f"[dim blue]Default setting: {default_status} (use '/config online on|off' to change)[/]")
|
||||
if selected_model:
|
||||
if supports_online_mode(selected_model):
|
||||
console.print(f"[dim green]Current model '{selected_model['name']}' supports online mode.[/]")
|
||||
@@ -635,12 +735,12 @@ def chat():
|
||||
app_logger.warning(f"Online mode activation failed - model {selected_model['id']} doesn't support it")
|
||||
continue
|
||||
online_mode_enabled = True
|
||||
console.print("[bold green]Online mode enabled. Model will use web search capabilities.[/]")
|
||||
console.print("[bold green]Online mode enabled for this session. Model will use web search capabilities.[/]")
|
||||
console.print(f"[dim blue]Effective model ID: {get_effective_model_id(selected_model['id'], True)}[/]")
|
||||
app_logger.info(f"Online mode enabled for model {selected_model['id']}")
|
||||
elif args.lower() == "off":
|
||||
online_mode_enabled = False
|
||||
console.print("[bold green]Online mode disabled. Model will not use web search.[/]")
|
||||
console.print("[bold green]Online mode disabled for this session. Model will not use web search.[/]")
|
||||
if selected_model:
|
||||
console.print(f"[dim blue]Effective model ID: {selected_model['id']}[/]")
|
||||
app_logger.info("Online mode disabled")
|
||||
@@ -660,7 +760,7 @@ def chat():
|
||||
continue
|
||||
if args.lower() == "on":
|
||||
conversation_memory_enabled = True
|
||||
memory_start_index = len(session_history) # Remember where we started
|
||||
memory_start_index = len(session_history)
|
||||
console.print("[bold green]Conversation memory enabled. Will remember conversations from this point forward.[/]")
|
||||
console.print(f"[dim blue]Memory will track messages starting from index {memory_start_index}.[/]")
|
||||
app_logger.info(f"Conversation memory enabled at index {memory_start_index}")
|
||||
@@ -673,7 +773,6 @@ def chat():
|
||||
console.print("[bold yellow]Usage: /memory on|off (or /memory to view status)[/]")
|
||||
continue
|
||||
elif user_input.lower().startswith("/paste"):
|
||||
# Get optional prompt after /paste
|
||||
optional_prompt = user_input[7:].strip()
|
||||
|
||||
try:
|
||||
@@ -688,13 +787,10 @@ def chat():
|
||||
app_logger.warning("Paste attempted with empty clipboard")
|
||||
continue
|
||||
|
||||
# Validate it's text (check if it's valid UTF-8 and printable)
|
||||
try:
|
||||
# Try to encode/decode to ensure it's valid text
|
||||
clipboard_content.encode('utf-8')
|
||||
|
||||
# Show preview of pasted content
|
||||
preview_lines = clipboard_content.split('\n')[:10] # First 10 lines
|
||||
preview_lines = clipboard_content.split('\n')[:10]
|
||||
preview_text = '\n'.join(preview_lines)
|
||||
if len(clipboard_content.split('\n')) > 10:
|
||||
preview_text += "\n... (content truncated for preview)"
|
||||
@@ -709,7 +805,6 @@ def chat():
|
||||
border_style="cyan"
|
||||
))
|
||||
|
||||
# Build the final prompt
|
||||
if optional_prompt:
|
||||
final_prompt = f"{optional_prompt}\n\n```\n{clipboard_content}\n```"
|
||||
console.print(f"[dim blue]Sending with prompt: '{optional_prompt}'[/]")
|
||||
@@ -717,7 +812,6 @@ def chat():
|
||||
final_prompt = clipboard_content
|
||||
console.print("[dim blue]Sending clipboard content without additional prompt[/]")
|
||||
|
||||
# Set user_input to the pasted content so it gets processed normally
|
||||
user_input = final_prompt
|
||||
app_logger.info(f"Pasted content from clipboard: {char_count} chars, {line_count} lines, with prompt: {bool(optional_prompt)}")
|
||||
|
||||
@@ -745,13 +839,11 @@ def chat():
|
||||
console.print("[bold red]No conversation history to export.[/]")
|
||||
continue
|
||||
|
||||
# Validate format
|
||||
if export_format not in ['md', 'json', 'html']:
|
||||
console.print("[bold red]Invalid format. Use: md, json, or html[/]")
|
||||
continue
|
||||
|
||||
try:
|
||||
# Generate export content
|
||||
if export_format == 'md':
|
||||
content = export_as_markdown(session_history, session_system_prompt)
|
||||
elif export_format == 'json':
|
||||
@@ -759,7 +851,6 @@ def chat():
|
||||
elif export_format == 'html':
|
||||
content = export_as_html(session_history, session_system_prompt)
|
||||
|
||||
# Write to file
|
||||
export_path = Path(filename).expanduser()
|
||||
with open(export_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
@@ -790,7 +881,6 @@ def chat():
|
||||
console.print("[bold yellow]Tip: Use /list to see numbered conversations[/]")
|
||||
continue
|
||||
|
||||
# Check if input is a number
|
||||
conversation_name = None
|
||||
if args.isdigit():
|
||||
conv_number = int(args)
|
||||
@@ -811,9 +901,8 @@ def chat():
|
||||
continue
|
||||
session_history = loaded_data
|
||||
current_index = len(session_history) - 1
|
||||
# When loading, reset memory tracking if memory is enabled
|
||||
if conversation_memory_enabled:
|
||||
memory_start_index = 0 # Include all loaded messages in memory
|
||||
memory_start_index = 0
|
||||
total_input_tokens = 0
|
||||
total_output_tokens = 0
|
||||
total_cost = 0.0
|
||||
@@ -828,7 +917,6 @@ def chat():
|
||||
console.print("[bold yellow]Tip: Use /list to see numbered conversations[/]")
|
||||
continue
|
||||
|
||||
# Check if input is a number
|
||||
conversation_name = None
|
||||
if args.isdigit():
|
||||
conv_number = int(args)
|
||||
@@ -842,7 +930,6 @@ def chat():
|
||||
else:
|
||||
conversation_name = args
|
||||
|
||||
# Confirm deletion
|
||||
try:
|
||||
confirm = typer.confirm(f"Delete conversation '{conversation_name}'? This cannot be undone.", default=False)
|
||||
if not confirm:
|
||||
@@ -856,7 +943,6 @@ def chat():
|
||||
if deleted_count > 0:
|
||||
console.print(f"[bold green]Conversation '{conversation_name}' deleted ({deleted_count} version(s) removed).[/]")
|
||||
app_logger.info(f"Conversation '{conversation_name}' deleted - {deleted_count} version(s)")
|
||||
# Refresh cache if deleted conversation was in it
|
||||
if saved_conversations_cache:
|
||||
saved_conversations_cache = [c for c in saved_conversations_cache if c['name'] != conversation_name]
|
||||
else:
|
||||
@@ -871,12 +957,10 @@ def chat():
|
||||
saved_conversations_cache = []
|
||||
continue
|
||||
|
||||
# Update cache for /load command
|
||||
saved_conversations_cache = conversations
|
||||
|
||||
table = Table("No.", "Name", "Messages", "Last Saved", show_header=True, header_style="bold magenta")
|
||||
for idx, conv in enumerate(conversations, 1):
|
||||
# Parse ISO timestamp and format it nicely
|
||||
try:
|
||||
dt = datetime.datetime.fromisoformat(conv['timestamp'])
|
||||
formatted_time = dt.strftime('%Y-%m-%d %H:%M:%S')
|
||||
@@ -899,7 +983,6 @@ def chat():
|
||||
continue
|
||||
current_index -= 1
|
||||
prev_response = session_history[current_index]['response']
|
||||
# Render as markdown with proper formatting
|
||||
md = Markdown(prev_response)
|
||||
console.print(Panel(md, title=f"[bold green]Previous Response ({current_index + 1}/{len(session_history)})[/]", title_align="left"))
|
||||
app_logger.debug(f"Viewed previous response at index {current_index}")
|
||||
@@ -910,7 +993,6 @@ def chat():
|
||||
continue
|
||||
current_index += 1
|
||||
next_response = session_history[current_index]['response']
|
||||
# Render as markdown with proper formatting
|
||||
md = Markdown(next_response)
|
||||
console.print(Panel(md, title=f"[bold green]Next Response ({current_index + 1}/{len(session_history)})[/]", title_align="left"))
|
||||
app_logger.debug(f"Viewed next response at index {current_index}")
|
||||
@@ -925,7 +1007,6 @@ def chat():
|
||||
console.print(Panel(table, title="[bold green]Session Cost Summary[/]", title_align="left"))
|
||||
app_logger.info(f"User viewed stats: {stats}")
|
||||
|
||||
# Cost warnings in /stats
|
||||
warnings = check_credit_alerts(credits)
|
||||
if warnings:
|
||||
warning_text = '|'.join(warnings)
|
||||
@@ -954,7 +1035,7 @@ def chat():
|
||||
session_history = []
|
||||
current_index = -1
|
||||
session_system_prompt = ""
|
||||
memory_start_index = 0 # Reset memory tracking
|
||||
memory_start_index = 0
|
||||
total_input_tokens = 0
|
||||
total_output_tokens = 0
|
||||
total_cost = 0.0
|
||||
@@ -976,7 +1057,6 @@ def chat():
|
||||
console.print(f"[bold red]Model '{args}' not found.[/]")
|
||||
continue
|
||||
|
||||
# Display model info
|
||||
pricing = model_to_show.get("pricing", {})
|
||||
architecture = model_to_show.get("architecture", {})
|
||||
supported_params = ", ".join(model_to_show.get("supported_parameters", [])) or "None"
|
||||
@@ -1002,7 +1082,6 @@ def chat():
|
||||
console.print(Panel(table, title=f"[bold green]Model Info: {model_to_show['name']}[/]", title_align="left"))
|
||||
continue
|
||||
|
||||
# Model selection with Image and Online columns
|
||||
elif user_input.startswith("/model"):
|
||||
app_logger.info("User initiated model selection")
|
||||
args = user_input[7:].strip()
|
||||
@@ -1014,14 +1093,12 @@ def chat():
|
||||
console.print(f"[bold red]No models match '{search_term}'. Try '/model'.[/]")
|
||||
continue
|
||||
|
||||
# Create table with Image and Online columns
|
||||
table = Table("No.", "Name", "ID", "Image", "Online", show_header=True, header_style="bold magenta")
|
||||
for i, model in enumerate(filtered_models, 1):
|
||||
image_support = "[green]✓[/green]" if has_image_capability(model) else "[red]✗[/red]"
|
||||
online_support = "[green]✓[/green]" if supports_online_mode(model) else "[red]✗[/red]"
|
||||
table.add_row(str(i), model["name"], model["id"], image_support, online_support)
|
||||
|
||||
# Use pagination for the table
|
||||
title = f"[bold green]Available Models ({'All' if not search_term else f'Search: {search_term}'})[/]"
|
||||
display_paginated_table(table, title)
|
||||
|
||||
@@ -1032,14 +1109,19 @@ def chat():
|
||||
break
|
||||
if 1 <= choice <= len(filtered_models):
|
||||
selected_model = filtered_models[choice - 1]
|
||||
# Disable online mode when switching models (user must re-enable)
|
||||
if online_mode_enabled:
|
||||
|
||||
# Apply default online mode if model supports it
|
||||
if supports_online_mode(selected_model) and DEFAULT_ONLINE_MODE == "on":
|
||||
online_mode_enabled = True
|
||||
console.print(f"[bold cyan]Selected: {selected_model['name']} ({selected_model['id']})[/]")
|
||||
console.print("[dim cyan]✓ Online mode auto-enabled (default setting). Use '/online off' to disable for this session.[/]")
|
||||
else:
|
||||
online_mode_enabled = False
|
||||
console.print("[dim yellow]Note: Online mode auto-disabled when changing models.[/]")
|
||||
console.print(f"[bold cyan]Selected: {selected_model['name']} ({selected_model['id']})[/]")
|
||||
if supports_online_mode(selected_model):
|
||||
console.print("[dim green]✓ This model supports online mode. Use '/online on' to enable web search.[/]")
|
||||
app_logger.info(f"Model selected: {selected_model['name']} ({selected_model['id']})")
|
||||
|
||||
app_logger.info(f"Model selected: {selected_model['name']} ({selected_model['id']}), Online: {online_mode_enabled}")
|
||||
break
|
||||
console.print("[bold red]Invalid choice. Try again.[/]")
|
||||
except ValueError:
|
||||
@@ -1047,7 +1129,6 @@ def chat():
|
||||
continue
|
||||
|
||||
elif user_input.startswith("/maxtoken"):
|
||||
# (unchanged)
|
||||
args = user_input[10:].strip()
|
||||
if not args:
|
||||
console.print(f"[bold blue]Current session max tokens: {session_max_token}[/]")
|
||||
@@ -1067,7 +1148,6 @@ def chat():
|
||||
continue
|
||||
|
||||
elif user_input.startswith("/system"):
|
||||
# (unchanged but added to history view)
|
||||
args = user_input[8:].strip()
|
||||
if not args:
|
||||
if session_system_prompt:
|
||||
@@ -1085,13 +1165,14 @@ def chat():
|
||||
|
||||
elif user_input.startswith("/config"):
|
||||
args = user_input[8:].strip().lower()
|
||||
update = check_for_updates(version)
|
||||
|
||||
if args == "api":
|
||||
try:
|
||||
new_api_key = typer.prompt("Enter new API key")
|
||||
if new_api_key.strip():
|
||||
set_config('api_key', new_api_key.strip())
|
||||
API_KEY = new_api_key.strip()
|
||||
# Reinitialize client with new API key
|
||||
client = OpenRouter(api_key=API_KEY)
|
||||
console.print("[bold green]API key updated![/]")
|
||||
else:
|
||||
@@ -1110,7 +1191,7 @@ def chat():
|
||||
except Exception as e:
|
||||
console.print(f"[bold red]Error updating URL: {e}[/]")
|
||||
elif args.startswith("costwarning"):
|
||||
sub_args = args[11:].strip() # Extract everything after "costwarning"
|
||||
sub_args = args[11:].strip()
|
||||
if not sub_args:
|
||||
console.print(f"[bold blue]Stored cost warning threshold: ${COST_WARNING_THRESHOLD:.4f}[/]")
|
||||
continue
|
||||
@@ -1132,6 +1213,43 @@ def chat():
|
||||
console.print(f"[bold green]Streaming {'enabled' if sub_args == 'on' else 'disabled'}.[/]")
|
||||
else:
|
||||
console.print("[bold yellow]Usage: /config stream on|off[/]")
|
||||
elif args.startswith("log"):
|
||||
sub_args = args[4:].strip()
|
||||
if not sub_args:
|
||||
console.print(f"[bold blue]Current log file size limit: {LOG_MAX_SIZE_MB} MB[/]")
|
||||
console.print(f"[bold blue]Log backup count: {LOG_BACKUP_COUNT} files[/]")
|
||||
console.print(f"[dim yellow]Total max disk usage: ~{LOG_MAX_SIZE_MB * (LOG_BACKUP_COUNT + 1)} MB[/]")
|
||||
continue
|
||||
try:
|
||||
new_size_mb = int(sub_args)
|
||||
if new_size_mb < 1:
|
||||
console.print("[bold red]Log size must be at least 1 MB.[/]")
|
||||
continue
|
||||
if new_size_mb > 100:
|
||||
console.print("[bold yellow]Warning: Log size > 100MB. Capping at 100MB.[/]")
|
||||
new_size_mb = 100
|
||||
set_config('log_max_size_mb', str(new_size_mb))
|
||||
LOG_MAX_SIZE_MB = new_size_mb
|
||||
console.print(f"[bold green]Log size limit set to {new_size_mb} MB.[/]")
|
||||
console.print("[bold yellow]⚠️ Restart the application for this change to take effect.[/]")
|
||||
app_logger.info(f"Log size limit updated to {new_size_mb} MB (requires restart)")
|
||||
except ValueError:
|
||||
console.print("[bold red]Invalid size. Provide a number in MB.[/]")
|
||||
elif args.startswith("online"):
|
||||
sub_args = args[7:].strip()
|
||||
if not sub_args:
|
||||
current_default = "enabled" if DEFAULT_ONLINE_MODE == "on" else "disabled"
|
||||
console.print(f"[bold blue]Default online mode: {current_default}[/]")
|
||||
console.print("[dim yellow]This sets the default for new models. Use '/online on|off' to override in current session.[/]")
|
||||
continue
|
||||
if sub_args in ["on", "off"]:
|
||||
set_config('default_online_mode', sub_args)
|
||||
DEFAULT_ONLINE_MODE = sub_args
|
||||
console.print(f"[bold green]Default online mode {'enabled' if sub_args == 'on' else 'disabled'}.[/]")
|
||||
console.print("[dim blue]Note: This affects new model selections. Current session unchanged.[/]")
|
||||
app_logger.info(f"Default online mode set to {sub_args}")
|
||||
else:
|
||||
console.print("[bold yellow]Usage: /config online on|off[/]")
|
||||
elif args.startswith("maxtoken"):
|
||||
sub_args = args[9:].strip()
|
||||
if not sub_args:
|
||||
@@ -1163,14 +1281,12 @@ def chat():
|
||||
console.print(f"[bold red]No models match '{search_term}'. Try without search.[/]")
|
||||
continue
|
||||
|
||||
# Create table with Image and Online columns
|
||||
table = Table("No.", "Name", "ID", "Image", "Online", show_header=True, header_style="bold magenta")
|
||||
for i, model in enumerate(filtered_models, 1):
|
||||
image_support = "[green]✓[/green]" if has_image_capability(model) else "[red]✗[/red]"
|
||||
online_support = "[green]✓[/green]" if supports_online_mode(model) else "[red]✗[/red]"
|
||||
table.add_row(str(i), model["name"], model["id"], image_support, online_support)
|
||||
|
||||
# Use pagination for the table
|
||||
title = f"[bold green]Available Models for Default ({'All' if not search_term else f'Search: {search_term}'})[/]"
|
||||
display_paginated_table(table, title)
|
||||
|
||||
@@ -1197,10 +1313,13 @@ def chat():
|
||||
table.add_row("Base URL", OPENROUTER_BASE_URL or "[Not set]")
|
||||
table.add_row("DB Path", str(database) or "[Not set]")
|
||||
table.add_row("Logfile", str(log_file) or "[Not set]")
|
||||
table.add_row("Log Size Limit", f"{LOG_MAX_SIZE_MB} MB")
|
||||
table.add_row("Log Backups", str(LOG_BACKUP_COUNT))
|
||||
table.add_row("Streaming", "Enabled" if STREAM_ENABLED == "on" else "Disabled")
|
||||
table.add_row("Default Model", DEFAULT_MODEL_ID or "[Not set]")
|
||||
table.add_row("Current Model", "[Not set]" if selected_model is None else str(selected_model["name"]))
|
||||
table.add_row("Online Mode", "Enabled" if online_mode_enabled else "Disabled")
|
||||
table.add_row("Default Online Mode", "Enabled" if DEFAULT_ONLINE_MODE == "on" else "Disabled")
|
||||
table.add_row("Session Online Mode", "Enabled" if online_mode_enabled else "Disabled")
|
||||
table.add_row("Max Token", str(MAX_TOKEN))
|
||||
table.add_row("Session Token", "[Not set]" if session_max_token == 0 else str(session_max_token))
|
||||
table.add_row("Session System Prompt", session_system_prompt or "[Not set]")
|
||||
@@ -1222,7 +1341,7 @@ def chat():
|
||||
table.add_row("Used Credits", "[Unavailable - Check API key]")
|
||||
table.add_row("Credits Left", "[Unavailable - Check API key]")
|
||||
|
||||
console.print(Panel(table, title="[bold green]Current Configurations[/]", title_align="left", subtitle="[bold green]oAI Version %s" % version, subtitle_align="right"))
|
||||
console.print(Panel(table, title="[bold green]Current Configurations[/]", title_align="left", subtitle="%s" %(update), subtitle_align="right"))
|
||||
continue
|
||||
|
||||
if user_input.lower() == "/credits":
|
||||
@@ -1250,7 +1369,7 @@ def chat():
|
||||
if user_input.lower() == "/help":
|
||||
help_table = Table("Command", "Description", "Example", show_header=True, header_style="bold cyan", width=console.width - 10)
|
||||
|
||||
# ===== SESSION COMMANDS =====
|
||||
# SESSION COMMANDS
|
||||
help_table.add_row(
|
||||
"[bold yellow]━━━ SESSION COMMANDS ━━━[/]",
|
||||
"",
|
||||
@@ -1278,7 +1397,7 @@ def chat():
|
||||
)
|
||||
help_table.add_row(
|
||||
"/online [on|off]",
|
||||
"Enable/disable online mode (web search) for current model. Only works with models that support tools.",
|
||||
"Enable/disable online mode (web search) for current session. Overrides default setting.",
|
||||
"/online on\n/online off"
|
||||
)
|
||||
help_table.add_row(
|
||||
@@ -1302,7 +1421,7 @@ def chat():
|
||||
"/retry"
|
||||
)
|
||||
|
||||
# ===== MODEL COMMANDS =====
|
||||
# MODEL COMMANDS
|
||||
help_table.add_row(
|
||||
"[bold yellow]━━━ MODEL COMMANDS ━━━[/]",
|
||||
"",
|
||||
@@ -1319,7 +1438,7 @@ def chat():
|
||||
"/model\n/model gpt"
|
||||
)
|
||||
|
||||
# ===== CONFIGURATION COMMANDS (ALPHABETICAL) =====
|
||||
# CONFIGURATION COMMANDS
|
||||
help_table.add_row(
|
||||
"[bold yellow]━━━ CONFIGURATION ━━━[/]",
|
||||
"",
|
||||
@@ -1340,6 +1459,11 @@ def chat():
|
||||
"Set the cost warning threshold. Alerts when response exceeds this cost (in USD).",
|
||||
"/config costwarning 0.05"
|
||||
)
|
||||
help_table.add_row(
|
||||
"/config log [size_mb]",
|
||||
"Set log file size limit in MB. Older logs are rotated automatically. Requires restart.",
|
||||
"/config log 20"
|
||||
)
|
||||
help_table.add_row(
|
||||
"/config maxtoken [value]",
|
||||
"Set stored max token limit (persisted in DB). View current if no value provided.",
|
||||
@@ -1350,6 +1474,11 @@ def chat():
|
||||
"Set default model that loads on startup. Shows image and online capabilities. Doesn't change current session model.",
|
||||
"/config model gpt"
|
||||
)
|
||||
help_table.add_row(
|
||||
"/config online [on|off]",
|
||||
"Set default online mode for new model selections. Use '/online on|off' to override current session.",
|
||||
"/config online on"
|
||||
)
|
||||
help_table.add_row(
|
||||
"/config stream [on|off]",
|
||||
"Enable or disable response streaming.",
|
||||
@@ -1361,7 +1490,7 @@ def chat():
|
||||
"/config url"
|
||||
)
|
||||
|
||||
# ===== TOKEN & SYSTEM COMMANDS =====
|
||||
# TOKEN & SYSTEM COMMANDS
|
||||
help_table.add_row(
|
||||
"[bold yellow]━━━ TOKEN & SYSTEM ━━━[/]",
|
||||
"",
|
||||
@@ -1383,7 +1512,7 @@ def chat():
|
||||
"/system You are a Python expert"
|
||||
)
|
||||
|
||||
# ===== CONVERSATION MANAGEMENT =====
|
||||
# CONVERSATION MANAGEMENT
|
||||
help_table.add_row(
|
||||
"[bold yellow]━━━ CONVERSATION MGMT ━━━[/]",
|
||||
"",
|
||||
@@ -1415,7 +1544,7 @@ def chat():
|
||||
"/save my_chat"
|
||||
)
|
||||
|
||||
# ===== MONITORING & STATS =====
|
||||
# MONITORING & STATS
|
||||
help_table.add_row(
|
||||
"[bold yellow]━━━ MONITORING & STATS ━━━[/]",
|
||||
"",
|
||||
@@ -1432,7 +1561,7 @@ def chat():
|
||||
"/stats"
|
||||
)
|
||||
|
||||
# ===== FILE ATTACHMENTS =====
|
||||
# INPUT METHODS
|
||||
help_table.add_row(
|
||||
"[bold yellow]━━━ INPUT METHODS ━━━[/]",
|
||||
"",
|
||||
@@ -1454,7 +1583,7 @@ def chat():
|
||||
"//help sends '/help' as text"
|
||||
)
|
||||
|
||||
# ===== EXIT =====
|
||||
# EXIT
|
||||
help_table.add_row(
|
||||
"[bold yellow]━━━ EXIT ━━━[/]",
|
||||
"",
|
||||
@@ -1514,7 +1643,6 @@ def chat():
|
||||
# Handle PDFs
|
||||
elif mime_type == 'application/pdf' or file_ext == '.pdf':
|
||||
modalities = selected_model.get("architecture", {}).get("input_modalities", [])
|
||||
# Check for various possible modality indicators for PDFs
|
||||
supports_pdf = any(mod in modalities for mod in ["document", "pdf", "file"])
|
||||
if not supports_pdf:
|
||||
console.print("[bold red]Selected model does not support PDF attachments.[/]")
|
||||
@@ -1560,13 +1688,10 @@ def chat():
|
||||
# Build API messages with conversation history if memory is enabled
|
||||
api_messages = []
|
||||
|
||||
# Add system prompt if set
|
||||
if session_system_prompt:
|
||||
api_messages.append({"role": "system", "content": session_system_prompt})
|
||||
|
||||
# Add conversation history only if memory is enabled (from memory start point onwards)
|
||||
if conversation_memory_enabled:
|
||||
# Only include history from when memory was last enabled
|
||||
for i in range(memory_start_index, len(session_history)):
|
||||
history_entry = session_history[i]
|
||||
api_messages.append({
|
||||
@@ -1578,13 +1703,12 @@ def chat():
|
||||
"content": history_entry['response']
|
||||
})
|
||||
|
||||
# Add current user message
|
||||
api_messages.append({"role": "user", "content": message_content})
|
||||
|
||||
# Get effective model ID with :online suffix if enabled
|
||||
effective_model_id = get_effective_model_id(selected_model["id"], online_mode_enabled)
|
||||
|
||||
# Build API params with app identification headers (using http_headers)
|
||||
# Build API params
|
||||
api_params = {
|
||||
"model": effective_model_id,
|
||||
"messages": api_messages,
|
||||
@@ -1606,17 +1730,17 @@ def chat():
|
||||
online_status = "ON" if online_mode_enabled else "OFF"
|
||||
app_logger.info(f"API Request: Model '{effective_model_id}' (Online: {online_status}), Prompt length: {len(text_part)} chars, {file_count} file(s) attached, Memory: {memory_status}, History sent: {history_messages_count} messages, Transforms: middle-out {'enabled' if middle_out_enabled else 'disabled'}, App: {APP_NAME} ({APP_URL}).")
|
||||
|
||||
# Send and handle response with metrics and timing
|
||||
# Send and handle response
|
||||
is_streaming = STREAM_ENABLED == "on"
|
||||
if is_streaming:
|
||||
console.print("[bold green]Streaming response...[/] [dim](Press Ctrl+C to cancel)[/]")
|
||||
if online_mode_enabled:
|
||||
console.print("[dim cyan]🌐 Online mode active - model has web search access[/]")
|
||||
console.print("") # Add spacing before response
|
||||
console.print("")
|
||||
else:
|
||||
console.print("[bold green]Thinking...[/]", end="\r")
|
||||
|
||||
start_time = time.time() # Start timing request
|
||||
start_time = time.time()
|
||||
try:
|
||||
response = client.chat.send(**api_params)
|
||||
app_logger.info(f"API call successful for model '{effective_model_id}'")
|
||||
@@ -1625,12 +1749,11 @@ def chat():
|
||||
app_logger.error(f"API Error: {type(e).__name__}: {e}")
|
||||
continue
|
||||
|
||||
response_time = time.time() - start_time # Calculate response time
|
||||
response_time = time.time() - start_time
|
||||
|
||||
full_response = ""
|
||||
if is_streaming:
|
||||
try:
|
||||
# Use Live display for smooth streaming with proper wrapping
|
||||
with Live("", console=console, refresh_per_second=10, auto_refresh=True) as live:
|
||||
for chunk in response:
|
||||
if hasattr(chunk, 'error') and chunk.error:
|
||||
@@ -1640,11 +1763,9 @@ def chat():
|
||||
if hasattr(chunk.choices[0].delta, 'content') and chunk.choices[0].delta.content:
|
||||
content_chunk = chunk.choices[0].delta.content
|
||||
full_response += content_chunk
|
||||
# Update live display with markdown rendering
|
||||
md = Markdown(full_response)
|
||||
live.update(md)
|
||||
|
||||
# Add newline after streaming completes
|
||||
console.print("")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
@@ -1656,15 +1777,14 @@ def chat():
|
||||
console.print(f"\r{' ' * 20}\r", end="")
|
||||
|
||||
if full_response:
|
||||
# Render response with proper markdown formatting
|
||||
if not is_streaming: # Only show panel for non-streaming (streaming already displayed)
|
||||
if not is_streaming:
|
||||
md = Markdown(full_response)
|
||||
console.print(Panel(md, title="[bold green]AI Response[/]", title_align="left", border_style="green"))
|
||||
|
||||
session_history.append({'prompt': user_input, 'response': full_response})
|
||||
current_index = len(session_history) - 1
|
||||
|
||||
# Process metrics for per-message display and session tracking
|
||||
# Process metrics
|
||||
usage = getattr(response, 'usage', None)
|
||||
input_tokens = usage.input_tokens if usage and hasattr(usage, 'input_tokens') else 0
|
||||
output_tokens = usage.output_tokens if usage and hasattr(usage, 'output_tokens') else 0
|
||||
@@ -1675,10 +1795,9 @@ def chat():
|
||||
total_cost += msg_cost
|
||||
message_count += 1
|
||||
|
||||
# Log response metrics
|
||||
app_logger.info(f"Response: Tokens - I:{input_tokens} O:{output_tokens} T:{input_tokens + output_tokens}, Cost: ${msg_cost:.4f}, Time: {response_time:.2f}s, Online: {online_mode_enabled}")
|
||||
|
||||
# Per-message metrics display with context info
|
||||
# Per-message metrics display
|
||||
if conversation_memory_enabled:
|
||||
context_count = len(session_history) - memory_start_index
|
||||
context_info = f", Context: {context_count} msg(s)" if context_count > 1 else ""
|
||||
@@ -1701,7 +1820,6 @@ def chat():
|
||||
console.print(f"[bold red]⚠️ {warning_text}[/]")
|
||||
app_logger.warning(f"Warnings triggered: {warning_text}")
|
||||
|
||||
# Add spacing before copy prompt
|
||||
console.print("")
|
||||
try:
|
||||
copy_choice = input("💾 Type 'c' to copy response, or press Enter to continue: ").strip().lower()
|
||||
@@ -1711,7 +1829,6 @@ def chat():
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
pass
|
||||
|
||||
# Add spacing after interaction
|
||||
console.print("")
|
||||
else:
|
||||
console.print("[bold red]No response received.[/]")
|
||||
|
||||
@@ -14,8 +14,9 @@ markdown2==2.5.4
|
||||
mdurl==0.1.2
|
||||
natsort==8.4.0
|
||||
openrouter==0.0.19
|
||||
packaging==25.0
|
||||
pipreqs==0.4.13
|
||||
prompt_toolkit==3.0.52
|
||||
prompt-toolkit==3.0.52
|
||||
Pygments==2.19.2
|
||||
pyperclip==1.11.0
|
||||
python-dateutil==2.9.0.post0
|
||||
@@ -30,7 +31,7 @@ soupsieve==2.8
|
||||
svgwrite==1.4.3
|
||||
tqdm==4.67.1
|
||||
typer==0.20.0
|
||||
typing_extensions==4.15.0
|
||||
typing-extensions==4.15.0
|
||||
urllib3==2.5.0
|
||||
wavedrom==2.0.3.post3
|
||||
wcwidth==0.2.14
|
||||
|
||||
Reference in New Issue
Block a user