1.9.6 (#1)
New functionality and bugfixes. Reviewed-on: #1 Co-authored-by: Rune Olsen <rune@rune.pm> Co-committed-by: Rune Olsen <rune@rune.pm>
This commit was merged in pull request #1.
This commit is contained in:
@@ -23,7 +23,7 @@ oAI is a command-line chat application that provides an interactive interface to
|
|||||||
- Python 3.7 or higher
|
- Python 3.7 or higher
|
||||||
- OpenRouter API key (get one at https://openrouter.ai)
|
- OpenRouter API key (get one at https://openrouter.ai)
|
||||||
|
|
||||||
## Screenshot
|
## Screenshot (<span style="font-size:0.8em;">from version 1.0</span>)
|
||||||
|
|
||||||
[<img src="https://gitlab.pm/rune/oai/raw/branch/main/images/screenshot_01.png">](https://gitlab.pm/rune/oai/src/branch/main/README.md)
|
[<img src="https://gitlab.pm/rune/oai/raw/branch/main/images/screenshot_01.png">](https://gitlab.pm/rune/oai/src/branch/main/README.md)
|
||||||
|
|
||||||
|
|||||||
486
oai.py
486
oai.py
@@ -30,7 +30,7 @@ from packaging import version as pkg_version
|
|||||||
import io # Added for custom handler
|
import io # Added for custom handler
|
||||||
|
|
||||||
# App version. Changes by author with new releases.
|
# App version. Changes by author with new releases.
|
||||||
version = '1.9.5'
|
version = '1.9.6'
|
||||||
|
|
||||||
app = typer.Typer()
|
app = typer.Typer()
|
||||||
|
|
||||||
@@ -60,6 +60,218 @@ VALID_COMMANDS = {
|
|||||||
'/info', '/model', '/maxtoken', '/system', '/config', '/credits', '/clear', '/cl', '/help'
|
'/info', '/model', '/maxtoken', '/system', '/config', '/credits', '/clear', '/cl', '/help'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Detailed command help database
|
||||||
|
COMMAND_HELP = {
|
||||||
|
'/clear': {
|
||||||
|
'aliases': ['/cl'],
|
||||||
|
'description': 'Clear the terminal screen for a clean interface.',
|
||||||
|
'usage': '/clear\n/cl',
|
||||||
|
'examples': [
|
||||||
|
('Clear screen', '/clear'),
|
||||||
|
('Using short alias', '/cl'),
|
||||||
|
],
|
||||||
|
'notes': 'You can also use the keyboard shortcut Ctrl+L to clear the screen.'
|
||||||
|
},
|
||||||
|
'/help': {
|
||||||
|
'description': 'Display help information for commands.',
|
||||||
|
'usage': '/help [command]',
|
||||||
|
'examples': [
|
||||||
|
('Show all commands', '/help'),
|
||||||
|
('Get help for a specific command', '/help /model'),
|
||||||
|
('Get help for config', '/help /config'),
|
||||||
|
],
|
||||||
|
'notes': 'Use /help without arguments to see the full command list, or /help <command> for detailed information about a specific command.'
|
||||||
|
},
|
||||||
|
'/memory': {
|
||||||
|
'description': 'Toggle conversation memory. When ON, the AI remembers conversation history. When OFF, each request is independent (saves tokens and cost).',
|
||||||
|
'usage': '/memory [on|off]',
|
||||||
|
'examples': [
|
||||||
|
('Check current memory status', '/memory'),
|
||||||
|
('Enable conversation memory', '/memory on'),
|
||||||
|
('Disable memory (save costs)', '/memory off'),
|
||||||
|
],
|
||||||
|
'notes': 'Memory is ON by default. Disabling memory reduces API costs but the AI won\'t remember previous messages. Messages are still saved locally for your reference.'
|
||||||
|
},
|
||||||
|
'/online': {
|
||||||
|
'description': 'Enable or disable online mode (web search capabilities) for the current session.',
|
||||||
|
'usage': '/online [on|off]',
|
||||||
|
'examples': [
|
||||||
|
('Check online mode status', '/online'),
|
||||||
|
('Enable web search', '/online on'),
|
||||||
|
('Disable web search', '/online off'),
|
||||||
|
],
|
||||||
|
'notes': 'Not all models support online mode. The model must have "tools" parameter support. This setting overrides the default online mode configured with /config online.'
|
||||||
|
},
|
||||||
|
'/paste': {
|
||||||
|
'description': 'Paste plain text or code from clipboard and send to the AI. Optionally add a prompt.',
|
||||||
|
'usage': '/paste [prompt]',
|
||||||
|
'examples': [
|
||||||
|
('Paste clipboard content', '/paste'),
|
||||||
|
('Paste with a question', '/paste Explain this code'),
|
||||||
|
('Paste and ask for review', '/paste Review this for bugs'),
|
||||||
|
],
|
||||||
|
'notes': 'Only plain text is supported. Binary clipboard data will be rejected. The clipboard content is shown as a preview before sending.'
|
||||||
|
},
|
||||||
|
'/retry': {
|
||||||
|
'description': 'Resend the last prompt from conversation history.',
|
||||||
|
'usage': '/retry',
|
||||||
|
'examples': [
|
||||||
|
('Retry last message', '/retry'),
|
||||||
|
],
|
||||||
|
'notes': 'Useful when you get an error or want a different response to the same prompt. Requires at least one message in history.'
|
||||||
|
},
|
||||||
|
'/next': {
|
||||||
|
'description': 'View the next response in conversation history.',
|
||||||
|
'usage': '/next',
|
||||||
|
'examples': [
|
||||||
|
('Navigate to next response', '/next'),
|
||||||
|
],
|
||||||
|
'notes': 'Navigate through conversation history. Use /prev to go backward.'
|
||||||
|
},
|
||||||
|
'/prev': {
|
||||||
|
'description': 'View the previous response in conversation history.',
|
||||||
|
'usage': '/prev',
|
||||||
|
'examples': [
|
||||||
|
('Navigate to previous response', '/prev'),
|
||||||
|
],
|
||||||
|
'notes': 'Navigate through conversation history. Use /next to go forward.'
|
||||||
|
},
|
||||||
|
'/reset': {
|
||||||
|
'description': 'Clear conversation history and reset system prompt. This resets all session metrics.',
|
||||||
|
'usage': '/reset',
|
||||||
|
'examples': [
|
||||||
|
('Reset conversation', '/reset'),
|
||||||
|
],
|
||||||
|
'notes': 'Requires confirmation. This clears all message history, resets the system prompt, and resets token/cost counters. Use when starting a completely new conversation topic.'
|
||||||
|
},
|
||||||
|
'/info': {
|
||||||
|
'description': 'Display detailed information about a model including pricing, capabilities, context length, and online support.',
|
||||||
|
'usage': '/info [model_id]',
|
||||||
|
'examples': [
|
||||||
|
('Show current model info', '/info'),
|
||||||
|
('Show specific model info', '/info gpt-4o'),
|
||||||
|
('Check model capabilities', '/info claude-3-opus'),
|
||||||
|
],
|
||||||
|
'notes': 'Without arguments, shows info for the currently selected model. Displays pricing per million tokens, supported modalities (text, image, etc.), and parameter support.'
|
||||||
|
},
|
||||||
|
'/model': {
|
||||||
|
'description': 'Select or change the AI model for the current session. Shows image and online capabilities.',
|
||||||
|
'usage': '/model [search_term]',
|
||||||
|
'examples': [
|
||||||
|
('List all models', '/model'),
|
||||||
|
('Search for GPT models', '/model gpt'),
|
||||||
|
('Search for Claude models', '/model claude'),
|
||||||
|
],
|
||||||
|
'notes': 'Models are numbered for easy selection. The table shows Image (✓ if model accepts images) and Online (✓ if model supports web search) columns.'
|
||||||
|
},
|
||||||
|
'/config': {
|
||||||
|
'description': 'View or modify application configuration settings.',
|
||||||
|
'usage': '/config [setting] [value]',
|
||||||
|
'examples': [
|
||||||
|
('View all settings', '/config'),
|
||||||
|
('Set API key', '/config api'),
|
||||||
|
('Set default model', '/config model'),
|
||||||
|
('Enable streaming', '/config stream on'),
|
||||||
|
('Set cost warning threshold', '/config costwarning 0.05'),
|
||||||
|
('Set log level', '/config loglevel debug'),
|
||||||
|
('Set default online mode', '/config online on'),
|
||||||
|
],
|
||||||
|
'notes': 'Available settings: api (API key), url (base URL), model (default model), stream (on/off), costwarning (threshold $), maxtoken (limit), online (default on/off), log (size MB), loglevel (debug/info/warning/error/critical).'
|
||||||
|
},
|
||||||
|
'/maxtoken': {
|
||||||
|
'description': 'Set a temporary session token limit (cannot exceed stored max token limit).',
|
||||||
|
'usage': '/maxtoken [value]',
|
||||||
|
'examples': [
|
||||||
|
('View current session limit', '/maxtoken'),
|
||||||
|
('Set session limit to 2000', '/maxtoken 2000'),
|
||||||
|
('Set to 50000', '/maxtoken 50000'),
|
||||||
|
],
|
||||||
|
'notes': 'This is a session-only setting and cannot exceed the stored max token limit (set with /config maxtoken). View without arguments to see current value.'
|
||||||
|
},
|
||||||
|
'/system': {
|
||||||
|
'description': 'Set or clear the session-level system prompt to guide AI behavior.',
|
||||||
|
'usage': '/system [prompt|clear]',
|
||||||
|
'examples': [
|
||||||
|
('View current system prompt', '/system'),
|
||||||
|
('Set as Python expert', '/system You are a Python expert'),
|
||||||
|
('Set as code reviewer', '/system You are a senior code reviewer. Focus on bugs and best practices.'),
|
||||||
|
('Clear system prompt', '/system clear'),
|
||||||
|
],
|
||||||
|
'notes': 'System prompts influence how the AI responds throughout the session. Use "clear" to remove the current system prompt.'
|
||||||
|
},
|
||||||
|
'/save': {
|
||||||
|
'description': 'Save the current conversation history to the database.',
|
||||||
|
'usage': '/save <name>',
|
||||||
|
'examples': [
|
||||||
|
('Save conversation', '/save my_chat'),
|
||||||
|
('Save with descriptive name', '/save python_debugging_2024'),
|
||||||
|
],
|
||||||
|
'notes': 'Saved conversations can be loaded later with /load. Use descriptive names to easily find them later.'
|
||||||
|
},
|
||||||
|
'/load': {
|
||||||
|
'description': 'Load a saved conversation from the database by name or number.',
|
||||||
|
'usage': '/load <name|number>',
|
||||||
|
'examples': [
|
||||||
|
('Load by name', '/load my_chat'),
|
||||||
|
('Load by number from /list', '/load 3'),
|
||||||
|
],
|
||||||
|
'notes': 'Use /list to see numbered conversations. Loading a conversation replaces current history and resets session metrics.'
|
||||||
|
},
|
||||||
|
'/delete': {
|
||||||
|
'description': 'Delete a saved conversation from the database. Requires confirmation.',
|
||||||
|
'usage': '/delete <name|number>',
|
||||||
|
'examples': [
|
||||||
|
('Delete by name', '/delete my_chat'),
|
||||||
|
('Delete by number from /list', '/delete 3'),
|
||||||
|
],
|
||||||
|
'notes': 'Use /list to see numbered conversations. This action cannot be undone!'
|
||||||
|
},
|
||||||
|
'/list': {
|
||||||
|
'description': 'List all saved conversations with numbers, message counts, and timestamps.',
|
||||||
|
'usage': '/list',
|
||||||
|
'examples': [
|
||||||
|
('Show saved conversations', '/list'),
|
||||||
|
],
|
||||||
|
'notes': 'Conversations are numbered for easy use with /load and /delete commands. Shows message count and last saved time.'
|
||||||
|
},
|
||||||
|
'/export': {
|
||||||
|
'description': 'Export the current conversation to a file in various formats.',
|
||||||
|
'usage': '/export <format> <filename>',
|
||||||
|
'examples': [
|
||||||
|
('Export as Markdown', '/export md notes.md'),
|
||||||
|
('Export as JSON', '/export json conversation.json'),
|
||||||
|
('Export as HTML', '/export html report.html'),
|
||||||
|
],
|
||||||
|
'notes': 'Available formats: md (Markdown), json (JSON), html (HTML). The export includes all messages and the system prompt if set.'
|
||||||
|
},
|
||||||
|
'/stats': {
|
||||||
|
'description': 'Display session statistics including tokens, costs, and credits.',
|
||||||
|
'usage': '/stats',
|
||||||
|
'examples': [
|
||||||
|
('View session statistics', '/stats'),
|
||||||
|
],
|
||||||
|
'notes': 'Shows total input/output tokens, total cost, average cost per message, and remaining credits. Also displays credit warnings if applicable.'
|
||||||
|
},
|
||||||
|
'/credits': {
|
||||||
|
'description': 'Display your OpenRouter account credits and usage.',
|
||||||
|
'usage': '/credits',
|
||||||
|
'examples': [
|
||||||
|
('Check credits', '/credits'),
|
||||||
|
],
|
||||||
|
'notes': 'Shows total credits, used credits, and credits left. Displays warnings if credits are low (< $1 or < 10% of total).'
|
||||||
|
},
|
||||||
|
'/middleout': {
|
||||||
|
'description': 'Enable or disable middle-out transform to compress prompts exceeding context size.',
|
||||||
|
'usage': '/middleout [on|off]',
|
||||||
|
'examples': [
|
||||||
|
('Check status', '/middleout'),
|
||||||
|
('Enable compression', '/middleout on'),
|
||||||
|
('Disable compression', '/middleout off'),
|
||||||
|
],
|
||||||
|
'notes': 'Middle-out transform intelligently compresses long prompts to fit within model context limits. Useful for very long conversations.'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
# Supported code file extensions
|
# Supported code file extensions
|
||||||
SUPPORTED_CODE_EXTENSIONS = {
|
SUPPORTED_CODE_EXTENSIONS = {
|
||||||
'.py', '.js', '.ts', '.cs', '.java', '.c', '.cpp', '.h', '.hpp',
|
'.py', '.js', '.ts', '.cs', '.java', '.c', '.cpp', '.h', '.hpp',
|
||||||
@@ -77,6 +289,15 @@ 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
|
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
|
HIGH_COST_WARNING = "cost_warning_threshold" # Configurable key for cost threshold, default $0.01
|
||||||
|
|
||||||
|
# Valid log levels mapping
|
||||||
|
VALID_LOG_LEVELS = {
|
||||||
|
'debug': logging.DEBUG,
|
||||||
|
'info': logging.INFO,
|
||||||
|
'warning': logging.WARNING,
|
||||||
|
'error': logging.ERROR,
|
||||||
|
'critical': logging.CRITICAL
|
||||||
|
}
|
||||||
|
|
||||||
# DB configuration
|
# DB configuration
|
||||||
database = config_dir / 'oai_config.db'
|
database = config_dir / 'oai_config.db'
|
||||||
DB_FILE = str(database)
|
DB_FILE = str(database)
|
||||||
@@ -148,27 +369,131 @@ class RotatingRichHandler(RotatingFileHandler):
|
|||||||
except Exception:
|
except Exception:
|
||||||
self.handleError(record)
|
self.handleError(record)
|
||||||
|
|
||||||
# Get log configuration from DB
|
# ============================================================================
|
||||||
|
# LOGGING SETUP - MUST BE DONE AFTER CONFIG IS LOADED
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Load log configuration from DB FIRST (before creating handler)
|
||||||
LOG_MAX_SIZE_MB = int(get_config('log_max_size_mb') or "10")
|
LOG_MAX_SIZE_MB = int(get_config('log_max_size_mb') or "10")
|
||||||
LOG_BACKUP_COUNT = int(get_config('log_backup_count') or "2")
|
LOG_BACKUP_COUNT = int(get_config('log_backup_count') or "2")
|
||||||
|
LOG_LEVEL_STR = get_config('log_level') or "info"
|
||||||
|
LOG_LEVEL = VALID_LOG_LEVELS.get(LOG_LEVEL_STR.lower(), logging.INFO)
|
||||||
|
|
||||||
# Create the custom rotating handler
|
# Global reference to the handler for dynamic reloading
|
||||||
app_handler = RotatingRichHandler(
|
app_handler = None
|
||||||
filename=str(log_file),
|
app_logger = None
|
||||||
maxBytes=LOG_MAX_SIZE_MB * 1024 * 1024, # Convert MB to bytes
|
|
||||||
backupCount=LOG_BACKUP_COUNT,
|
|
||||||
encoding='utf-8'
|
|
||||||
)
|
|
||||||
|
|
||||||
logging.basicConfig(
|
def setup_logging():
|
||||||
level=logging.NOTSET,
|
"""Setup or reset logging configuration with current settings."""
|
||||||
format="%(message)s", # Rich formats it
|
global app_handler, LOG_MAX_SIZE_MB, LOG_BACKUP_COUNT, LOG_LEVEL, app_logger
|
||||||
datefmt="[%X]",
|
|
||||||
handlers=[app_handler]
|
# Get the root logger
|
||||||
)
|
root_logger = logging.getLogger()
|
||||||
|
|
||||||
|
# Remove existing handler if present
|
||||||
|
if app_handler is not None:
|
||||||
|
root_logger.removeHandler(app_handler)
|
||||||
|
try:
|
||||||
|
app_handler.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Check if log file needs immediate rotation
|
||||||
|
if os.path.exists(log_file):
|
||||||
|
current_size = os.path.getsize(log_file)
|
||||||
|
max_bytes = LOG_MAX_SIZE_MB * 1024 * 1024
|
||||||
|
|
||||||
|
if current_size >= max_bytes:
|
||||||
|
# Perform immediate rotation
|
||||||
|
import shutil
|
||||||
|
timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||||
|
backup_file = f"{log_file}.{timestamp}"
|
||||||
|
try:
|
||||||
|
shutil.move(str(log_file), backup_file)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Could not rotate log file: {e}")
|
||||||
|
|
||||||
|
# Clean up old backups if exceeding limit
|
||||||
|
log_dir = os.path.dirname(log_file)
|
||||||
|
log_basename = os.path.basename(log_file)
|
||||||
|
backup_pattern = f"{log_basename}.*"
|
||||||
|
|
||||||
|
import glob
|
||||||
|
backups = sorted(glob.glob(os.path.join(log_dir, backup_pattern)))
|
||||||
|
|
||||||
|
# Keep only the most recent backups
|
||||||
|
while len(backups) > LOG_BACKUP_COUNT:
|
||||||
|
oldest = backups.pop(0)
|
||||||
|
try:
|
||||||
|
os.remove(oldest)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Create new handler with current settings
|
||||||
|
app_handler = RotatingRichHandler(
|
||||||
|
filename=str(log_file),
|
||||||
|
maxBytes=LOG_MAX_SIZE_MB * 1024 * 1024,
|
||||||
|
backupCount=LOG_BACKUP_COUNT,
|
||||||
|
encoding='utf-8'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set handler level to NOTSET so it processes all records
|
||||||
|
app_handler.setLevel(logging.NOTSET)
|
||||||
|
|
||||||
|
# Configure root logger - set to WARNING to suppress third-party library noise
|
||||||
|
root_logger.setLevel(logging.WARNING)
|
||||||
|
root_logger.addHandler(app_handler)
|
||||||
|
|
||||||
|
# Suppress noisy third-party loggers
|
||||||
|
# These libraries create DEBUG logs that pollute our log file
|
||||||
|
logging.getLogger('asyncio').setLevel(logging.WARNING)
|
||||||
|
logging.getLogger('urllib3').setLevel(logging.WARNING)
|
||||||
|
logging.getLogger('requests').setLevel(logging.WARNING)
|
||||||
|
logging.getLogger('httpx').setLevel(logging.WARNING)
|
||||||
|
logging.getLogger('httpcore').setLevel(logging.WARNING)
|
||||||
|
logging.getLogger('openai').setLevel(logging.WARNING)
|
||||||
|
logging.getLogger('openrouter').setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
# Get or create app logger and set its level (this filters what gets logged)
|
||||||
|
app_logger = logging.getLogger("oai_app")
|
||||||
|
app_logger.setLevel(LOG_LEVEL)
|
||||||
|
# Don't propagate to avoid root logger filtering
|
||||||
|
app_logger.propagate = True
|
||||||
|
|
||||||
|
return app_logger
|
||||||
|
|
||||||
app_logger = logging.getLogger("oai_app")
|
# Initial logging setup
|
||||||
app_logger.setLevel(logging.INFO)
|
app_logger = setup_logging()
|
||||||
|
|
||||||
|
def set_log_level(level_str: str) -> bool:
|
||||||
|
"""Set the application log level. Returns True if successful."""
|
||||||
|
global LOG_LEVEL, LOG_LEVEL_STR, app_logger
|
||||||
|
level_str_lower = level_str.lower()
|
||||||
|
if level_str_lower not in VALID_LOG_LEVELS:
|
||||||
|
return False
|
||||||
|
LOG_LEVEL = VALID_LOG_LEVELS[level_str_lower]
|
||||||
|
LOG_LEVEL_STR = level_str_lower
|
||||||
|
|
||||||
|
# Update the logger level immediately
|
||||||
|
if app_logger:
|
||||||
|
app_logger.setLevel(LOG_LEVEL)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def reload_logging_config():
|
||||||
|
"""Reload logging configuration from database and reinitialize handler."""
|
||||||
|
global LOG_MAX_SIZE_MB, LOG_BACKUP_COUNT, LOG_LEVEL, LOG_LEVEL_STR, app_logger
|
||||||
|
|
||||||
|
# Reload from database
|
||||||
|
LOG_MAX_SIZE_MB = int(get_config('log_max_size_mb') or "10")
|
||||||
|
LOG_BACKUP_COUNT = int(get_config('log_backup_count') or "2")
|
||||||
|
LOG_LEVEL_STR = get_config('log_level') or "info"
|
||||||
|
LOG_LEVEL = VALID_LOG_LEVELS.get(LOG_LEVEL_STR.lower(), logging.INFO)
|
||||||
|
|
||||||
|
# Reinitialize logging
|
||||||
|
app_logger = setup_logging()
|
||||||
|
|
||||||
|
return app_logger
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# END OF LOGGING SETUP
|
# END OF LOGGING SETUP
|
||||||
@@ -408,7 +733,64 @@ def export_as_html(session_history: List[Dict[str, str]], session_system_prompt:
|
|||||||
|
|
||||||
return "\n".join(html_parts)
|
return "\n".join(html_parts)
|
||||||
|
|
||||||
# Load configs
|
def show_command_help(command: str):
|
||||||
|
"""Display detailed help for a specific command."""
|
||||||
|
# Normalize command to ensure it starts with /
|
||||||
|
if not command.startswith('/'):
|
||||||
|
command = '/' + command
|
||||||
|
|
||||||
|
# Check if command exists
|
||||||
|
if command not in COMMAND_HELP:
|
||||||
|
console.print(f"[bold red]Unknown command: {command}[/]")
|
||||||
|
console.print("[bold yellow]Type /help to see all available commands.[/]")
|
||||||
|
app_logger.warning(f"Help requested for unknown command: {command}")
|
||||||
|
return
|
||||||
|
|
||||||
|
help_data = COMMAND_HELP[command]
|
||||||
|
|
||||||
|
# Create detailed help panel
|
||||||
|
help_content = []
|
||||||
|
|
||||||
|
# Aliases if available
|
||||||
|
if 'aliases' in help_data:
|
||||||
|
aliases_str = ", ".join(help_data['aliases'])
|
||||||
|
help_content.append(f"[bold cyan]Aliases:[/] {aliases_str}")
|
||||||
|
help_content.append("")
|
||||||
|
|
||||||
|
# Description
|
||||||
|
help_content.append(f"[bold cyan]Description:[/]")
|
||||||
|
help_content.append(help_data['description'])
|
||||||
|
help_content.append("")
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
help_content.append(f"[bold cyan]Usage:[/]")
|
||||||
|
help_content.append(f"[yellow]{help_data['usage']}[/]")
|
||||||
|
help_content.append("")
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
if 'examples' in help_data and help_data['examples']:
|
||||||
|
help_content.append(f"[bold cyan]Examples:[/]")
|
||||||
|
for desc, example in help_data['examples']:
|
||||||
|
help_content.append(f" [dim]{desc}:[/]")
|
||||||
|
help_content.append(f" [green]{example}[/]")
|
||||||
|
help_content.append("")
|
||||||
|
|
||||||
|
# Notes
|
||||||
|
if 'notes' in help_data:
|
||||||
|
help_content.append(f"[bold cyan]Notes:[/]")
|
||||||
|
help_content.append(f"[dim]{help_data['notes']}[/]")
|
||||||
|
|
||||||
|
console.print(Panel(
|
||||||
|
"\n".join(help_content),
|
||||||
|
title=f"[bold green]Help: {command}[/]",
|
||||||
|
title_align="left",
|
||||||
|
border_style="green",
|
||||||
|
width=console.width - 4
|
||||||
|
))
|
||||||
|
|
||||||
|
app_logger.info(f"Displayed detailed help for command: {command}")
|
||||||
|
|
||||||
|
# Load configs (AFTER logging is set up)
|
||||||
API_KEY = get_config('api_key')
|
API_KEY = get_config('api_key')
|
||||||
OPENROUTER_BASE_URL = get_config('base_url') or "https://openrouter.ai/api/v1"
|
OPENROUTER_BASE_URL = get_config('base_url') or "https://openrouter.ai/api/v1"
|
||||||
STREAM_ENABLED = get_config('stream_enabled') or "on"
|
STREAM_ENABLED = get_config('stream_enabled') or "on"
|
||||||
@@ -618,7 +1000,7 @@ def display_paginated_table(table: Table, title: str):
|
|||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def chat():
|
def chat():
|
||||||
global API_KEY, OPENROUTER_BASE_URL, STREAM_ENABLED, MAX_TOKEN, COST_WARNING_THRESHOLD, DEFAULT_ONLINE_MODE, LOG_MAX_SIZE_MB, LOG_BACKUP_COUNT
|
global API_KEY, OPENROUTER_BASE_URL, STREAM_ENABLED, MAX_TOKEN, COST_WARNING_THRESHOLD, DEFAULT_ONLINE_MODE, LOG_MAX_SIZE_MB, LOG_BACKUP_COUNT, LOG_LEVEL, LOG_LEVEL_STR, app_logger
|
||||||
session_max_token = 0
|
session_max_token = 0
|
||||||
session_system_prompt = ""
|
session_system_prompt = ""
|
||||||
session_history = []
|
session_history = []
|
||||||
@@ -1028,9 +1410,13 @@ def chat():
|
|||||||
console.print("[bold yellow]Usage: /middleout on|off (or /middleout to view status)[/]")
|
console.print("[bold yellow]Usage: /middleout on|off (or /middleout to view status)[/]")
|
||||||
continue
|
continue
|
||||||
elif user_input.lower() == "/reset":
|
elif user_input.lower() == "/reset":
|
||||||
confirm = typer.confirm("Reset conversation context? This clears history and prompt.", default=False)
|
try:
|
||||||
if not confirm:
|
confirm = typer.confirm("Reset conversation context? This clears history and prompt.", default=False)
|
||||||
console.print("[bold yellow]Reset cancelled.[/]")
|
if not confirm:
|
||||||
|
console.print("[bold yellow]Reset cancelled.[/]")
|
||||||
|
continue
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
console.print("\n[bold yellow]Reset cancelled.[/]")
|
||||||
continue
|
continue
|
||||||
session_history = []
|
session_history = []
|
||||||
current_index = -1
|
current_index = -1
|
||||||
@@ -1213,11 +1599,28 @@ def chat():
|
|||||||
console.print(f"[bold green]Streaming {'enabled' if sub_args == 'on' else 'disabled'}.[/]")
|
console.print(f"[bold green]Streaming {'enabled' if sub_args == 'on' else 'disabled'}.[/]")
|
||||||
else:
|
else:
|
||||||
console.print("[bold yellow]Usage: /config stream on|off[/]")
|
console.print("[bold yellow]Usage: /config stream on|off[/]")
|
||||||
|
elif args.startswith("loglevel"):
|
||||||
|
sub_args = args[8:].strip()
|
||||||
|
if not sub_args:
|
||||||
|
console.print(f"[bold blue]Current log level: {LOG_LEVEL_STR.upper()}[/]")
|
||||||
|
console.print(f"[dim yellow]Valid levels: debug, info, warning, error, critical[/]")
|
||||||
|
continue
|
||||||
|
if sub_args.lower() in VALID_LOG_LEVELS:
|
||||||
|
if set_log_level(sub_args):
|
||||||
|
set_config('log_level', sub_args.lower())
|
||||||
|
console.print(f"[bold green]Log level set to: {sub_args.upper()}[/]")
|
||||||
|
app_logger.info(f"Log level changed to {sub_args.upper()}")
|
||||||
|
else:
|
||||||
|
console.print(f"[bold red]Failed to set log level.[/]")
|
||||||
|
else:
|
||||||
|
console.print(f"[bold red]Invalid log level: {sub_args}[/]")
|
||||||
|
console.print(f"[bold yellow]Valid levels: debug, info, warning, error, critical[/]")
|
||||||
elif args.startswith("log"):
|
elif args.startswith("log"):
|
||||||
sub_args = args[4:].strip()
|
sub_args = args[4:].strip()
|
||||||
if not sub_args:
|
if not sub_args:
|
||||||
console.print(f"[bold blue]Current log file size limit: {LOG_MAX_SIZE_MB} MB[/]")
|
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"[bold blue]Log backup count: {LOG_BACKUP_COUNT} files[/]")
|
||||||
|
console.print(f"[bold blue]Log level: {LOG_LEVEL_STR.upper()}[/]")
|
||||||
console.print(f"[dim yellow]Total max disk usage: ~{LOG_MAX_SIZE_MB * (LOG_BACKUP_COUNT + 1)} MB[/]")
|
console.print(f"[dim yellow]Total max disk usage: ~{LOG_MAX_SIZE_MB * (LOG_BACKUP_COUNT + 1)} MB[/]")
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
@@ -1230,9 +1633,13 @@ def chat():
|
|||||||
new_size_mb = 100
|
new_size_mb = 100
|
||||||
set_config('log_max_size_mb', str(new_size_mb))
|
set_config('log_max_size_mb', str(new_size_mb))
|
||||||
LOG_MAX_SIZE_MB = 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.[/]")
|
# Reload logging configuration immediately
|
||||||
app_logger.info(f"Log size limit updated to {new_size_mb} MB (requires restart)")
|
app_logger = reload_logging_config()
|
||||||
|
|
||||||
|
console.print(f"[bold green]Log size limit set to {new_size_mb} MB and applied immediately.[/]")
|
||||||
|
console.print(f"[dim cyan]Log file rotated if it exceeded the new limit.[/]")
|
||||||
|
app_logger.info(f"Log size limit updated to {new_size_mb} MB and reloaded")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
console.print("[bold red]Invalid size. Provide a number in MB.[/]")
|
console.print("[bold red]Invalid size. Provide a number in MB.[/]")
|
||||||
elif args.startswith("online"):
|
elif args.startswith("online"):
|
||||||
@@ -1315,6 +1722,7 @@ def chat():
|
|||||||
table.add_row("Logfile", str(log_file) 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 Size Limit", f"{LOG_MAX_SIZE_MB} MB")
|
||||||
table.add_row("Log Backups", str(LOG_BACKUP_COUNT))
|
table.add_row("Log Backups", str(LOG_BACKUP_COUNT))
|
||||||
|
table.add_row("Log Level", LOG_LEVEL_STR.upper())
|
||||||
table.add_row("Streaming", "Enabled" if STREAM_ENABLED == "on" else "Disabled")
|
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("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("Current Model", "[Not set]" if selected_model is None else str(selected_model["name"]))
|
||||||
@@ -1366,7 +1774,15 @@ def chat():
|
|||||||
console.print("[bold cyan]Online mode: Enabled (web search active)[/]")
|
console.print("[bold cyan]Online mode: Enabled (web search active)[/]")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if user_input.lower() == "/help":
|
if user_input.lower().startswith("/help"):
|
||||||
|
args = user_input[6:].strip()
|
||||||
|
|
||||||
|
# If a specific command is requested
|
||||||
|
if args:
|
||||||
|
show_command_help(args)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Otherwise show the full help menu
|
||||||
help_table = Table("Command", "Description", "Example", show_header=True, header_style="bold cyan", width=console.width - 10)
|
help_table = Table("Command", "Description", "Example", show_header=True, header_style="bold cyan", width=console.width - 10)
|
||||||
|
|
||||||
# SESSION COMMANDS
|
# SESSION COMMANDS
|
||||||
@@ -1381,9 +1797,9 @@ def chat():
|
|||||||
"/clear\n/cl"
|
"/clear\n/cl"
|
||||||
)
|
)
|
||||||
help_table.add_row(
|
help_table.add_row(
|
||||||
"/help",
|
"/help [command]",
|
||||||
"Show this help menu with all available commands.",
|
"Show this help menu or get detailed help for a specific command.",
|
||||||
"/help"
|
"/help\n/help /model"
|
||||||
)
|
)
|
||||||
help_table.add_row(
|
help_table.add_row(
|
||||||
"/memory [on|off]",
|
"/memory [on|off]",
|
||||||
@@ -1461,9 +1877,14 @@ def chat():
|
|||||||
)
|
)
|
||||||
help_table.add_row(
|
help_table.add_row(
|
||||||
"/config log [size_mb]",
|
"/config log [size_mb]",
|
||||||
"Set log file size limit in MB. Older logs are rotated automatically. Requires restart.",
|
"Set log file size limit in MB. Older logs are rotated automatically. Takes effect immediately.",
|
||||||
"/config log 20"
|
"/config log 20"
|
||||||
)
|
)
|
||||||
|
help_table.add_row(
|
||||||
|
"/config loglevel [level]",
|
||||||
|
"Set log verbosity level. Valid levels: debug, info, warning, error, critical. Takes effect immediately.",
|
||||||
|
"/config loglevel debug\n/config loglevel warning"
|
||||||
|
)
|
||||||
help_table.add_row(
|
help_table.add_row(
|
||||||
"/config maxtoken [value]",
|
"/config maxtoken [value]",
|
||||||
"Set stored max token limit (persisted in DB). View current if no value provided.",
|
"Set stored max token limit (persisted in DB). View current if no value provided.",
|
||||||
@@ -1599,7 +2020,7 @@ def chat():
|
|||||||
help_table,
|
help_table,
|
||||||
title="[bold cyan]oAI Chat Help (Version %s)[/]" % version,
|
title="[bold cyan]oAI Chat Help (Version %s)[/]" % version,
|
||||||
title_align="center",
|
title_align="center",
|
||||||
subtitle="💡 Tip: Commands are case-insensitive • Memory ON by default (toggle with /memory) • Use // to escape / at start of input • Visit: https://iurl.no/oai",
|
subtitle="💡 Tip: Commands are case-insensitive • Use /help <command> for detailed help • Memory ON by default • Use // to escape / • Visit: https://iurl.no/oai",
|
||||||
subtitle_align="center",
|
subtitle_align="center",
|
||||||
border_style="cyan"
|
border_style="cyan"
|
||||||
))
|
))
|
||||||
@@ -1608,8 +2029,7 @@ def chat():
|
|||||||
if not selected_model:
|
if not selected_model:
|
||||||
console.print("[bold yellow]Select a model first with '/model'.[/]")
|
console.print("[bold yellow]Select a model first with '/model'.[/]")
|
||||||
continue
|
continue
|
||||||
|
# Process file attachments with PDF support
|
||||||
# Process file attachments with PDF support
|
|
||||||
content_blocks = []
|
content_blocks = []
|
||||||
text_part = user_input
|
text_part = user_input
|
||||||
file_attachments = []
|
file_attachments = []
|
||||||
|
|||||||
Reference in New Issue
Block a user