lollmsbot 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
lollmsbot/wizard.py ADDED
@@ -0,0 +1,1493 @@
1
+ #!/usr/bin/env python
2
+ """
3
+ lollmsBot Interactive Setup Wizard - Skills Edition
4
+
5
+ Now includes:
6
+ - Binding-first backend configuration (remote vs local bindings)
7
+ - Soul configuration (personality, identity, values)
8
+ - Heartbeat settings (self-maintenance frequency, tasks)
9
+ - Memory monitoring (compression, retention, optimization)
10
+ - Skills management (browse, test, create, configure)
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ import json
16
+ import hashlib
17
+ from pathlib import Path
18
+ from typing import Any, Dict, List, Optional
19
+ from dataclasses import dataclass
20
+
21
+ try:
22
+ from rich.console import Console
23
+ from rich.panel import Panel
24
+ from rich.prompt import Prompt, Confirm, IntPrompt, FloatPrompt
25
+ from rich.table import Table
26
+ from rich.text import Text
27
+ from rich.tree import Tree
28
+ import questionary
29
+ from questionary import Choice
30
+ except ImportError:
31
+ print("❌ Install dev deps: pip install -e .[dev]")
32
+ exit(1)
33
+
34
+ from lollmsbot.config import LollmsSettings
35
+ from lollmsbot.lollms_client import build_lollms_client
36
+ from lollmsbot.soul import Soul, PersonalityTrait, TraitIntensity, ValueStatement, CommunicationStyle, ExpertiseDomain
37
+ from lollmsbot.heartbeat import Heartbeat, HeartbeatConfig, MaintenanceTask, get_heartbeat
38
+ from lollmsbot.skills import SkillRegistry, SkillComplexity, get_skill_registry, SkillLearner
39
+
40
+
41
+ console = Console()
42
+
43
+
44
+ @dataclass
45
+ class BindingInfo:
46
+ """Information about an LLM binding."""
47
+ name: str
48
+ display_name: str
49
+ category: str # "remote", "local_server", "local_direct"
50
+ description: str
51
+ default_host: Optional[str] = None
52
+ requires_api_key: bool = True
53
+ supports_ssl_verify: bool = True
54
+ requires_models_path: bool = False
55
+ default_model: Optional[str] = None
56
+
57
+
58
+ # Binding registry - all available bindings
59
+ AVAILABLE_BINDINGS: Dict[str, BindingInfo] = {
60
+ # Remote / SaaS bindings
61
+ "lollms": BindingInfo(
62
+ name="lollms",
63
+ display_name="🔗 LoLLMS (Default)",
64
+ category="remote",
65
+ description="LoLLMS WebUI - Local or remote LoLLMS server",
66
+ default_host="http://localhost:9600",
67
+ requires_api_key=False, # Optional for local, required for remote
68
+ supports_ssl_verify=True,
69
+ default_model=None,
70
+ ),
71
+ "openai": BindingInfo(
72
+ name="openai",
73
+ display_name="🤖 OpenAI",
74
+ category="remote",
75
+ description="OpenAI GPT models (GPT-4, GPT-3.5, etc.)",
76
+ default_host="https://api.openai.com/v1",
77
+ requires_api_key=True,
78
+ supports_ssl_verify=True,
79
+ default_model="gpt-4o-mini",
80
+ ),
81
+ "azure_openai": BindingInfo(
82
+ name="azure_openai",
83
+ display_name="☁️ Azure OpenAI",
84
+ category="remote",
85
+ description="Microsoft Azure OpenAI Service",
86
+ default_host="https://YOUR_RESOURCE.openai.azure.com/",
87
+ requires_api_key=True,
88
+ supports_ssl_verify=True,
89
+ default_model="gpt-4",
90
+ ),
91
+ "claude": BindingInfo(
92
+ name="claude",
93
+ display_name="🧠 Anthropic Claude",
94
+ category="remote",
95
+ description="Anthropic Claude models",
96
+ default_host="https://api.anthropic.com",
97
+ requires_api_key=True,
98
+ supports_ssl_verify=True,
99
+ default_model="claude-3-5-sonnet-20241022",
100
+ ),
101
+ "gemini": BindingInfo(
102
+ name="gemini",
103
+ display_name="💎 Google Gemini",
104
+ category="remote",
105
+ description="Google Gemini models",
106
+ default_host="https://generativelanguage.googleapis.com",
107
+ requires_api_key=True,
108
+ supports_ssl_verify=True,
109
+ default_model="gemini-1.5-flash",
110
+ ),
111
+ "groq": BindingInfo(
112
+ name="groq",
113
+ display_name="⚡ Groq",
114
+ category="remote",
115
+ description="Groq ultra-fast inference",
116
+ default_host="https://api.groq.com/openai/v1",
117
+ requires_api_key=True,
118
+ supports_ssl_verify=True,
119
+ default_model="llama-3.1-8b-instant",
120
+ ),
121
+ "grok": BindingInfo(
122
+ name="grok",
123
+ display_name="🐦 xAI Grok",
124
+ category="remote",
125
+ description="xAI Grok models",
126
+ default_host="https://api.x.ai/v1",
127
+ requires_api_key=True,
128
+ supports_ssl_verify=True,
129
+ default_model="grok-2",
130
+ ),
131
+ "mistral": BindingInfo(
132
+ name="mistral",
133
+ display_name="🌊 Mistral AI",
134
+ category="remote",
135
+ description="Mistral AI models",
136
+ default_host="https://api.mistral.ai/v1",
137
+ requires_api_key=True,
138
+ supports_ssl_verify=True,
139
+ default_model="mistral-small-latest",
140
+ ),
141
+ "ollama": BindingInfo(
142
+ name="ollama",
143
+ display_name="🦙 Ollama",
144
+ category="local_server",
145
+ description="Ollama local LLM server",
146
+ default_host="http://localhost:11434",
147
+ requires_api_key=False, # Local by default, key optional for proxy
148
+ supports_ssl_verify=False, # Usually local
149
+ default_model="llama3.2",
150
+ ),
151
+ "open_router": BindingInfo(
152
+ name="open_router",
153
+ display_name="🌐 OpenRouter",
154
+ category="remote",
155
+ description="OpenRouter - unified API for many models",
156
+ default_host="https://openrouter.ai/api/v1",
157
+ requires_api_key=True,
158
+ supports_ssl_verify=True,
159
+ default_model="meta-llama/llama-3.1-8b-instruct",
160
+ ),
161
+ "perplexity": BindingInfo(
162
+ name="perplexity",
163
+ display_name="❓ Perplexity",
164
+ category="remote",
165
+ description="Perplexity AI API",
166
+ default_host="https://api.perplexity.ai",
167
+ requires_api_key=True,
168
+ supports_ssl_verify=True,
169
+ default_model="llama-3.1-sonar-small-128k-online",
170
+ ),
171
+ "novita_ai": BindingInfo(
172
+ name="novita_ai",
173
+ display_name="✨ Novita AI",
174
+ category="remote",
175
+ description="Novita AI inference platform",
176
+ default_host="https://api.novita.ai/v3/openai",
177
+ requires_api_key=True,
178
+ supports_ssl_verify=True,
179
+ default_model="meta-llama/llama-3.1-8b-instruct",
180
+ ),
181
+ "litellm": BindingInfo(
182
+ name="litellm",
183
+ display_name="📡 LiteLLM",
184
+ category="remote",
185
+ description="LiteLLM proxy/gateway",
186
+ default_host="http://localhost:4000",
187
+ requires_api_key=True,
188
+ supports_ssl_verify=True,
189
+ default_model=None,
190
+ ),
191
+ "hugging_face_inference_api": BindingInfo(
192
+ name="hugging_face_inference_api",
193
+ display_name="🤗 Hugging Face",
194
+ category="remote",
195
+ description="Hugging Face Inference API",
196
+ default_host="https://api-inference.huggingface.co",
197
+ requires_api_key=True,
198
+ supports_ssl_verify=True,
199
+ default_model=None,
200
+ ),
201
+ "openllm": BindingInfo(
202
+ name="openllm",
203
+ display_name="🔧 OpenLLM",
204
+ category="local_server",
205
+ description="BentoML OpenLLM serving",
206
+ default_host="http://localhost:3000",
207
+ requires_api_key=False,
208
+ supports_ssl_verify=True,
209
+ default_model=None,
210
+ ),
211
+ "openwebui": BindingInfo(
212
+ name="openwebui",
213
+ display_name="🌟 OpenWebUI",
214
+ category="local_server",
215
+ description="OpenWebUI backend",
216
+ default_host="http://localhost:8080",
217
+ requires_api_key=True, # OpenWebUI uses API keys
218
+ supports_ssl_verify=True,
219
+ default_model=None,
220
+ ),
221
+ # Local direct bindings
222
+ "llama_cpp_server": BindingInfo(
223
+ name="llama_cpp_server",
224
+ display_name="🦙 Llama.cpp (Server)",
225
+ category="local_server",
226
+ description="llama.cpp server mode (local)",
227
+ default_host="http://localhost:8080",
228
+ requires_api_key=False,
229
+ supports_ssl_verify=False,
230
+ requires_models_path=True,
231
+ default_model=None,
232
+ ),
233
+ "vllm": BindingInfo(
234
+ name="vllm",
235
+ display_name="🔥 vLLM",
236
+ category="local_server",
237
+ description="vLLM high-throughput inference",
238
+ default_host="http://localhost:8000",
239
+ requires_api_key=False,
240
+ supports_ssl_verify=True,
241
+ requires_models_path=False,
242
+ default_model=None,
243
+ ),
244
+ "tensor_rt": BindingInfo(
245
+ name="tensor_rt",
246
+ display_name="🚀 TensorRT",
247
+ category="local_direct",
248
+ description="NVIDIA TensorRT LLM (local)",
249
+ default_host=None,
250
+ requires_api_key=False,
251
+ supports_ssl_verify=False,
252
+ requires_models_path=True,
253
+ default_model=None,
254
+ ),
255
+ "transformers": BindingInfo(
256
+ name="transformers",
257
+ display_name="🤗 Transformers",
258
+ category="local_direct",
259
+ description="Hugging Face Transformers (local)",
260
+ default_host=None,
261
+ requires_api_key=False,
262
+ supports_ssl_verify=False,
263
+ requires_models_path=True,
264
+ default_model=None,
265
+ ),
266
+ }
267
+
268
+
269
+ class Wizard:
270
+ """Interactive setup wizard for lollmsBot services - Full 7 Pillars Edition."""
271
+
272
+ def __init__(self):
273
+ self.config_path = Path.home() / ".lollmsbot" / "config.json"
274
+ self.config_path.parent.mkdir(exist_ok=True)
275
+ self.config: Dict[str, Dict[str, Any]] = self._load_config()
276
+
277
+ # Initialize subsystems for configuration
278
+ self.soul = Soul()
279
+ self.heartbeat = get_heartbeat()
280
+ self.skill_registry = get_skill_registry()
281
+
282
+ # Track what's been configured
283
+ self._configured: set = set()
284
+
285
+ def _load_config(self) -> Dict[str, Dict[str, Any]]:
286
+ if self.config_path.exists():
287
+ return json.loads(self.config_path.read_text())
288
+ return {}
289
+
290
+ def _save_config(self) -> None:
291
+ self.config_path.write_text(json.dumps(self.config, indent=2))
292
+
293
+ def run_wizard(self) -> None:
294
+ """Main wizard loop - Full Edition with all 7 Pillars."""
295
+ console.clear()
296
+
297
+ # Beautiful animated banner
298
+ banner = Panel.fit(
299
+ Text.assemble(
300
+ ("🧬 ", "bold magenta"),
301
+ ("lollmsBot", "bold cyan"),
302
+ (" Setup Wizard\n", "bold blue"),
303
+ ("Configure your ", "dim"),
304
+ ("sovereign AI companion", "italic green"),
305
+ ),
306
+ border_style="bright_blue",
307
+ padding=(1, 4),
308
+ )
309
+ console.print(banner)
310
+ console.print()
311
+
312
+ # Show current status
313
+ self._show_status_tree()
314
+
315
+ while True:
316
+ action = questionary.select(
317
+ "What would you like to configure?",
318
+ choices=[
319
+ Choice("🔗 AI Backend (Select Binding First)", "lollms"),
320
+ Choice("🤖 Discord Channel", "discord"),
321
+ Choice("✈️ Telegram Channel", "telegram"),
322
+ Choice("🧬 Soul (Personality & Identity)", "soul"),
323
+ Choice("💓 Heartbeat (Self-Maintenance)", "heartbeat"),
324
+ Choice("🧠 Memory (Storage & Retention)", "memory"),
325
+ Choice("📚 Skills (Capabilities & Learning)", "skills"),
326
+ Choice("🔍 Test Connections", "test"),
327
+ Choice("📄 View Full Configuration", "view"),
328
+ Choice("💾 Save & Exit", "save"),
329
+ Choice("❌ Quit Without Saving", "quit"),
330
+ ],
331
+ use_indicator=True,
332
+ ).ask()
333
+
334
+ if action == "lollms":
335
+ self.configure_backend() # New binding-first configuration
336
+ elif action == "discord":
337
+ self.configure_service("discord")
338
+ elif action == "telegram":
339
+ self.configure_service("telegram")
340
+ elif action == "soul":
341
+ self.configure_soul()
342
+ elif action == "heartbeat":
343
+ self.configure_heartbeat()
344
+ elif action == "memory":
345
+ self.configure_memory()
346
+ elif action == "skills":
347
+ self.configure_skills()
348
+ elif action == "test":
349
+ self.test_connections()
350
+ elif action == "view":
351
+ self.show_full_config()
352
+ elif action == "save":
353
+ self._save_all()
354
+ console.print("\n[bold green]✅ All configurations saved![/]")
355
+ console.print(f"[dim]Location: {self.config_path}[/]")
356
+ break
357
+ elif action == "quit":
358
+ if questionary.confirm("Discard unsaved changes?", default=False).ask():
359
+ break
360
+
361
+ console.print("\n[bold cyan]🚀 Ready to start your lollmsBot journey![/]")
362
+ console.print("[dim]Run: lollmsbot gateway[/]")
363
+
364
+ def _show_status_tree(self) -> None:
365
+ """Show configuration status as a tree."""
366
+ tree = Tree("📊 Configuration Status")
367
+
368
+ # Core services
369
+ services = tree.add("[bold]Services[/]")
370
+ for key in ["lollms", "discord", "telegram"]:
371
+ configured = key in self.config and self.config[key]
372
+ status = "✅" if configured else "⭕"
373
+ color = "green" if configured else "dim"
374
+ display_name = "AI Backend" if key == "lollms" else key.title()
375
+ services.add(f"[{color}]{status} {display_name}[/{color}]")
376
+
377
+ # Show current binding if configured
378
+ if "lollms" in self.config and "binding_name" in self.config["lollms"]:
379
+ binding = self.config["lollms"]["binding_name"]
380
+ services.add(f" [dim cyan]↳ Using: {binding}[/]")
381
+
382
+ # 7 Pillars
383
+ pillars = tree.add("[bold]7 Pillars[/]")
384
+ soul_ok = self.soul.name != "LollmsBot" or len(self.soul.traits) > 4
385
+ pillars.add(f"{'✅' if soul_ok else '⭕'} [cyan]Soul[/] (identity)")
386
+ pillars.add("✅ [cyan]Guardian[/] (security) - always active")
387
+
388
+ hb_config = self.heartbeat.config
389
+ pillars.add(f"{'✅' if hb_config.enabled else '⭕'} [cyan]Heartbeat[/] ({hb_config.interval_minutes}min)")
390
+ pillars.add(f"⭕ [dim]Memory[/] (configure in Heartbeat)")
391
+
392
+ # Skills
393
+ skill_count = len(self.skill_registry._skills)
394
+ pillars.add(f"{'✅' if skill_count > 5 else '⭕'} [cyan]Skills[/] ({skill_count} loaded)")
395
+
396
+ pillars.add("⭕ [dim]Tools[/] (enabled by default)")
397
+ pillars.add("⭕ [dim]Identity[/] (configure in Soul)")
398
+
399
+ console.print(tree)
400
+ console.print()
401
+
402
+ def configure_backend(self) -> None:
403
+ """Configure AI backend with binding-first selection."""
404
+ console.print("\n[bold blue]🔗 AI Backend Configuration[/]")
405
+ console.print("[dim]Select your LLM provider and configure connection details[/]")
406
+ console.print()
407
+
408
+ # Step 1: Select binding category
409
+ console.print("[bold]Step 1: Choose binding category[/]")
410
+
411
+ category = questionary.select(
412
+ "What type of backend?",
413
+ choices=[
414
+ Choice("🌐 Remote / Cloud APIs (OpenAI, Claude, etc.)", "remote"),
415
+ Choice("🏠 Local Server (Ollama, vLLM, Llama.cpp, etc.)", "local_server"),
416
+ Choice("💻 Local Direct (Transformers, TensorRT - no server)", "local_direct"),
417
+ ],
418
+ use_indicator=True,
419
+ ).ask()
420
+
421
+ # Step 2: Select specific binding from category
422
+ console.print(f"\n[bold]Step 2: Select {category.replace('_', ' ').title()} binding[/]")
423
+
424
+ # Filter bindings by category
425
+ category_bindings = {
426
+ name: info for name, info in AVAILABLE_BINDINGS.items()
427
+ if info.category == category
428
+ }
429
+
430
+ # Create choices with descriptions
431
+ binding_choices = [
432
+ Choice(
433
+ f"{info.display_name} - {info.description}",
434
+ name
435
+ )
436
+ for name, info in sorted(
437
+ category_bindings.items(),
438
+ key=lambda x: x[1].display_name
439
+ )
440
+ ]
441
+
442
+ binding_name = questionary.select(
443
+ "Which binding?",
444
+ choices=binding_choices,
445
+ use_indicator=True,
446
+ ).ask()
447
+
448
+ binding_info = AVAILABLE_BINDINGS[binding_name]
449
+
450
+ # Step 3: Configure based on binding type
451
+ console.print(f"\n[bold]Step 3: Configure {binding_info.display_name}[/]")
452
+
453
+ lollms_config = self.config.setdefault("lollms", {})
454
+ lollms_config["binding_name"] = binding_name
455
+
456
+ # Common configuration
457
+ console.print(Panel(
458
+ f"[bold]{binding_info.display_name}[/]\n"
459
+ f"Category: {binding_info.category.replace('_', ' ').title()}\n"
460
+ f"Description: {binding_info.description}",
461
+ title="Selected Binding",
462
+ border_style="green"
463
+ ))
464
+
465
+ # Model name (required for all)
466
+ default_model = binding_info.default_model or ""
467
+ current_model = lollms_config.get("model_name", default_model)
468
+ model_name = questionary.text(
469
+ "Model name",
470
+ default=current_model,
471
+ instruction="e.g., gpt-4o-mini, llama3.2, claude-3-5-sonnet-20241022"
472
+ ).ask()
473
+ lollms_config["model_name"] = model_name
474
+
475
+ # Host address (for remote and local_server)
476
+ if binding_info.category in ("remote", "local_server"):
477
+ default_host = binding_info.default_host or "http://localhost:8080"
478
+ current_host = lollms_config.get("host_address", default_host)
479
+ host_address = questionary.text(
480
+ "Host address / API endpoint",
481
+ default=current_host,
482
+ instruction="Full URL including http:// or https://"
483
+ ).ask()
484
+ lollms_config["host_address"] = host_address
485
+
486
+ # API key / service key
487
+ if binding_info.requires_api_key:
488
+ has_key = questionary.confirm(
489
+ "Do you have an API key / service key?",
490
+ default=True
491
+ ).ask()
492
+
493
+ if has_key:
494
+ current_key = lollms_config.get("api_key", "")
495
+ api_key = questionary.password(
496
+ "API / Service key",
497
+ default=current_key,
498
+ ).ask()
499
+ lollms_config["api_key"] = api_key
500
+ else:
501
+ console.print("[yellow]⚠️ Most remote APIs require a key. You can add one later.[/]")
502
+ lollms_config["api_key"] = ""
503
+ else:
504
+ # Optional key (e.g., for local servers with optional auth)
505
+ current_key = lollms_config.get("api_key", "")
506
+ if current_key or questionary.confirm(
507
+ "Add optional API key? (for authenticated servers/proxies)",
508
+ default=bool(current_key)
509
+ ).ask():
510
+ api_key = questionary.password(
511
+ "API / Service key (optional)",
512
+ default=current_key,
513
+ ).ask()
514
+ lollms_config["api_key"] = api_key
515
+
516
+ # SSL verification (if supported)
517
+ if binding_info.supports_ssl_verify:
518
+ default_verify = lollms_config.get("verify_ssl", True)
519
+ # For local servers, default to False for convenience
520
+ if binding_info.category == "local_server" and "localhost" in host_address:
521
+ default_verify = lollms_config.get("verify_ssl", False)
522
+
523
+ verify_ssl = questionary.confirm(
524
+ "Verify SSL certificates?",
525
+ default=default_verify
526
+ ).ask()
527
+ lollms_config["verify_ssl"] = verify_ssl
528
+
529
+ if not verify_ssl:
530
+ console.print("[yellow]⚠️ SSL verification disabled. Only use for trusted local servers.[/]")
531
+
532
+ # Custom certificate (advanced)
533
+ if questionary.confirm("Use custom SSL certificate file? (advanced)", default=False).ask():
534
+ cert_path = questionary.text("Path to certificate file (.pem, .crt):").ask()
535
+ lollms_config["certificate_file_path"] = cert_path
536
+
537
+ # Models path (for local direct bindings and some local servers)
538
+ if binding_info.requires_models_path or (
539
+ binding_info.category == "local_direct" and
540
+ questionary.confirm("Specify models folder path?", default=True).ask()
541
+ ):
542
+ default_path = str(Path.home() / "models")
543
+ current_path = lollms_config.get("models_path", default_path)
544
+ models_path = questionary.text(
545
+ "Models folder path",
546
+ default=current_path,
547
+ instruction="Directory containing .gguf, .bin, or model files"
548
+ ).ask()
549
+ lollms_config["models_path"] = models_path
550
+
551
+ # Expand user path
552
+ models_path_expanded = os.path.expanduser(models_path)
553
+ if not Path(models_path_expanded).exists():
554
+ console.print(f"[yellow]⚠️ Path doesn't exist yet: {models_path_expanded}[/]")
555
+ if questionary.confirm("Create this directory?", default=True).ask():
556
+ Path(models_path_expanded).mkdir(parents=True, exist_ok=True)
557
+ console.print("[green]✅ Directory created[/]")
558
+
559
+ # Step 4: Optional advanced settings
560
+ console.print("\n[bold]Step 4: Advanced settings (optional)[/]")
561
+
562
+ if questionary.confirm("Configure advanced options?", default=False).ask():
563
+ # Context size
564
+ current_ctx = lollms_config.get("context_size", 4096)
565
+ context_size = IntPrompt.ask(
566
+ "Context size (tokens)",
567
+ default=current_ctx
568
+ )
569
+ lollms_config["context_size"] = context_size
570
+
571
+ # Temperature
572
+ current_temp = lollms_config.get("temperature", 0.7)
573
+ temperature = FloatPrompt.ask(
574
+ "Default temperature (0-2)",
575
+ default=current_temp
576
+ )
577
+ lollms_config["temperature"] = max(0.0, min(2.0, temperature))
578
+
579
+ # Summary and test
580
+ console.print("\n[bold green]✅ Backend configured![/]")
581
+ self._configured.add("lollms")
582
+
583
+ # Show configuration summary
584
+ self._show_backend_summary(lollms_config, binding_info)
585
+
586
+ # Offer to test
587
+ if questionary.confirm("Test connection now?", default=True).ask():
588
+ self._test_backend_connection(lollms_config)
589
+
590
+ def _show_backend_summary(self, config: Dict[str, Any], binding_info: BindingInfo) -> None:
591
+ """Show a summary of the backend configuration."""
592
+ table = Table(title="Backend Configuration Summary")
593
+ table.add_column("Setting", style="cyan")
594
+ table.add_column("Value", style="green")
595
+
596
+ table.add_row("Binding", binding_info.display_name)
597
+ table.add_row("Model", config.get("model_name", "Not set") or "Not set")
598
+
599
+ if binding_info.category in ("remote", "local_server"):
600
+ table.add_row("Host", config.get("host_address", "Not set") or "Not set")
601
+ has_key = bool(config.get("api_key"))
602
+ table.add_row("API Key", "✅ Set" if has_key else "⭕ Not set")
603
+ if binding_info.supports_ssl_verify:
604
+ table.add_row("SSL Verify", "✅ Yes" if config.get("verify_ssl", True) else "❌ No")
605
+
606
+ if binding_info.requires_models_path or config.get("models_path"):
607
+ table.add_row("Models Path", config.get("models_path", "Not set") or "Not set")
608
+
609
+ table.add_row("Context Size", str(config.get("context_size", 4096)))
610
+ table.add_row("Temperature", str(config.get("temperature", 0.7)))
611
+
612
+ console.print(table)
613
+
614
+ def _test_backend_connection(self, config: Dict[str, Any]) -> None:
615
+ """Test the backend connection."""
616
+ console.print("\n[bold]🧪 Testing connection...[/]")
617
+
618
+ try:
619
+ # Build settings from config
620
+ settings = LollmsSettings(
621
+ host_address=config.get("host_address", ""),
622
+ api_key=config.get("api_key"),
623
+ binding_name=config.get("binding_name"),
624
+ model_name=config.get("model_name"),
625
+ context_size=config.get("context_size", 4096),
626
+ verify_ssl=config.get("verify_ssl", True),
627
+ )
628
+
629
+ # Try to build client
630
+ client = build_lollms_client(settings)
631
+
632
+ if client:
633
+ console.print("[bold green]✅ Client initialized successfully![/]")
634
+ console.print("[dim]Connection appears valid. Full test requires running gateway.[/]")
635
+ else:
636
+ console.print("[yellow]⚠️ Could not initialize client - check configuration[/]")
637
+
638
+ except Exception as e:
639
+ console.print(f"[red]❌ Connection test failed: {e}[/]")
640
+ console.print("[dim]Tip: Ensure the backend service is running and accessible[/]")
641
+
642
+ # Legacy method - kept for backward compatibility but not used in main flow
643
+ def configure_service(self, service_name: str) -> None:
644
+ """Configure a non-backend service (Discord, Telegram)."""
645
+ if service_name == "lollms":
646
+ # Redirect to new binding-first configuration
647
+ return self.configure_backend()
648
+
649
+ # Legacy configuration for other services
650
+ SERVICES_CONFIG: Dict[str, Dict[str, Any]] = {
651
+ "discord": {
652
+ "title": "🤖 Discord Bot",
653
+ "fields": [
654
+ {"name": "bot_token", "prompt": "Discord Bot Token", "secret": True},
655
+ {"name": "allowed_users", "prompt": "Allowed User IDs (comma-separated, optional)", "optional": True},
656
+ {"name": "allowed_guilds", "prompt": "Allowed Server IDs (comma-separated, optional)", "optional": True},
657
+ ],
658
+ "setup_instructions": """🤖 Discord Setup (2 min):
659
+
660
+ 1. https://discord.com/developers/applications → [+ New Application]
661
+ 2. Bot → [Add Bot] → Copy **TOKEN** (MTIz... format)
662
+ 3. Bot → Privileged Gateway Intents → ✅ Message Content
663
+ 4. OAuth2 → URL Generator → bot scope → Invite to server""",
664
+ },
665
+ "telegram": {
666
+ "title": "✈️ Telegram Bot",
667
+ "fields": [
668
+ {"name": "bot_token", "prompt": "Telegram Bot Token (from @BotFather)", "secret": True},
669
+ {"name": "allowed_users", "prompt": "Allowed User IDs (comma-separated, optional)", "optional": True},
670
+ ],
671
+ "setup_instructions": """✈️ Telegram Setup (1 min):
672
+
673
+ 1. Message @BotFather on Telegram
674
+ 2. Send /newbot and follow instructions
675
+ 3. Copy the HTTP API token provided""",
676
+ },
677
+ }
678
+
679
+ service = SERVICES_CONFIG.get(service_name)
680
+ if not service:
681
+ console.print(f"[red]Unknown service: {service_name}[/]")
682
+ return
683
+
684
+ console.print(f"\n[bold yellow]{service['title']}[/]")
685
+
686
+ if "setup_instructions" in service:
687
+ console.print(Panel(service["setup_instructions"], title="📋 Instructions"))
688
+
689
+ service_config = self.config.setdefault(service_name, {})
690
+
691
+ for field in service["fields"]:
692
+ current = service_config.get(field["name"])
693
+ default = str(current) if current is not None else field.get("default", "")
694
+
695
+ if field.get("type") == "bool":
696
+ value = questionary.confirm(field["prompt"], default=field.get("default", False)).ask()
697
+ elif field.get("secret"):
698
+ value = questionary.password(field["prompt"], default=default).ask()
699
+ else:
700
+ value = questionary.text(field["prompt"], default=default).ask()
701
+
702
+ # Parse comma-separated lists
703
+ if "users" in field["name"] or "guilds" in field["name"]:
704
+ if value:
705
+ value = [v.strip() for v in value.split(",") if v.strip()]
706
+ else:
707
+ value = []
708
+ elif field.get("optional") and not value:
709
+ continue
710
+
711
+ service_config[field["name"]] = value
712
+
713
+ self._configured.add(service_name)
714
+ console.print("[green]✅ Updated![/]")
715
+
716
+ def configure_soul(self) -> None:
717
+ """Interactive Soul (personality) configuration."""
718
+ console.print("\n[bold magenta]🧬 Soul Configuration[/]")
719
+
720
+ while True:
721
+ section = questionary.select(
722
+ "Configure aspect:",
723
+ choices=[
724
+ "🎭 Core Identity (name, purpose, origin)",
725
+ "🌈 Personality Traits",
726
+ "⚖️ Core Values",
727
+ "💬 Communication Style",
728
+ "🎓 Expertise Domains",
729
+ "👥 Relationship Stances",
730
+ "🔍 Preview System Prompt",
731
+ "💾 Save & Return",
732
+ ]
733
+ ).ask()
734
+
735
+ if section == "🎭 Core Identity (name, purpose, origin)":
736
+ self._configure_core_identity()
737
+ elif section == "🌈 Personality Traits":
738
+ self._configure_personality_traits()
739
+ elif section == "⚖️ Core Values":
740
+ self._configure_values()
741
+ elif section == "💬 Communication Style":
742
+ self._configure_communication()
743
+ elif section == "🎓 Expertise Domains":
744
+ self._configure_expertise()
745
+ elif section == "👥 Relationship Stances":
746
+ self._configure_relationships()
747
+ elif section == "🔍 Preview System Prompt":
748
+ self._preview_soul_prompt()
749
+ else:
750
+ self.soul._save()
751
+ self._configured.add("soul")
752
+ console.print("[green]✅ Soul saved![/]")
753
+ break
754
+
755
+ def _configure_core_identity(self) -> None:
756
+ """Configure name, purpose, and origin story."""
757
+ console.print("\n[bold]Core Identity[/]")
758
+
759
+ self.soul.name = questionary.text("AI Name", default=self.soul.name).ask()
760
+ self.soul.purpose = questionary.text("Primary Purpose", default=self.soul.purpose).ask()
761
+ self.soul.origin_story = questionary.text("Origin Story", default=self.soul.origin_story).ask()
762
+
763
+ def _configure_personality_traits(self) -> None:
764
+ """Add, edit, or remove personality traits."""
765
+ console.print("\n[bold]Personality Traits[/]")
766
+
767
+ while True:
768
+ table = Table(title="Current Traits")
769
+ table.add_column("Trait")
770
+ table.add_column("Intensity")
771
+ table.add_column("Description")
772
+
773
+ for trait in self.soul.traits:
774
+ intensity_emoji = {
775
+ TraitIntensity.SUBTLE: "◐",
776
+ TraitIntensity.MODERATE: "◑",
777
+ TraitIntensity.STRONG: "◕",
778
+ TraitIntensity.EXTREME: "⬤",
779
+ }.get(trait.intensity, "◑")
780
+ table.add_row(trait.name, f"{intensity_emoji} {trait.intensity.name.lower()}", trait.description[:40])
781
+
782
+ console.print(table)
783
+
784
+ action = questionary.select(
785
+ "Action:",
786
+ choices=["➕ Add Trait", "✏️ Edit Trait", "🗑️ Remove Trait", "🔙 Back"]
787
+ ).ask()
788
+
789
+ if action == "➕ Add Trait":
790
+ name = questionary.text("Trait name (e.g., 'curiosity', 'pragmatism')").ask()
791
+ description = questionary.text("How does this manifest?").ask()
792
+ intensity = questionary.select(
793
+ "Intensity",
794
+ choices=["subtle", "moderate", "strong", "extreme"],
795
+ default="moderate"
796
+ ).ask()
797
+
798
+ trait = PersonalityTrait(
799
+ name=name,
800
+ description=description,
801
+ intensity=TraitIntensity[intensity.upper()],
802
+ )
803
+ self.soul.traits.append(trait)
804
+
805
+ elif action == "✏️ Edit Trait" and self.soul.traits:
806
+ trait_names = [t.name for t in self.soul.traits]
807
+ to_edit = questionary.select("Edit which trait?", choices=trait_names).ask()
808
+ trait = next(t for t in self.soul.traits if t.name == to_edit)
809
+
810
+ trait.description = questionary.text("Description", default=trait.description).ask()
811
+ new_intensity = questionary.select(
812
+ "Intensity",
813
+ choices=["subtle", "moderate", "strong", "extreme"],
814
+ default=trait.intensity.name.lower()
815
+ ).ask()
816
+ trait.intensity = TraitIntensity[new_intensity.upper()]
817
+
818
+ elif action == "🗑️ Remove Trait" and self.soul.traits:
819
+ to_remove = questionary.select(
820
+ "Remove which trait?",
821
+ choices=[t.name for t in self.soul.traits]
822
+ ).ask()
823
+ self.soul.traits = [t for t in self.soul.traits if t.name != to_remove]
824
+ else:
825
+ break
826
+
827
+ def _configure_values(self) -> None:
828
+ """Configure core ethical values."""
829
+ console.print("\n[bold]Core Values[/]")
830
+
831
+ while True:
832
+ table = Table(title="Current Values (by priority)")
833
+ table.add_column("Priority")
834
+ table.add_column("Value")
835
+ table.add_column("Category")
836
+
837
+ for v in sorted(self.soul.values, key=lambda x: -x.priority):
838
+ priority_color = "red" if v.priority >= 9 else "yellow" if v.priority >= 7 else "green"
839
+ table.add_row(f"[{priority_color}]{v.priority}[/{priority_color}]", v.statement[:50], v.category)
840
+
841
+ console.print(table)
842
+
843
+ action = questionary.select(
844
+ "Action:",
845
+ choices=["➕ Add Value", "✏️ Edit Priority", "🗑️ Remove Value", "🔙 Back"]
846
+ ).ask()
847
+
848
+ if action == "➕ Add Value":
849
+ statement = questionary.text("Value statement").ask()
850
+ category = questionary.text("Category", default="general").ask()
851
+ priority = IntPrompt.ask("Priority (1-10)", default=5)
852
+ self.soul.values.append(ValueStatement(statement, category, max(1, min(10, priority))))
853
+
854
+ elif action == "✏️ Edit Priority" and self.soul.values:
855
+ statements = [v.statement[:40] + "..." for v in self.soul.values]
856
+ to_edit = questionary.select("Edit which value?", choices=statements).ask()
857
+ val = next(v for v in self.soul.values if v.statement.startswith(to_edit[:20]))
858
+ val.priority = IntPrompt.ask("New priority (1-10)", default=val.priority)
859
+
860
+ elif action == "🗑️ Remove Value" and self.soul.values:
861
+ to_remove = questionary.select(
862
+ "Remove which value?",
863
+ choices=[v.statement[:40] for v in self.soul.values]
864
+ ).ask()
865
+ self.soul.values = [v for v in self.soul.values if not v.statement.startswith(to_remove[:20])]
866
+ else:
867
+ break
868
+
869
+ def _configure_communication(self) -> None:
870
+ """Configure communication style."""
871
+ style = self.soul.communication
872
+
873
+ style.formality = questionary.select(
874
+ "Formality",
875
+ choices=["formal", "casual", "technical", "playful"],
876
+ default=style.formality
877
+ ).ask()
878
+
879
+ style.verbosity = questionary.select(
880
+ "Default verbosity",
881
+ choices=["terse", "concise", "detailed", "exhaustive"],
882
+ default=style.verbosity
883
+ ).ask()
884
+
885
+ humor = questionary.select(
886
+ "Humor style",
887
+ choices=["None (serious)", "witty", "dry", "punny", "absurdist"],
888
+ default=style.humor_style or "None (serious)"
889
+ ).ask()
890
+ style.humor_style = None if humor == "None (serious)" else humor
891
+
892
+ style.emoji_usage = questionary.select(
893
+ "Emoji usage",
894
+ choices=["none", "minimal", "moderate", "liberal"],
895
+ default=style.emoji_usage
896
+ ).ask()
897
+
898
+ def _configure_expertise(self) -> None:
899
+ """Configure knowledge domains."""
900
+ console.print("\n[bold]Expertise Domains[/]")
901
+
902
+ while True:
903
+ table = Table(title="Current Expertise")
904
+ table.add_column("Domain")
905
+ table.add_column("Level")
906
+ table.add_column("Specialties")
907
+
908
+ for e in self.soul.expertise:
909
+ level_color = {
910
+ "novice": "red", "competent": "yellow", "expert": "green",
911
+ "authority": "blue", "pioneer": "magenta",
912
+ }.get(e.level, "white")
913
+ table.add_row(e.domain, f"[{level_color}]{e.level}[/{level_color}]", ", ".join(e.specialties[:2]))
914
+
915
+ console.print(table)
916
+
917
+ action = questionary.select(
918
+ "Action:",
919
+ choices=["➕ Add Domain", "🔙 Back"]
920
+ ).ask()
921
+
922
+ if action == "➕ Add Domain":
923
+ domain = questionary.text("Domain name").ask()
924
+ level = questionary.select(
925
+ "Competence level",
926
+ choices=["novice", "competent", "expert", "authority", "pioneer"],
927
+ default="competent"
928
+ ).ask()
929
+ specialties = [s.strip() for s in questionary.text("Specialties (comma-separated)").ask().split(",") if s.strip()]
930
+
931
+ self.soul.expertise.append(ExpertiseDomain(domain=domain, level=level, specialties=specialties))
932
+ else:
933
+ break
934
+
935
+ def _configure_relationships(self) -> None:
936
+ """Configure relationship stances."""
937
+ console.print("\n[bold]Relationship Stances[/]")
938
+ console.print("[dim]Simplified configuration - full implementation in soul.md[/]")
939
+
940
+ def _preview_soul_prompt(self) -> None:
941
+ """Preview the generated system prompt."""
942
+ prompt = self.soul.generate_system_prompt()
943
+ preview = prompt[:1000] + ("..." if len(prompt) > 1000 else "")
944
+ console.print(Panel(preview, title="System Prompt", border_style="cyan"))
945
+
946
+ def configure_heartbeat(self) -> None:
947
+ """Configure self-maintenance heartbeat."""
948
+ console.print("\n[bold magenta]💓 Heartbeat Configuration[/]")
949
+
950
+ config = self.heartbeat.config
951
+
952
+ config.enabled = questionary.confirm("Enable automatic self-maintenance?", default=config.enabled).ask()
953
+ if not config.enabled:
954
+ self.heartbeat._save_config()
955
+ return
956
+
957
+ config.interval_minutes = FloatPrompt.ask("Maintenance interval (minutes)", default=config.interval_minutes)
958
+
959
+ console.print("\n[bold]Maintenance Tasks[/]")
960
+ for task in MaintenanceTask:
961
+ task_name = task.name.replace("_", " ").title()
962
+ config.tasks_enabled[task] = questionary.confirm(
963
+ f"Enable {task_name}?", default=config.tasks_enabled.get(task, True)
964
+ ).ask()
965
+
966
+ console.print("\n[bold]Self-Healing Behavior[/]")
967
+ config.auto_heal_minor = questionary.confirm("Auto-fix minor issues?", default=config.auto_heal_minor).ask()
968
+ config.confirm_heal_major = questionary.confirm("Confirm before major changes?", default=config.confirm_heal_major).ask()
969
+
970
+ self.heartbeat.update_config(**{
971
+ k: getattr(config, k) for k in [
972
+ "enabled", "interval_minutes", "tasks_enabled",
973
+ "auto_heal_minor", "confirm_heal_major"
974
+ ]
975
+ })
976
+
977
+ self.config["heartbeat"] = {
978
+ "enabled": config.enabled,
979
+ "interval_minutes": config.interval_minutes,
980
+ "tasks_enabled": [t.name for t, v in config.tasks_enabled.items() if v],
981
+ }
982
+ self._configured.add("heartbeat")
983
+ console.print("[green]✅ Heartbeat configured![/]")
984
+
985
+ def configure_memory(self) -> None:
986
+ """Configure memory and retention settings."""
987
+ console.print("\n[bold magenta]🧠 Memory Configuration[/]")
988
+
989
+ hb_config = self.heartbeat.config
990
+
991
+ hb_config.memory_pressure_threshold = FloatPrompt.ask(
992
+ "Memory pressure threshold (0-1)", default=hb_config.memory_pressure_threshold
993
+ )
994
+ hb_config.log_retention_days = IntPrompt.ask(
995
+ "Audit log retention (days)", default=hb_config.log_retention_days
996
+ )
997
+
998
+ console.print("\n[bold]Forgetting Curve Parameters[/]")
999
+ halflife = FloatPrompt.ask("Memory half-life (days)", default=7.0)
1000
+ strength_mult = FloatPrompt.ask("Review strength multiplier", default=2.0)
1001
+
1002
+ self.heartbeat.memory_monitor.retention_halflife_days = halflife
1003
+ self.heartbeat.memory_monitor.strength_multiplier = strength_mult
1004
+ self.heartbeat._save_config()
1005
+
1006
+ self.config["memory"] = {
1007
+ "pressure_threshold": hb_config.memory_pressure_threshold,
1008
+ "log_retention_days": hb_config.log_retention_days,
1009
+ "retention_halflife_days": halflife,
1010
+ "strength_multiplier": strength_mult,
1011
+ }
1012
+ self._configured.add("memory")
1013
+ console.print("[green]✅ Memory configured![/]")
1014
+
1015
+ def configure_skills(self) -> None:
1016
+ """Configure Skills - browse, test, and manage capabilities."""
1017
+ console.print("\n[bold magenta]📚 Skills Configuration[/]")
1018
+ console.print("[dim]Browse, test, and configure LollmsBot's capabilities[/]")
1019
+
1020
+ while True:
1021
+ # Show skill statistics
1022
+ stats = self._get_skill_stats()
1023
+
1024
+ table = Table(title=f"Skills Library ({stats['total']} total)")
1025
+ table.add_column("Category")
1026
+ table.add_column("Built-in")
1027
+ table.add_column("User-created")
1028
+ table.add_column("Avg Confidence")
1029
+
1030
+ for cat, data in sorted(stats['by_category'].items()):
1031
+ table.add_row(
1032
+ cat,
1033
+ str(data['builtin']),
1034
+ str(data['user']),
1035
+ f"{data['avg_confidence']:.0%}"
1036
+ )
1037
+
1038
+ console.print(table)
1039
+
1040
+ action = questionary.select(
1041
+ "Skills action:",
1042
+ choices=[
1043
+ "🔍 Browse & Search Skills",
1044
+ "📖 View Skill Details",
1045
+ "🧪 Test Skill Execution",
1046
+ "➕ Compose New Skill (from existing)",
1047
+ "📤 Export Skill Library",
1048
+ "📥 Import Skills",
1049
+ "⚙️ Skill Preferences",
1050
+ "🔙 Back to Main Menu",
1051
+ ]
1052
+ ).ask()
1053
+
1054
+ if action == "🔍 Browse & Search Skills":
1055
+ self._browse_skills()
1056
+ elif action == "📖 View Skill Details":
1057
+ self._view_skill_details()
1058
+ elif action == "🧪 Test Skill Execution":
1059
+ self._test_skill()
1060
+ elif action == "➕ Compose New Skill (from existing)":
1061
+ self._compose_skill()
1062
+ elif action == "📤 Export Skill Library":
1063
+ self._export_skills()
1064
+ elif action == "📥 Import Skills":
1065
+ self._import_skills()
1066
+ elif action == "⚙️ Skill Preferences":
1067
+ self._skill_preferences()
1068
+ else:
1069
+ self._configured.add("skills")
1070
+ break
1071
+
1072
+ def _get_skill_stats(self) -> Dict[str, Any]:
1073
+ """Get statistics about loaded skills."""
1074
+ skills = list(self.skill_registry._skills.values())
1075
+
1076
+ by_category: Dict[str, Dict[str, Any]] = {}
1077
+ for skill in skills:
1078
+ for cat in skill.metadata.categories or ["uncategorized"]:
1079
+ if cat not in by_category:
1080
+ by_category[cat] = {'builtin': 0, 'user': 0, 'confidence_sum': 0, 'count': 0}
1081
+ # Simplified: would track builtin vs user properly
1082
+ by_category[cat]['count'] += 1
1083
+ by_category[cat]['confidence_sum'] += skill.metadata.confidence_score
1084
+
1085
+ # Calculate averages
1086
+ for cat in by_category:
1087
+ data = by_category[cat]
1088
+ data['avg_confidence'] = data['confidence_sum'] / data['count'] if data['count'] > 0 else 0
1089
+
1090
+ return {
1091
+ 'total': len(skills),
1092
+ 'by_category': by_category,
1093
+ 'by_complexity': {
1094
+ c.name: len(self.skill_registry.list_skills(complexity=c))
1095
+ for c in SkillComplexity
1096
+ },
1097
+ }
1098
+
1099
+ def _browse_skills(self) -> None:
1100
+ """Browse and search skills interactively."""
1101
+ search = questionary.text("Search skills (empty for all):").ask()
1102
+
1103
+ if search:
1104
+ results = self.skill_registry.search(search)
1105
+ skills = [s for s, _ in results]
1106
+ else:
1107
+ category = questionary.select(
1108
+ "Filter by category:",
1109
+ choices=["All"] + list(self.skill_registry._categories.keys())
1110
+ ).ask()
1111
+ if category == "All":
1112
+ skills = list(self.skill_registry._skills.values())
1113
+ else:
1114
+ skills = self.skill_registry.list_skills(category=category)
1115
+
1116
+ # Display results
1117
+ table = Table(title=f"Skills ({len(skills)} found)")
1118
+ table.add_column("Name")
1119
+ table.add_column("Complexity")
1120
+ table.add_column("Description")
1121
+ table.add_column("Confidence")
1122
+
1123
+ for skill in skills[:20]: # Limit display
1124
+ conf_color = "green" if skill.metadata.confidence_score > 0.8 else "yellow" if skill.metadata.confidence_score > 0.5 else "red"
1125
+ table.add_row(
1126
+ skill.name,
1127
+ skill.metadata.complexity.name,
1128
+ skill.metadata.description[:40],
1129
+ f"[{conf_color}]{skill.metadata.confidence_score:.0%}[/{conf_color}]"
1130
+ )
1131
+
1132
+ console.print(table)
1133
+
1134
+ def _view_skill_details(self) -> None:
1135
+ """View detailed information about a specific skill."""
1136
+ skill_name = questionary.select(
1137
+ "Select skill:",
1138
+ choices=list(self.skill_registry._skills.keys())
1139
+ ).ask()
1140
+
1141
+ skill = self.skill_registry.get(skill_name)
1142
+ if not skill:
1143
+ console.print("[red]Skill not found[/]")
1144
+ return
1145
+
1146
+ md = skill.metadata
1147
+
1148
+ details = f"""
1149
+ [bold]{md.name}[/] v{md.version}
1150
+ [dim]{md.description}[/]
1151
+
1152
+ [bold]Complexity:[/] {md.complexity.name}
1153
+ [bold]Categories:[/] {', '.join(md.categories)}
1154
+ [bold]Tags:[/] {', '.join(md.tags)}
1155
+
1156
+ [bold]When to use:[/] {md.when_to_use or 'N/A'}
1157
+ [bold]When NOT to use:[/] {md.when_not_to_use or 'N/A'}
1158
+
1159
+ [bold]Parameters:[/]
1160
+ {chr(10).join(f" • {p.name} ({p.type}){' [required]' if p.required else ''}: {p.description}" for p in md.parameters)}
1161
+
1162
+ [bold]Dependencies:[/]
1163
+ {chr(10).join(f" • {d.kind}:{d.name}{' (optional)' if d.optional else ''}" for d in md.dependencies)}
1164
+
1165
+ [bold]Statistics:[/]
1166
+ • Executed: {md.execution_count} times
1167
+ • Success rate: {md.success_rate:.1%}
1168
+ • Confidence score: {md.confidence_score:.0%}
1169
+ """
1170
+ console.print(Panel(details, title=f"Skill: {md.name}", border_style="blue"))
1171
+
1172
+ # Show examples if any
1173
+ if md.examples:
1174
+ console.print("\n[bold]Examples:[/]")
1175
+ for i, ex in enumerate(md.examples[:2], 1):
1176
+ console.print(Panel(
1177
+ f"Input: {json.dumps(ex.input_params, indent=2)}\n"
1178
+ f"Output: {json.dumps(ex.expected_output, indent=2)}",
1179
+ title=f"Example {i}"
1180
+ ))
1181
+
1182
+ def _test_skill(self) -> None:
1183
+ """Test execute a skill with sample inputs."""
1184
+ console.print("[yellow]Note: Full execution requires running agent. Showing validation only.[/]")
1185
+
1186
+ skill_name = questionary.select(
1187
+ "Select skill to test:",
1188
+ choices=list(self.skill_registry._skills.keys())
1189
+ ).ask()
1190
+
1191
+ skill = self.skill_registry.get(skill_name)
1192
+
1193
+ # Gather inputs
1194
+ inputs = {}
1195
+ for param in skill.metadata.parameters:
1196
+ if not param.required:
1197
+ if not questionary.confirm(f"Provide optional parameter '{param.name}'?", default=False).ask():
1198
+ continue
1199
+
1200
+ value = questionary.text(f"{param.name} ({param.type}): {param.description}").ask()
1201
+
1202
+ # Simple type coercion
1203
+ if param.type == "number":
1204
+ value = float(value) if '.' in value else int(value)
1205
+ elif param.type == "boolean":
1206
+ value = value.lower() in ('true', 'yes', '1', 'on')
1207
+ elif param.type == "array":
1208
+ value = [v.strip() for v in value.split(',')]
1209
+ elif param.type == "object":
1210
+ try:
1211
+ value = json.loads(value)
1212
+ except:
1213
+ value = {"raw": value}
1214
+
1215
+ inputs[param.name] = value
1216
+
1217
+ # Validate
1218
+ valid, errors = skill.validate_inputs(inputs)
1219
+ if valid:
1220
+ console.print("[green]✅ Inputs valid![/]")
1221
+
1222
+ # Check dependencies
1223
+ # Would need actual agent/tools to check properly
1224
+ console.print("[dim]Dependency check: would validate against available tools[/]")
1225
+ else:
1226
+ console.print("[red]❌ Validation failed:[/]")
1227
+ for err in errors:
1228
+ console.print(f" • {err}")
1229
+
1230
+ def _compose_skill(self) -> None:
1231
+ """Create new skill by composing existing skills."""
1232
+ console.print("\n[bold]Compose New Skill[/]")
1233
+ console.print("[dim]Combine existing skills into a workflow[/]")
1234
+
1235
+ name = questionary.text("Name for new skill:").ask()
1236
+ description = questionary.text("What does this skill do?").ask()
1237
+
1238
+ # Select component skills
1239
+ available = list(self.skill_registry._skills.keys())
1240
+ components = []
1241
+
1242
+ while True:
1243
+ remaining = [s for s in available if s not in components]
1244
+ if not remaining:
1245
+ break
1246
+
1247
+ choice = questionary.select(
1248
+ "Add component skill (or Done):",
1249
+ choices=["Done"] + remaining
1250
+ ).ask()
1251
+
1252
+ if choice == "Done":
1253
+ break
1254
+
1255
+ components.append(choice)
1256
+ console.print(f"[green]Added: {choice}[/]")
1257
+
1258
+ if len(components) < 1:
1259
+ console.print("[yellow]Need at least one component[/]")
1260
+ return
1261
+
1262
+ # Define data flow (simplified)
1263
+ console.print("\n[dim]Data flow would be configured here - mapping outputs to inputs[/]")
1264
+
1265
+ # Preview and confirm
1266
+ console.print(Panel(
1267
+ f"Name: {name}\n"
1268
+ f"Description: {description}\n"
1269
+ f"Components: {' → '.join(components)}",
1270
+ title="New Skill Preview"
1271
+ ))
1272
+
1273
+ if questionary.confirm("Create this skill?", default=True).ask():
1274
+ # Would call skill learner
1275
+ console.print("[green]✅ Skill composition recorded (implementation in code)[/]")
1276
+
1277
+ def _export_skills(self) -> None:
1278
+ """Export skills to file."""
1279
+ export_path = Path.home() / ".lollmsbot" / "skills_export.json"
1280
+
1281
+ data = {
1282
+ "export_date": datetime.now().isoformat(),
1283
+ "skills": [skill.to_dict() for skill in self.skill_registry._skills.values()],
1284
+ }
1285
+
1286
+ export_path.write_text(json.dumps(data, indent=2))
1287
+ console.print(f"[green]✅ Exported {len(data['skills'])} skills to {export_path}[/]")
1288
+
1289
+ def _import_skills(self) -> None:
1290
+ """Import skills from file."""
1291
+ import_path = questionary.text("Path to skills file:").ask()
1292
+ path = Path(import_path)
1293
+
1294
+ if not path.exists():
1295
+ console.print("[red]File not found[/]")
1296
+ return
1297
+
1298
+ try:
1299
+ data = json.loads(path.read_text())
1300
+ count = len(data.get("skills", []))
1301
+ console.print(f"[green]✅ Found {count} skills to import[/]")
1302
+ console.print("[dim]Import would validate and register skills here[/]")
1303
+ except Exception as e:
1304
+ console.print(f"[red]Import failed: {e}[/]")
1305
+
1306
+ def _skill_preferences(self) -> None:
1307
+ """Configure skill execution preferences."""
1308
+ console.print("\n[bold]Skill Preferences[/]")
1309
+
1310
+ # Would configure: auto-skill vs manual, confidence thresholds, etc.
1311
+ prefs = {
1312
+ "auto_skill_selection": questionary.confirm("Allow automatic skill selection?", default=True).ask(),
1313
+ "min_confidence_threshold": FloatPrompt.ask("Minimum skill confidence (0-1)", default=0.6),
1314
+ "confirm_complex_skills": questionary.confirm("Confirm before complex skill execution?", default=True).ask(),
1315
+ }
1316
+
1317
+ self.config["skill_preferences"] = prefs
1318
+ console.print("[green]✅ Preferences saved[/]")
1319
+
1320
+ def test_connections(self) -> None:
1321
+ """Test all configured connections."""
1322
+ table = Table(title="🧪 Connection Tests")
1323
+ table.add_column("Service")
1324
+ table.add_column("Status")
1325
+ table.add_column("Details")
1326
+
1327
+ # Test backend first
1328
+ lollms_config = self.config.get("lollms", {})
1329
+ if lollms_config:
1330
+ status, details = self._test_single("lollms", lollms_config)
1331
+ # Show binding name in details
1332
+ binding_name = lollms_config.get("binding_name", "unknown")
1333
+ details = f"{binding_name}: {details}"
1334
+ table.add_row("AI Backend", status, details)
1335
+
1336
+ # Test other services
1337
+ for service_name, svc_config in self.config.items():
1338
+ if service_name in ["lollms", "heartbeat", "memory", "soul", "skill_preferences"]:
1339
+ continue
1340
+
1341
+ status, details = self._test_single(service_name, svc_config)
1342
+ display_name = "Discord" if service_name == "discord" else "Telegram" if service_name == "telegram" else service_name.title()
1343
+ table.add_row(display_name, status, details)
1344
+
1345
+ # Test Soul
1346
+ soul_hash = hashlib.sha256(
1347
+ json.dumps(self.soul.to_dict(), sort_keys=True).encode()
1348
+ ).hexdigest()[:16]
1349
+ table.add_row("Soul", "✅ VALID", f"Hash: {soul_hash}")
1350
+
1351
+ # Test Heartbeat
1352
+ hb_status = self.heartbeat.get_status()
1353
+ table.add_row(
1354
+ "Heartbeat",
1355
+ "✅ ACTIVE" if hb_status["running"] else "⭕ STOPPED",
1356
+ f"Interval: {hb_status['interval_minutes']}min"
1357
+ )
1358
+
1359
+ # Test Skills
1360
+ skill_stats = self._get_skill_stats()
1361
+ table.add_row(
1362
+ "Skills",
1363
+ "✅ LOADED",
1364
+ f"{skill_stats['total']} skills, {len(skill_stats['by_category'])} categories"
1365
+ )
1366
+
1367
+ console.print(table)
1368
+
1369
+ def _test_single(self, service_name: str, config: Dict[str, Any]) -> tuple[str, str]:
1370
+ """Test a single service connection."""
1371
+ try:
1372
+ if service_name == "lollms":
1373
+ settings = LollmsSettings(
1374
+ host_address=config.get("host_address", ""),
1375
+ api_key=config.get("api_key"),
1376
+ binding_name=config.get("binding_name"),
1377
+ model_name=config.get("model_name"),
1378
+ context_size=config.get("context_size", 4096),
1379
+ verify_ssl=config.get("verify_ssl", True),
1380
+ )
1381
+ client = build_lollms_client(settings)
1382
+
1383
+ if client:
1384
+ # Show model and binding info
1385
+ binding = config.get("binding_name", "unknown")
1386
+ model = config.get("model_name", "default")
1387
+ return ("✅ READY", f"{binding}/{model}")
1388
+ else:
1389
+ return ("❌ ERROR", "Client initialization failed")
1390
+
1391
+ elif service_name == "discord":
1392
+ token = config.get("bot_token", "")
1393
+ has_token = bool(token)
1394
+ allowed_users = config.get("allowed_users", [])
1395
+ return (
1396
+ "🔍 CONFIGURED" if has_token else "⭕ NO TOKEN",
1397
+ f"Token: {'✅' if has_token else '❌'}, Users: {len(allowed_users)}"
1398
+ )
1399
+
1400
+ elif service_name == "telegram":
1401
+ token = config.get("bot_token", "")
1402
+ has_token = bool(token)
1403
+ return (
1404
+ "🔍 CONFIGURED" if has_token else "⭕ NO TOKEN",
1405
+ f"Token: {'✅' if has_token else '❌'}"
1406
+ )
1407
+
1408
+ return ("❓ SKIP", "-")
1409
+
1410
+ except Exception as e:
1411
+ return ("❌ ERROR", str(e)[:40])
1412
+
1413
+ def show_full_config(self) -> None:
1414
+ """Display complete configuration."""
1415
+ console.print("\n[bold]📄 Full Configuration[/]")
1416
+
1417
+ # Backend with binding details
1418
+ if "lollms" in self.config:
1419
+ lollms = self.config["lollms"]
1420
+ console.print(Panel(
1421
+ json.dumps(lollms, indent=2),
1422
+ title=f"AI Backend: {lollms.get('binding_name', 'unknown')}",
1423
+ border_style="blue"
1424
+ ))
1425
+
1426
+ # Other services
1427
+ for svc in ["discord", "telegram"]:
1428
+ if svc in self.config:
1429
+ # Mask secrets
1430
+ safe_config = {k: (v if "token" not in k else "***") for k, v in self.config[svc].items()}
1431
+ console.print(Panel(
1432
+ json.dumps(safe_config, indent=2),
1433
+ title=svc.title(),
1434
+ border_style="blue"
1435
+ ))
1436
+
1437
+ # Soul
1438
+ console.print(Panel(
1439
+ json.dumps(self.soul.to_dict(), indent=2),
1440
+ title=f"Soul: {self.soul.name}",
1441
+ border_style="magenta"
1442
+ ))
1443
+
1444
+ # Heartbeat
1445
+ hb_status = self.heartbeat.get_status()
1446
+ console.print(Panel(
1447
+ json.dumps(hb_status, indent=2),
1448
+ title="Heartbeat Status",
1449
+ border_style="green"
1450
+ ))
1451
+
1452
+ # Skills
1453
+ skill_stats = self._get_skill_stats()
1454
+ console.print(Panel(
1455
+ json.dumps(skill_stats, indent=2),
1456
+ title=f"Skills Library ({skill_stats['total']} skills)",
1457
+ border_style="yellow"
1458
+ ))
1459
+
1460
+ def _save_all(self) -> None:
1461
+ """Save all configurations."""
1462
+ self.soul._save()
1463
+ self.heartbeat._save_config()
1464
+
1465
+ self.config["soul"] = {
1466
+ "name": self.soul.name,
1467
+ "version": self.soul.version,
1468
+ "trait_count": len(self.soul.traits),
1469
+ "value_count": len(self.soul.values),
1470
+ }
1471
+
1472
+ self.config["skills"] = {
1473
+ "total_loaded": len(self.skill_registry._skills),
1474
+ "categories": list(self.skill_registry._categories.keys()),
1475
+ }
1476
+
1477
+ self._save_config()
1478
+
1479
+
1480
+ # Entry point
1481
+ def run_wizard() -> None:
1482
+ """CLI entrypoint."""
1483
+ try:
1484
+ Wizard().run_wizard()
1485
+ except KeyboardInterrupt:
1486
+ console.print("\n[yellow]👋 Bye![/]")
1487
+ except Exception as e:
1488
+ console.print(f"[red]Error: {e}[/]")
1489
+ raise
1490
+
1491
+
1492
+ if __name__ == "__main__":
1493
+ run_wizard()