jarohullowicki's picture
feat: melusina-cli v0.1.0 β€” 11/11 tests PASS
bdce909 verified
Raw
History Blame Contribute Delete
14.4 kB
"""
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()