Files
roadpad/lucidia.py

641 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Lucidia - BlackRoad OS Native AI.
Lucidia is BlackRoad's sovereign AI interface. It routes to various
backends (Ollama, Copilot, external APIs) but the IDENTITY is Lucidia.
Architecture:
Lucidia (this) = The mind, personality, router
Backends = Pluggable inference engines (local-first)
Priority order:
1. Local Ollama (cecilia/lucidia Pi with Hailo-8)
2. Local Ollama (any available node)
3. GitHub Copilot (if authenticated)
4. External API fallback
Lucidia is NOT a wrapper around Claude. Lucidia IS the AI.
"""
import os
import sys
import subprocess
import threading
import json
from datetime import datetime
from dataclasses import dataclass
from typing import Callable, List, Dict, Any, Tuple
# Security module (sovereign protection)
try:
from security import SecurityContext, verify_backend_host
SECURITY_ENABLED = True
except ImportError:
SECURITY_ENABLED = False
SecurityContext = None
# ═══════════════════════════════════════════════════════════════════════════════
# LUCIDIA BRANDING
# ═══════════════════════════════════════════════════════════════════════════════
LOGO_LARGE = """\
██╗ ██╗ ██╗ ██████╗██╗██████╗ ██╗ █████╗
██║ ██║ ██║██╔════╝██║██╔══██╗██║██╔══██╗
██║ ██║ ██║██║ ██║██║ ██║██║███████║
██║ ██║ ██║██║ ██║██║ ██║██║██╔══██║
███████╗╚██████╔╝╚██████╗██║██████╔╝██║██║ ██║
╚══════╝ ╚═════╝ ╚═════╝╚═╝╚═════╝ ╚═╝╚═╝ ╚═╝"""
ROBOT_FACE = """\
>─╮
▣═▣
● ●"""
ROBOT_FULL = """\
╭─╮ ╭─╮
╰─╯ ╰─╯
▒▔▔▒
╭─────╮
│ ░░░ │
╰─┬─┬─╯
│ │
| ╲"""
ROBOT_MINI = """\
>─╮
▣═▣
- - -
● ●"""
LAYERS_BOOT = """\
+ Layer 3 (agents/system) loaded
+ Layer 4 (deploy/orchestration) loaded
+ Layer 5 (branches/environments) loaded
+ Layer 6 (Lucidia core/memory) loaded
+ Layer 7 (orchestration) loaded
+ Layer 8 (network/API) loaded"""
# Box drawing
def box(content: List[str], width: int = 80, title: str = "") -> List[str]:
"""Draw a box around content."""
lines = []
inner = width - 2
# Top
if title:
pad = inner - len(title) - 2
lines.append(f"╭─ {title} " + "" * pad + "")
else:
lines.append("" + "" * inner + "")
# Content
for line in content:
if len(line) > inner:
line = line[:inner-1] + ""
lines.append("" + line + " " * (inner - len(line)) + "")
# Bottom
lines.append("" + "" * inner + "")
return lines
def header_box(version: str = "0.1.0", model: str = "Lucidia", directory: str = "~") -> List[str]:
"""Create the main header box."""
content = [
f" >_ Road Code (v{version})",
"",
f" model: {model} /model to change",
f" directory: {directory}",
]
return box(content, width=56)
def welcome_screen(last_login: str = None) -> str:
"""Generate full welcome screen."""
if not last_login:
last_login = datetime.now().strftime("%b %d %Y %H:%M")
lines = [
"",
LOGO_LARGE,
"",
" BlackRoad OS, Inc. | AI-Native",
"",
]
# Robot with info
lines.extend([
"" + "" * 46 + "",
"│ │",
"│ >─╮ BlackRoad OS, Inc. │",
"│ ▣═▣ │",
f"│ ● ● Last Login: {last_login:<20}",
"│ │",
"" + "" * 46 + "",
"",
])
# Layer boot
lines.append(" ✓ BlackRoad CLI v3 → br-help")
lines.extend(LAYERS_BOOT.split("\n"))
lines.append("")
return "\n".join(lines)
def prompt_box(model: str = "Lucidia", version: str = "0.1.0", cwd: str = "~") -> str:
"""Generate the prompt area."""
lines = [
"" + "" * 94 + "",
"" + " " * 94 + "",
"│ BlackRoad OS, Inc. | AI-Native" + " " * 60 + "",
"│ ▣═▣ Lucidia by BlackRoad OS, Inc." + " " * 56 + "",
"│ ╰─ Describe a task to get started." + " " * 52 + "",
]
# Inner box
inner = [
f"│ ╭{'' * 52}" + " " * 38 + "",
f"│ │ >_ Road Code (v{version})" + " " * (52 - 26 - len(version)) + "" + " " * 38 + "",
"│ │" + " " * 52 + "" + " " * 38 + "",
f"│ │ model: {model}" + " " * (52 - 20 - len(model)) + "" + " " * 38 + "",
f"│ │ directory: {cwd}" + " " * (52 - 22 - len(cwd)) + "" + " " * 38 + "",
f"│ ╰{'' * 52}" + " " * 38 + "",
]
lines.extend(inner)
lines.extend([
"" + " " * 94 + "",
"│ Pick a model with /model. Copilot uses AI, so always check for mistakes." + " " * 19 + "",
"" + "" * 94 + "",
])
return "\n".join(lines)
# ═══════════════════════════════════════════════════════════════════════════════
# BACKEND DEFINITIONS (Pluggable inference engines)
# ═══════════════════════════════════════════════════════════════════════════════
@dataclass
class Backend:
"""A pluggable AI backend. Lucidia routes to these."""
name: str
id: str
type: str # ollama, copilot, api
description: str
command: List[str]
priority: int = 10 # Lower = preferred (local-first)
requires_network: bool = False
def is_available(self) -> bool:
"""Check if this backend is currently available."""
import shutil
if not self.command:
return False
return shutil.which(self.command[0]) is not None
# Backends ordered by preference (local-first, sovereign-first)
BACKENDS = {
# === LOCAL INFERENCE (Priority 1-3) ===
"cecilia": Backend(
name="Cecilia",
id="cecilia",
type="ollama",
description="Hailo-8 edge AI (26 TOPS)",
command=["ssh", "cecilia", "ollama", "run", "llama3.2"],
priority=1,
requires_network=False # Local network
),
"lucidia-node": Backend(
name="Lucidia Node",
id="lucidia-pi",
type="ollama",
description="Pi 5 local inference",
command=["ssh", "lucidia", "ollama", "run", "llama3.2"],
priority=2,
requires_network=False
),
"ollama": Backend(
name="Ollama Local",
id="ollama",
type="ollama",
description="Local LLM on this machine",
command=["ollama", "run", "llama3.2"],
priority=3,
requires_network=False
),
# === AUTHENTICATED SERVICES (Priority 5) ===
"copilot": Backend(
name="GitHub Copilot",
id="copilot",
type="copilot",
description="GitHub AI (requires auth)",
command=["gh", "copilot", "suggest", "-t", "shell"],
priority=5,
requires_network=True
),
# === EXTERNAL APIs (Priority 10 - fallback only) ===
"anthropic": Backend(
name="Anthropic API",
id="anthropic",
type="api",
description="External API fallback",
command=["curl", "-X", "POST", "https://api.anthropic.com/v1/messages"],
priority=10,
requires_network=True
),
}
# Legacy alias for compatibility
MODELS = BACKENDS
def get_best_backend() -> Backend:
"""Get the best available backend (local-first)."""
import subprocess
# Sort by priority
sorted_backends = sorted(BACKENDS.values(), key=lambda b: b.priority)
for backend in sorted_backends:
# Skip network-required backends if we prefer local
if backend.type == "ollama":
# Check if Ollama is reachable
try:
if "ssh" in backend.command:
# Remote Ollama - quick ping check
host = backend.command[1]
result = subprocess.run(
["ssh", "-o", "ConnectTimeout=2", "-o", "BatchMode=yes", host, "echo", "ok"],
capture_output=True, timeout=3
)
if result.returncode == 0:
return backend
else:
# Local Ollama
result = subprocess.run(["ollama", "list"], capture_output=True, timeout=2)
if result.returncode == 0:
return backend
except:
continue
elif backend.type == "copilot":
try:
result = subprocess.run(["gh", "auth", "status"], capture_output=True, timeout=2)
if result.returncode == 0:
return backend
except:
continue
# Fallback to first available
return sorted_backends[0] if sorted_backends else None
# ═══════════════════════════════════════════════════════════════════════════════
# BACKEND RUNNER (Generic inference engine)
# ═══════════════════════════════════════════════════════════════════════════════
class BackendRunner:
"""Runs any AI backend as subprocess. Lucidia routes here."""
def __init__(self, backend: str = None, security: 'SecurityContext' = None):
if backend and backend in BACKENDS:
self.backend = BACKENDS[backend]
else:
# Auto-select best available backend
self.backend = get_best_backend()
if not self.backend:
self.backend = BACKENDS.get("ollama")
self.process: subprocess.Popen = None
self.running = False
self.output_buffer: List[str] = []
self.on_output: Callable[[str], None] = None
self.cwd = os.getcwd()
# Security context
self.security = security
if SECURITY_ENABLED and not self.security:
self.security = SecurityContext()
def start(self) -> bool:
"""Start backend subprocess with security verification."""
try:
cmd = self.backend.command.copy()
# Security: Verify backend is trusted
if self.security:
allowed, reason = self.security.check_backend(cmd)
if not allowed:
print(f" ✗ Security blocked: {reason}")
return False
self.security.audit.log("backend_start", self.backend.name)
self.process = subprocess.Popen(
cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
cwd=self.cwd
)
self.running = True
# Start reader thread
self._reader = threading.Thread(target=self._read_output, daemon=True)
self._reader.start()
return True
except Exception as e:
print(f" ✗ Backend failed: {e}")
if self.security:
self.security.audit.log_security("backend_error", {"error": str(e)})
return False
def _read_output(self):
"""Read output from backend with security filtering."""
while self.running and self.process:
try:
line = self.process.stdout.readline()
if line:
output = line.rstrip()
# Security: Filter output for secrets
if self.security:
output, warnings = self.security.filter_output(output)
for w in warnings:
print(f"{w}")
self.output_buffer.append(output)
if self.on_output:
self.on_output(output)
elif self.process.poll() is not None:
self.running = False
break
except:
break
def send(self, text: str) -> Tuple[bool, str]:
"""Send input to backend with security checks."""
if not self.process or not self.process.stdin:
return False, "backend not running"
# Security: Check and sanitize input
if self.security:
allowed, sanitized, warning = self.security.check_input(text)
if not allowed:
return False, warning
if warning:
print(f"{warning}")
text = sanitized
self.security.audit.log_input(self.backend.name, text)
try:
self.process.stdin.write(text + "\n")
self.process.stdin.flush()
return True, ""
except Exception as e:
return False, str(e)
def stop(self) -> None:
"""Stop backend subprocess."""
if self.security:
self.security.audit.log("backend_stop", self.backend.name if self.backend else "unknown")
self.running = False
if self.process:
self.process.terminate()
try:
self.process.wait(timeout=2)
except:
self.process.kill()
def get_output(self) -> List[str]:
"""Get and clear output buffer."""
output = self.output_buffer.copy()
self.output_buffer.clear()
return output
def get_security_status(self) -> Dict:
"""Get security status for this backend."""
if self.security:
return self.security.get_status()
return {"security": "disabled"}
# Legacy alias
ClaudeWrapper = BackendRunner
# ═══════════════════════════════════════════════════════════════════════════════
# LUCIDIA SHELL (The sovereign AI interface)
# ═══════════════════════════════════════════════════════════════════════════════
class LucidiaShell:
"""
Lucidia - BlackRoad OS Native AI Shell.
This is THE interface. Backends are pluggable inference engines.
Lucidia has her own personality, routes intelligently, prefers local.
"""
def __init__(self):
self.backend = None
self.backend_name = None # Will auto-select
self.running = False
self.history: List[str] = []
self.cwd = os.getcwd()
def boot(self) -> None:
"""Display boot sequence."""
print(welcome_screen())
def select_backend(self) -> str:
"""Interactive backend selector."""
print("\n Select Backend")
print(" " + "" * 60)
backends = list(BACKENDS.items())
for i, (key, backend) in enumerate(backends):
marker = "" if key == self.backend_name else " "
status = "" if backend.priority <= 3 else "" # Local vs remote
print(f" {marker} {i+1}. {status} {backend.name:20} {backend.description}")
print("\n ● = Local (sovereign) ○ = Remote")
print(" Press number to select or Enter to auto-select")
try:
choice = input(" > ").strip()
if choice.isdigit():
idx = int(choice) - 1
if 0 <= idx < len(backends):
self.backend_name = backends[idx][0]
print(f"\n ✓ Selected: {BACKENDS[self.backend_name].name}")
except:
pass
return self.backend_name
# Legacy alias
select_model = select_backend
def show_prompt(self) -> None:
"""Show the prompt UI."""
home = os.path.expanduser("~")
cwd = self.cwd.replace(home, "~")
# Show Lucidia as the identity, backend in parentheses
backend_info = ""
if self.backend and self.backend.backend:
backend_info = f" ({self.backend.backend.name})"
print(f"\n >─╮ Lucidia{backend_info}")
print(f" ▣═▣ {cwd}")
print(f" ● ●")
def handle_command(self, cmd: str) -> bool:
"""Handle slash commands. Returns True if handled."""
if cmd in ("/model", "/backend"):
self.select_backend()
return True
elif cmd == "/new":
self.history.clear()
print("\n ✓ Started new session")
return True
elif cmd == "/help":
print("\n Lucidia Commands:")
print(" /backend - Select inference backend")
print(" /new - New session")
print(" /quit - Exit")
print(" /layers - Show loaded layers")
print(" /status - Show backend status")
print(" /security - Show security status")
return True
elif cmd == "/layers":
print(LAYERS_BOOT)
return True
elif cmd == "/status":
if self.backend and self.backend.backend:
b = self.backend.backend
print(f"\n Backend: {b.name}")
print(f" Type: {b.type}")
print(f" Local: {'Yes' if b.priority <= 3 else 'No'}")
else:
print("\n No backend connected")
return True
elif cmd == "/security":
if self.backend:
status = self.backend.get_security_status()
print("\n Security Status:")
if "session_stats" in status:
stats = status["session_stats"]
print(f" Session: {stats.get('session_id', 'unknown')}")
print(f" Events: {stats.get('total_events', 0)}")
print(f" Inputs: {stats.get('inputs', 0)}")
print(f" Outputs: {stats.get('outputs', 0)}")
print(f" Rate limit: {status.get('rate_limit_remaining', 'N/A')} remaining")
print(f" Warnings: {status.get('warnings', 0)}")
print(f" Blocked: {status.get('blocked', 0)}")
else:
print("\n Security: disabled (no backend)")
return True
elif cmd in ("/quit", "/exit", "/q"):
self.running = False
return True
return False
def run(self) -> None:
"""Run the Lucidia shell."""
self.boot()
self.running = True
# Auto-select best backend (local-first)
print("\n ◐ Detecting backends...")
self.backend = BackendRunner(self.backend_name)
if self.backend.backend:
locality = "local" if self.backend.backend.priority <= 3 else "remote"
print(f" ✓ Using {self.backend.backend.name} ({locality})")
else:
print(" ⚠ No backend available - running in offline mode")
# Security status
if SECURITY_ENABLED:
print(" ✓ Security: enabled (audit, sanitize, filter)")
else:
print(" ⚠ Security: module not loaded")
if not self.backend.start():
print(" ✗ Failed to start backend")
print(" → Try: ollama serve (start local inference)")
return
print("\n ✓ Lucidia ready")
while self.running:
self.show_prompt()
try:
user_input = input("\n ").strip()
except (EOFError, KeyboardInterrupt):
print("\n")
break
if not user_input:
continue
# Check for commands
if user_input.startswith("/"):
if self.handle_command(user_input):
continue
# Send to backend (with security checks)
self.history.append(user_input)
success, error = self.backend.send(user_input)
if not success:
print(f"\n ✗ Blocked: {error}")
continue
# Lucidia thinking indicator
print("\n ▣═▣ ···")
# Wait for and display response
import time
time.sleep(0.5)
while True:
output = self.backend.get_output()
if output:
for line in output:
# Filter UI noise from various backends
if not any(skip in line for skip in ["", "", "", "thinking", ">"]):
print(f" {line}")
if not self.backend.running:
break
time.sleep(0.1)
if not self.backend.output_buffer:
break
self.backend.stop()
print("\n ╰─ Goodbye from Lucidia\n")
# ═══════════════════════════════════════════════════════════════════════════════
# ENTRY POINT
# ═══════════════════════════════════════════════════════════════════════════════
def main():
"""Main entry point."""
shell = LucidiaShell()
shell.run()
if __name__ == "__main__":
main()