File size: 14,449 Bytes
bdce909
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
"""
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()