""" Interactive CLI Chat Interface for Nova AI - ChatGPT Astra Edition. Rich-powered terminal interface featuring live token streaming, Astra Deep Reasoning, token vault management, multimodal generation (/image, /web), and session controls. """ import os import sys from pathlib import Path from typing import List, Dict, Any # Ensure UTF-8 output on Windows consoles if sys.platform == "win32": try: if sys.stdout and hasattr(sys.stdout, "reconfigure"): sys.stdout.reconfigure(encoding="utf-8") if sys.stderr and hasattr(sys.stderr, "reconfigure"): sys.stderr.reconfigure(encoding="utf-8") except Exception: pass # Ensure project root in sys.path PROJECT_ROOT = Path(__file__).resolve().parent if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) from rich.console import Console from rich.panel import Panel from rich.table import Table from rich.markdown import Markdown from rich.prompt import Prompt from src.config import config, AVAILABLE_MODELS, SYSTEM_PROMPT_PRESETS, GenerationConfig from src.model_engine import engine from src.token_system import token_vault, TOKEN_COSTS from src.multimodal import generate_ai_image, generate_ai_website from src.web_search import search_live_web from src.utils import ( format_chat_as_markdown, format_chat_as_json, format_chat_as_html, enhance_prompt_with_astra, get_system_hardware_info ) from src.domain_registry import domain_registry console = Console(legacy_windows=False) def print_banner(reasoning_mode: bool = False, grounding_mode: bool = False, active_domain: str = None): active_model = engine.model_id or config.default_model_id reasoning_status = "[bold green]ENABLED[/bold green]" if reasoning_mode else "[dim]DISABLED[/dim]" grounding_status = "[bold green]ENABLED[/bold green]" if grounding_mode else "[dim]DISABLED[/dim]" domain_status = f"[bold magenta]{active_domain}[/bold magenta]" if active_domain else f"[bold green]{domain_registry.total_count:,} Domains[/bold green] [dim](Auto-Detect)[/dim]" balance = token_vault.get_balance() hw = get_system_hardware_info() banner_text = ( "[bold cyan]🌟 NOVA AI — Flagship ChatGPT Astra Enterprise Suite[/bold cyan]\n" "[dim]Full 7B/8B Foundation Models • NVIDIA ZeroGPU & CUDA • Deep Reasoning • Multimodal Studio[/dim]\n\n" f"• Active Model: [bold yellow]{active_model}[/bold yellow]\n" f"• Hardware Engine: [bold green]{hw.get('hardware_badge')}[/bold green]\n" f"• Astra Tokens: [bold cyan]🪙 {balance} ASTRA[/bold cyan]\n" f"• Specialization: {domain_status}\n" f"• Deep Reasoning: {reasoning_status}\n" f"• Web Grounding: {grounding_status}\n\n" "[dim]Commands: [bold cyan]/help[/bold cyan], [bold yellow]/think[/bold yellow], [bold blue]/domains[/bold blue], [bold magenta]/image[/bold magenta], [bold blue]/web[/bold blue], [bold green]/tokens[/bold green], [bold red]/quit[/bold red][/dim]" ) console.print(Panel(banner_text, border_style="cyan", title="[bold magenta]Astra Flagship Console[/bold magenta]")) def print_help(): help_md = """ ### 🌟 Nova Astra CLI Commands: - `/help` - Display this command reference - `/domains` - Browse and search 1,000+ domain specializations (1,425 available) - `/domain` - Lock into a specific domain specialization (e.g. `/domain upi` or `/domain auto`) - `/train` - Launch AI/ML Fine-Tuning Studio (PEFT LoRA + Weight Merging) - `/tokens` - View Astra token balance & transaction history - `/think` - Toggle Astra Deep Reasoning Mode (5 ASTRA) - `/search` - Live web search with DuckDuckGo & Wikipedia (3 ASTRA) - `/ground` - Toggle automatic real-time web grounding for chat - `/image` - Generate an AI Image with FLUX / SDXL (15 ASTRA) - `/web` - Generate a full responsive HTML5 website (20 ASTRA) - `/spark` - Enhance a prompt with Astra Spark Optimizer - `/persona` - Switch ChatGPT Astra persona mode - `/model` - Switch active model architecture - `/stats` - View hardware profiler & session statistics - `/export` - Export dialogue transcript to Markdown, JSON, or HTML - `/clear` - Reset active conversation history - `/quit` - Exit interactive session """ console.print(Markdown(help_md)) def cli_web_search(): query = Prompt.ask("\n[bold cyan]🌐 Enter search query[/bold cyan]").strip() if not query: return with console.status("[bold cyan]Searching live web with DuckDuckGo & Wikipedia (3 ASTRA)...[/bold cyan]"): res = search_live_web(query) if res.get("status") == "success": results = res.get("results", []) console.print(f"[bold green]✓ Found {len(results)} live web sources:[/bold green]") for idx, r in enumerate(results, 1): console.print(f"[bold cyan][{idx}][/bold cyan] [bold white]{r['title']}[/bold white] [dim]({r['source']})[/dim]") console.print(f" 🔗 [link={r['url']}]{r['url']}[/link]") console.print(f" 📄 [dim]{r['snippet'][:180]}...[/dim]\n") console.print(f"🪙 Remaining Balance: [bold cyan]{res['remaining_tokens']} ASTRA[/bold cyan]") else: console.print(f"[bold red]Search error:[/bold red] {res.get('message')}") def show_tokens(): bal = token_vault.get_balance() history = token_vault.get_transaction_history() table = Table(title=f"🪙 Astra Token Vault (Balance: {bal} ASTRA)", border_style="cyan") table.add_column("Date", style="dim") table.add_column("Action", style="bold") table.add_column("Amount", justify="right") table.add_column("Balance After", justify="right", style="bold green") for t in history[:8]: amt_color = "green" if t.get("amount", 0) > 0 else "red" table.add_row( t.get("date", ""), t.get("action", ""), f"[{amt_color}]{t.get('amount', 0):+d}[/{amt_color}]", str(t.get("balance_after", "")) ) console.print(table) def cli_image_generator(): prompt = Prompt.ask("\n[bold magenta]🎨 Enter image description[/bold magenta]").strip() if not prompt: return with console.status("[bold cyan]Synthesizing image with Astra FLUX neural engine (15 ASTRA)...[/bold cyan]"): res = generate_ai_image(prompt) if res.get("status") == "success": console.print(f"[bold green]✓ Image generated successfully![/bold green]") console.print(f"📁 Local File: [link={res['file_path']}]{res['file_path']}[/link]") console.print(f"🪙 Remaining Balance: [bold cyan]{res['remaining_tokens']} ASTRA[/bold cyan]") else: console.print(f"[bold red]Generation error:[/bold red] {res.get('message')}") def cli_website_generator(): prompt = Prompt.ask("\n[bold blue]🌐 Enter React & Next.js website concept[/bold blue]").strip() if not prompt: return with console.status("[bold cyan]Synthesizing React.js & Next.js project with Nova Astra WebCoder-V1 (20 ASTRA)...[/bold cyan]"): res = generate_ai_website(prompt) if res.get("status") == "success": console.print(f"[bold green]✓ React & Next.js Project generated successfully![/bold green]") console.print(f"📦 Auto ZIP Archive: [bold yellow]{res.get('zip_path')}[/bold yellow]") console.print(f"⚛️ React Component: [bold cyan]{res.get('jsx_path')}[/bold cyan]") console.print(f"⚡ Next.js Page: [bold magenta]{res.get('tsx_path')}[/bold magenta]") console.print(f"🌐 Live Sandbox: [link={res['file_path']}]{res['file_path']}[/link]") console.print(f"🪙 Remaining Balance: [bold cyan]{res['remaining_tokens']} ASTRA[/bold cyan]") else: console.print(f"[bold red]Generation error:[/bold red] {res.get('message')}") def cli_fine_tuning(): from src.fine_tune import run_lora_finetuning from src.dataset_builder import compile_huge_training_dataset, HUGE_DATASET_FILE console.print(Panel( "[bold cyan]🧠 Nova Astra AI/ML Fine-Tuning Studio[/bold cyan]\n" "[dim]LoRA Parameter-Efficient Fine-Tuning on Huge Multi-Turn Datasets (React, Fintech INR, Multimodal, Reasoning)[/dim]", border_style="magenta" )) choice = Prompt.ask( "\n[bold yellow]Select Training Dataset Scope[/bold yellow]\n" " [1] Quick LoRA Trial (10 samples, ~1-2 min)\n" " [2] High-Yield Benchmark (50 samples, ~5 min)\n" " [3] Massive Dataset (All 610 samples)\n" " [4] Recompile / Verify Huge Dataset JSONL\n" "Choice", choices=["1", "2", "3", "4"], default="1" ) if choice == "4": with console.status("[bold cyan]Compiling 600+ sample multi-turn dataset...[/bold cyan]"): d_res = compile_huge_training_dataset(target_count=600) console.print(f"[bold green]✓ Dataset compiled successfully:[/bold green] {d_res['sample_count']} samples (~{d_res['estimated_tokens']:,} tokens)") console.print(f"📁 Location: {d_res['file_path']}") return sample_map = {"1": 10, "2": 50, "3": None} max_samples = sample_map[choice] active_model = engine.model_id or config.default_model_id console.print(f"\n[bold cyan]Starting LoRA Fine-Tuning on {active_model} (samples: {max_samples or 'ALL'})...[/bold cyan]") with console.status("[bold green]Executing PEFT LoRA training loop & weight merging...[/bold green]"): res = run_lora_finetuning( base_model_id=active_model, epochs=1, batch_size=1, max_samples=max_samples, export_merged=True ) if res.get("status") == "success": console.print("\n[bold green]🎉 Fine-Tuning & Weight Export Succeeded![/bold green]") console.print(f" • Trained Samples: [bold white]{res.get('samples_trained')}[/bold white]") console.print(f" • Training Loss: [bold cyan]{res.get('train_loss')}[/bold cyan]") console.print(f" • Time Elapsed: [bold yellow]{res.get('training_time_seconds')}s[/bold yellow]") console.print(f" • Adapter Dir: [dim]{res.get('adapter_dir')}[/dim]") console.print(f" • Merged Safetensors: [bold green]{res.get('exported_model_dir')}[/bold green]") else: console.print(f"[bold red]❌ Training failed:[/bold red] {res.get('message')}") def cli_domain_browser(): console.print(Panel( f"[bold cyan]🌐 Nova Astra 1000+ Domain Specializations Hub[/bold cyan]\n" f"[dim]Total Available: {domain_registry.total_count:,} domains across {len(domain_registry.get_all_categories())} categories.[/dim]", border_style="cyan" )) search_q = Prompt.ask("[bold yellow]Enter keyword to search domains (or press Enter to list categories)[/bold yellow]", default="").strip() if search_q: results = domain_registry.search_domains(search_q, limit=12) if not results: console.print("[yellow]No exact match found. Showing closest matches.[/yellow]") table = Table(title=f"Domain Search Results for '{search_q}'", border_style="magenta") table.add_column("ID", style="dim") table.add_column("Domain Specialization", style="bold cyan") table.add_column("Category", style="green") table.add_column("Scope Summary") for d in results: table.add_row(d.id, d.name, d.category, d.description[:60] + "...") console.print(table) else: cats = domain_registry.get_all_categories() table = Table(title="1,000+ Domain Categories", border_style="cyan") table.add_column("#", style="dim") table.add_column("Category", style="bold white") table.add_column("Domains Count", style="bold yellow") for i, c in enumerate(cats, 1): count = len(domain_registry.get_domains_by_category(c)) table.add_row(str(i), c, f"{count} domains") console.print(table) console.print("[dim]Tip: Type '/domain ' to lock into any domain, or '/domain auto' to restore auto-detect.[/dim]") def cli_main(): reasoning_mode = False grounding_mode = False active_domain = None active_domain_name = None print_banner(reasoning_mode, grounding_mode, active_domain_name) with console.status("[bold green]🌟 Initializing Nova Astra Engine...[/bold green]"): try: engine.load_model() except Exception as e: console.print(f"[bold red]Initialization failed:[/bold red] {e}") return system_prompt = SYSTEM_PROMPT_PRESETS["ChatGPT Astra Core (Omniscient & Empathetic)"] messages: List[Dict[str, str]] = [] while True: try: user_input = Prompt.ask(f"\n[bold green]👤 You[/bold green] [dim]({token_vault.get_balance()} ASTRA)[/dim]").strip() except (KeyboardInterrupt, EOFError): console.print("\n[bold yellow]Exiting Nova AI. Farewell![/bold yellow]") break if not user_input: continue if user_input.startswith("/"): cmd = user_input.lower().split()[0] if cmd in ("/quit", "/exit"): console.print("[bold yellow]Exiting Nova AI. Farewell![/bold yellow]") break elif cmd == "/help": print_help() continue elif cmd == "/tokens": show_tokens() continue elif cmd == "/domains": cli_domain_browser() continue elif cmd == "/domain": parts = user_input.split(maxsplit=1) if len(parts) < 2 or parts[1].strip().lower() in ("auto", "none", "reset"): active_domain = None active_domain_name = None console.print("[bold green]✓ Domain specialization reset to: AUTO-DETECT (1,425 domains active).[/bold green]") else: query = parts[1].strip() matched = domain_registry.get_domain(query) if not matched: matches = domain_registry.search_domains(query, limit=1) if matches: matched = matches[0] if matched: active_domain = matched.id active_domain_name = f"{matched.name} ({matched.category})" console.print(f"[bold green]✓ Specialization locked to:[/bold green] [bold cyan]{active_domain_name}[/bold cyan]") else: console.print(f"[bold red]Domain '{query}' not found. Type '/domains' to search 1,000+ domains.[/bold red]") continue elif cmd == "/train": cli_fine_tuning() continue elif cmd in ("/search", "/websearch"): cli_web_search() continue elif cmd == "/ground": grounding_mode = not grounding_mode status_str = "ENABLED" if grounding_mode else "DISABLED" console.print(f"[bold cyan]🌐 Real-Time Web Grounding:[/bold cyan] {status_str}") continue elif cmd == "/image": cli_image_generator() continue elif cmd == "/web": cli_website_generator() continue elif cmd == "/think": reasoning_mode = not reasoning_mode status_str = "ENABLED (Chain-of-Thought active)" if reasoning_mode else "DISABLED" console.print(f"[bold cyan]🧠 Astra Deep Reasoning Mode:[/bold cyan] {status_str}") continue elif cmd == "/spark": raw_query = Prompt.ask("[bold cyan]Enter query to enhance[/bold cyan]") enhanced = enhance_prompt_with_astra(raw_query) console.print(Panel(enhanced, title="[bold magenta]✨ Astra Spark[/bold magenta]", border_style="magenta")) continue elif cmd == "/clear": messages.clear() console.print("[bold yellow]✓ Conversation cleared.[/bold yellow]") continue elif cmd == "/stats": hw = get_system_hardware_info() table = Table(title="Astra Runtime Telemetry", border_style="cyan") table.add_column("Metric", style="bold") table.add_column("Value") table.add_row("Active Model", str(engine.model_id)) table.add_row("Astra Balance", f"{token_vault.get_balance()} ASTRA") table.add_row("Device", engine.device.upper()) table.add_row("RAM", f"{hw.get('available_ram_gb')} GB / {hw.get('total_ram_gb')} GB") console.print(table) continue else: console.print(f"[bold red]Unknown command:[/bold red] {user_input}. Type [bold yellow]/help[/bold yellow].") continue # Check and deduct tokens for chat action = "deep_reasoning" if reasoning_mode else "chat_query" total_cost = TOKEN_COSTS[action] + (TOKEN_COSTS["web_search"] if grounding_mode else 0) if token_vault.get_balance() < total_cost: console.print(f"[bold red]Insufficient tokens. Requires {total_cost} ASTRA, but you have {token_vault.get_balance()} ASTRA.[/bold red]") continue success, msg, remaining = token_vault.deduct_tokens(action) if not success: console.print(f"[bold red]{msg}[/bold red]") continue grounded_context = "" citations_summary = [] if grounding_mode: token_vault.deduct_tokens("web_search") with console.status("[bold cyan]Querying live web sources...[/bold cyan]"): search_res = search_live_web(user_input, max_results=3, deduct_tokens=False) if search_res.get("status") == "success" and search_res.get("results"): grounded_context = search_res.get("grounding_context", "") citations_summary = search_res.get("results", []) messages.append({"role": "user", "content": user_input}) gen_messages = [] if system_prompt: gen_messages.append({"role": "system", "content": system_prompt}) for m in messages[:-1]: gen_messages.append({"role": m["role"], "content": m["content"]}) if grounded_context: gen_messages.append({"role": "user", "content": f"{grounded_context}\n\nUser Question: {user_input}"}) else: gen_messages.append({"role": "user", "content": user_input}) gen_cfg = GenerationConfig( max_new_tokens=config.gen_config.max_new_tokens, temperature=config.gen_config.temperature, deep_reasoning=reasoning_mode, domain_id=active_domain, auto_detect_domain=(active_domain is None) ) rem_bal = token_vault.get_balance() ground_tag = " | 🌐 Web Grounded" if grounding_mode else "" domain_tag = f" | 🎓 {active_domain_name}" if active_domain_name else "" console.print(f"\n[bold magenta]🌟 Nova Astra:[/bold magenta] [dim](-{total_cost} ASTRA{ground_tag}{domain_tag} | Bal: {rem_bal})[/dim]") full_response = "" last_metrics = {} try: for partial_text, metrics in engine.stream_chat(gen_messages, gen_cfg): new_chunk = partial_text[len(full_response):] console.print(new_chunk, end="", style="white") full_response = partial_text last_metrics = metrics console.print() if citations_summary: console.print("\n[bold cyan]🌐 Verified Live Sources:[/bold cyan]") for idx, r in enumerate(citations_summary, 1): console.print(f" [cyan][{idx}][/cyan] {r['title']} [dim]({r['url']})[/dim]") messages.append({"role": "assistant", "content": full_response}) tps = last_metrics.get("tokens_per_second", 0.0) ttft = last_metrics.get("time_to_first_token_ms", 0.0) tokens = last_metrics.get("tokens_generated", 0) lat = last_metrics.get("total_latency_seconds", 0.0) detected_domain = last_metrics.get("active_domain") domain_info = f" | 🎓 Specialization: {detected_domain}" if detected_domain else "" console.print( f"[dim]⚡ Speed: {tps} tps | TTFT: {ttft}ms | Tokens: {tokens} | Latency: {lat}s{domain_info} | Tokens Rem: {rem_bal} ASTRA[/dim]" ) except Exception as e: console.print(f"\n[bold red]Error during generation:[/bold red] {e}") if __name__ == "__main__": cli_main()