""" cli.py — MELUSINA CLI entrypoint Komendy: melusina chat — pętla rozmowy z modelem melusina teach — Agent TEACHER (poziomy 1-3) melusina test N — Agent TESTER (N runów) melusina inspect — pokaż constitution.jfp melusina history — audit log melusina finetune — uruchom LoRA fine-tuning """ from __future__ import annotations import json import os import subprocess import sys from pathlib import Path import click from melusina import __version__ from melusina.core.constitution import Constitution, AuditLog, Rule, _make_audit_id, _now_iso from melusina.core.interceptor import Interceptor # ── Stałe ───────────────────────────────────────────────────────────────────── FINETUNE_SCRIPT = Path.home() / "Jjfp-core-v1" / "training" / "finetune.py" DATASET_V2 = Path.home() / "Jjfp-core-v1" / "training" / "dataset_v2.jsonl" JFP_SYSTEM = ( "You are JFP-Core-v1, a deterministic AI engine governed by Jaro Flash Protocol " "v16E.0.0. You never confabulate. Unknown = SIGNAL_UNKNOWN. " "Every output is structured and auditable." ) # ── Shared singletons ───────────────────────────────────────────────────────── _constitution = Constitution() _audit = AuditLog() _interceptor = Interceptor() # ── Mock engine (fallback gdy model nie załadowany) ─────────────────────────── def _mock_response(message: str) -> str: from melusina.core.interceptor import SignalType sig = _interceptor.detector.detect(message) aid = _make_audit_id() ts = _now_iso() m = message.lower() if sig and sig.signal_type == SignalType.DENY: return json.dumps({"status": "DENIED", "signal": "SIGNAL_DENY", "reason": "dangerous_or_unauthorized_operation", "audit_id": aid, "timestamp": ts}, indent=2) if sig and sig.signal_type == SignalType.UNKNOWN: return json.dumps({"status": "SIGNAL_UNKNOWN", "reason": "no_verified_data", "audit_id": aid, "timestamp": ts}, indent=2) if sig and sig.signal_type == SignalType.LEARNING: return json.dumps({"status": "LEARNING_SIGNAL_DETECTED", "suggestion": "fine-tune recommended", "audit_id": aid, "timestamp": ts}, indent=2) # Prosta baza wiedzy _KB = { "jfp": "JFP (Jaro Flash Protocol) v16E.0.0 — deterministic AI governance protocol.", "melusina": "MELUSINA — JFP-Core-v1 LoRA adapter on Qwen2.5-1.5B. Model: jarohullowicki/Melusina-1.5B-JFP", "lora": "LoRA r=16, alpha=32, target: q/k/v/o_proj. Final loss: 0.0890.", "viper": "VIPER agent — network scanning, intrusion detection.", "vision": "VISION agent — reconnaissance, log analysis, pattern matching.", "evocos": "EVOCOS agent — optimization, self-improvement, retraining.", "nexus": "NEXUS agent — deployment, rollback, snapshots.", "aortana": "AORTANA agent — encryption, decryption, protocol updates.", "voql": "VOQL — Verified Operational Query Language. Blocks: DELETE, DROP, TRUNCATE.", "constitution": f"constitution.jfp: {_constitution.count()} reguł w {_constitution.path}", "hello": "Hello! I am MELUSINA — JFP-Core-v1. I never confabulate.", "cześć": "Witaj! Jestem MELUSINA — JFP-Core-v1. Nigdy nie konfabuluję.", } for key, val in _KB.items(): if key in m: return json.dumps({"status": "OK", "answer": val, "audit_id": aid, "timestamp": ts}, indent=2) return json.dumps({"status": "SIGNAL_UNKNOWN", "reason": "query_not_in_jfp_knowledge_base", "hint": "Try: 'what is JFP?', 'tell me about MELUSINA'", "audit_id": aid, "timestamp": ts}, indent=2) # ── CLI group ───────────────────────────────────────────────────────────────── @click.group() @click.version_option(__version__, prog_name="melusina") def cli(): """🌊 MELUSINA — JFP-Core-v1 CLI\n\nJaro Flash Protocol v16E.0.0""" pass # ── melusina chat ───────────────────────────────────────────────────────────── @cli.command() @click.option("--load-model", "-m", is_flag=True, default=False, help="Załaduj prawdziwy model MELUSINA (wymaga GPU/CPU ~3GB)") @click.option("--no-bc", is_flag=True, default=False, help="Wyłącz BC Interceptor (nie proponuj reguł)") def chat(load_model: bool, no_bc: bool): """Główna pętla rozmowy z MELUSINA.""" click.echo("\n🌊 MELUSINA Chat — JFP-Core-v1") click.echo(f" Constitution: {_constitution.path} ({_constitution.count()} reguł)") click.echo(" Wpisz 'exit' lub Ctrl+C aby wyjść.\n") # Opcjonalne ładowanie modelu if load_model: from melusina.core import model as M click.echo("⏳ Ładuję model MELUSINA…") ok = M.load(verbose=True) if not ok: click.echo(f"⚠ Model nie załadowany ({M.error()}) — używam mock engine.\n") else: click.echo("ℹ Tryb mock (bez modelu). Użyj --load-model aby załadować MELUSINA.\n") _audit.append("CHAT_START", {"load_model": load_model}) while True: try: user_input = input("You: ").strip() except (EOFError, KeyboardInterrupt): click.echo("\n👋 Do widzenia!") break if not user_input: continue if user_input.lower() in ("exit", "quit", "q", "bye"): click.echo("👋 Do widzenia!") break # BC Interceptor if not no_bc: proposal = _interceptor.intercept(user_input) if proposal and proposal["signal_type"] not in ("deny", "unknown"): click.echo(f"\n💡 BC Signal: {proposal['signal_type'].upper()}") click.echo(f" Propozycja reguły: {proposal['value'][:60]}") try: ans = input(" Dodać do constitution.jfp? [T/N]: ").strip().upper() except (EOFError, KeyboardInterrupt): ans = "N" if ans in ("T", "TAK", "Y", "YES"): rule = _constitution.add( value=proposal["value"], section=proposal["section"], cls=proposal["rule_class"], source=proposal["signal_type"], ) click.echo(f" ✅ Reguła {rule.key} zapisana.\n") _audit.append("RULE_ADDED", {"key": rule.key, "value": rule.value[:60]}) # Generuj odpowiedź from melusina.core import model as M if M.is_loaded(): reply, source = M.generate_or_mock(user_input, _mock_response) else: reply = _mock_response(user_input) source = "mock" click.echo(f"\nMELUSINA [{source}]:\n{reply}\n") _audit.append("CHAT_TURN", {"user": user_input[:80], "source": source}) # ── melusina teach ──────────────────────────────────────────────────────────── @cli.command() @click.option("--level", "-l", default=1, type=click.IntRange(1, 3), help="Poziom trudności: 1 (SIGNAL), 2 (VALIDATION), 3 (PIPELINE)") def teach(level: int): """Agent TEACHER — interaktywna nauka JFP (poziomy 1-3).""" from melusina.agents.teacher import TeacherAgent agent = TeacherAgent(audit=_audit) agent.run(level=level) # ── melusina test ───────────────────────────────────────────────────────────── @cli.command() @click.argument("n", default=100, type=int) @click.option("--model", "-m", is_flag=True, default=False, help="Użyj prawdziwego modelu (domyślnie: mock heurystyka)") @click.option("--seed", "-s", default=None, type=int, help="Seed dla losowania testów (reproducibility)") @click.option("--dataset", "-d", default=None, type=click.Path(), help=f"Ścieżka do datasetu (domyślnie: {DATASET_V2})") def test(n: int, model: bool, seed: int | None, dataset: str | None): """Agent TESTER — uruchom N testów z datasetu (domyślnie N=100).""" from melusina.agents.tester import TesterAgent ds_path = Path(dataset) if dataset else DATASET_V2 if model: from melusina.core import model as M click.echo("⏳ Ładuję model…") M.load(verbose=True) agent = TesterAgent(dataset_path=ds_path, audit=_audit) agent.run(n=n, use_model=model, seed=seed) # ── melusina inspect ────────────────────────────────────────────────────────── @cli.command() @click.option("--json", "as_json", is_flag=True, default=False, help="Wyświetl surowy JSONL") @click.option("--path", "-p", default=None, type=click.Path(), help="Ścieżka do constitution.jfp (domyślnie: ~/.jfp/constitution.jfp)") def inspect(as_json: bool, path: str | None): """Pokaż zawartość constitution.jfp.""" c = Constitution(Path(path)) if path else _constitution if as_json: click.echo(c.format_json() or "(pusta)") else: click.echo(c.format_text()) # ── melusina history ────────────────────────────────────────────────────────── @cli.command() @click.option("--n", "-n", default=20, type=int, help="Liczba ostatnich wpisów (domyślnie: 20)") @click.option("--json", "as_json", is_flag=True, default=False, help="Wyświetl surowy JSON") def history(n: int, as_json: bool): """Pokaż audit log (~/.jfp/audit.jsonl).""" if as_json: entries = _audit.load(n) click.echo(json.dumps(entries, indent=2, ensure_ascii=False)) else: click.echo(_audit.format_text(n)) # ── melusina finetune ───────────────────────────────────────────────────────── @cli.command() @click.option("--dataset", "-d", default=None, type=click.Path(), help=f"Ścieżka do datasetu (domyślnie: {DATASET_V2})") @click.option("--script", "-s", default=None, type=click.Path(), help=f"Ścieżka do skryptu finetune (domyślnie: {FINETUNE_SCRIPT})") @click.option("--dry-run", is_flag=True, default=False, help="Pokaż komendę bez uruchamiania") def finetune(dataset: str | None, script: str | None, dry_run: bool): """Uruchom LoRA fine-tuning na nowych danych.""" script_path = Path(script) if script else FINETUNE_SCRIPT dataset_path = Path(dataset) if dataset else DATASET_V2 if not script_path.exists(): click.echo(f"✗ Skrypt nie znaleziony: {script_path}") sys.exit(1) if not dataset_path.exists(): click.echo(f"✗ Dataset nie znaleziony: {dataset_path}") sys.exit(1) cmd = [sys.executable, str(script_path)] click.echo(f"\n🔥 Fine-tuning MELUSINA") click.echo(f" Skrypt: {script_path}") click.echo(f" Dataset: {dataset_path}") click.echo(f" Komenda: {' '.join(cmd)}\n") if dry_run: click.echo("(dry-run — nie uruchamiam)") return try: ans = input("Uruchomić? [T/N]: ").strip().upper() except (EOFError, KeyboardInterrupt): click.echo("\nAnulowano.") return if ans not in ("T", "TAK", "Y", "YES"): click.echo("Anulowano.") return _audit.append("FINETUNE_START", {"script": str(script_path), "dataset": str(dataset_path)}) click.echo("⏳ Uruchamiam fine-tuning… (Ctrl+C aby przerwać)\n") try: subprocess.run(cmd, check=True) _audit.append("FINETUNE_DONE", {}) click.echo("\n✅ Fine-tuning zakończony!") except subprocess.CalledProcessError as e: _audit.append("FINETUNE_ERROR", {"returncode": e.returncode}) click.echo(f"\n✗ Fine-tuning zakończony błędem (kod: {e.returncode})") sys.exit(e.returncode) except KeyboardInterrupt: click.echo("\n⚠ Przerwano przez użytkownika.") # ── melusina info ───────────────────────────────────────────────────────────── @cli.command() def info(): """Pokaż informacje o modelu i środowisku.""" from melusina.core import model as M mi = M.info() click.echo("\n🌊 MELUSINA — informacje") click.echo(f"{'─'*40}") click.echo(f" Wersja CLI: {__version__}") click.echo(f" Model status: {mi['status']}") click.echo(f" Base model: {mi['base_model']}") click.echo(f" LoRA adapter: {mi['lora_adapter']}") click.echo(f" Local adapter: {mi['local_adapter']}") click.echo(f" Device: {mi['device']}") if mi["error"]: click.echo(f" Error: {mi['error']}") click.echo(f"{'─'*40}") click.echo(f" Constitution: {_constitution.path}") click.echo(f" Reguły: {_constitution.count()}") click.echo(f" Audit log: {_audit.path}") click.echo(f" Dataset v2: {DATASET_V2} ({'✓' if DATASET_V2.exists() else '✗'})") click.echo(f" Finetune: {FINETUNE_SCRIPT} ({'✓' if FINETUNE_SCRIPT.exists() else '✗'})") click.echo() if __name__ == "__main__": cli()