2026-02-24 20:55:29 -06:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""
|
|
|
|
|
TrueRecall v2 - Interactive Install Script
|
|
|
|
|
|
|
|
|
|
Prompts for configuration and sets up the system.
|
|
|
|
|
Simple inline format with go-back navigation.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
import os
|
|
|
|
|
import sys
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
# Configuration state
|
|
|
|
|
config = {
|
|
|
|
|
"mode": None,
|
|
|
|
|
"embedding_model": "mxbai-embed-large",
|
|
|
|
|
"timer_minutes": 5,
|
|
|
|
|
"batch_size": 100,
|
|
|
|
|
"qdrant_url": "http://localhost:6333",
|
|
|
|
|
"ollama_url": "http://localhost:11434",
|
|
|
|
|
"curator_model": "qwen3:30b-a3b-instruct",
|
|
|
|
|
"user_id": "rob",
|
|
|
|
|
"source_collection": "memories_tr",
|
|
|
|
|
"target_collection": "gems_tr"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Track position for go-back
|
|
|
|
|
history = []
|
|
|
|
|
|
|
|
|
|
def prompt_inline(question, options, default_key, allow_custom=False):
|
|
|
|
|
"""
|
|
|
|
|
Show inline prompt with 0. go back option.
|
|
|
|
|
|
|
|
|
|
options: dict like {"1": ("value", "description"), ...}
|
|
|
|
|
Returns: (value, go_back_flag)
|
|
|
|
|
"""
|
|
|
|
|
print(f"\n{question} [{default_key}]")
|
|
|
|
|
print(" 0. go back (return to previous question)")
|
|
|
|
|
for key, (value, desc) in options.items():
|
|
|
|
|
marker = " [default]" if key == default_key else ""
|
|
|
|
|
print(f" {key}. {value:<25} ({desc}){marker}")
|
|
|
|
|
if allow_custom:
|
|
|
|
|
print(f" {len(options)+1}. custom (enter your own value)")
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
choice = input(f"\nSelect [0-{len(options) + (1 if allow_custom else 0)} or type name]: ").strip()
|
|
|
|
|
|
|
|
|
|
# Go back
|
|
|
|
|
if choice == "0":
|
|
|
|
|
return None, True
|
|
|
|
|
|
|
|
|
|
# Empty = default
|
|
|
|
|
if choice == "":
|
|
|
|
|
return options[default_key][0], False
|
|
|
|
|
|
|
|
|
|
# Number selection
|
|
|
|
|
if choice in options:
|
|
|
|
|
return options[choice][0], False
|
|
|
|
|
|
|
|
|
|
# Custom
|
|
|
|
|
if allow_custom and choice == str(len(options) + 1):
|
|
|
|
|
custom = input("Enter custom value: ").strip()
|
|
|
|
|
return custom if custom else options[default_key][0], False
|
|
|
|
|
|
|
|
|
|
# Direct name entry
|
|
|
|
|
for key, (value, desc) in options.items():
|
|
|
|
|
if choice.lower() == value.lower():
|
|
|
|
|
return value, False
|
|
|
|
|
|
|
|
|
|
print("Invalid choice. Try again.")
|
|
|
|
|
|
|
|
|
|
def prompt_custom(question, default):
|
|
|
|
|
"""Prompt for custom text input with go-back option."""
|
|
|
|
|
print(f"\n{question}")
|
|
|
|
|
print(" 0. go back (return to previous question)")
|
|
|
|
|
print(f" 1. use default ({default})")
|
|
|
|
|
print(" 2. custom (enter your own value)")
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
choice = input("\nSelect [0-2]: ").strip()
|
|
|
|
|
|
|
|
|
|
if choice == "0":
|
|
|
|
|
return None, True
|
|
|
|
|
if choice == "1" or choice == "":
|
|
|
|
|
return default, False
|
|
|
|
|
if choice == "2":
|
|
|
|
|
custom = input("Enter value: ").strip()
|
|
|
|
|
return custom if custom else default, False
|
|
|
|
|
|
|
|
|
|
print("Invalid choice. Try again.")
|
|
|
|
|
|
|
|
|
|
def install_full():
|
|
|
|
|
"""Full installation prompts."""
|
|
|
|
|
prompts = [
|
|
|
|
|
# (key, question, options, default_key, allow_custom)
|
|
|
|
|
("embedding_model", "Embedding model", {
|
|
|
|
|
"1": ("snowflake-arctic-embed2", "fast, MTEB ~60, high-volume"),
|
|
|
|
|
"2": ("mxbai-embed-large", "accurate, MTEB 66.5, recommended")
|
|
|
|
|
}, "2", True),
|
|
|
|
|
|
|
|
|
|
("timer_minutes", "Timer interval", {
|
|
|
|
|
"1": ("5", "minutes, fast backlog clearing"),
|
|
|
|
|
"2": ("30", "minutes, balanced, recommended"),
|
|
|
|
|
"3": ("60", "minutes, minimal overhead")
|
|
|
|
|
}, "2", True),
|
|
|
|
|
|
|
|
|
|
("batch_size", "Batch size", {
|
|
|
|
|
"1": ("50", "conservative, less memory"),
|
|
|
|
|
"2": ("100", "balanced, recommended"),
|
|
|
|
|
"3": ("500", "aggressive, faster backlog")
|
|
|
|
|
}, "2", True),
|
|
|
|
|
|
|
|
|
|
("qdrant_url", "Qdrant URL", {
|
|
|
|
|
"1": ("http://localhost:6333", "localhost hostname"),
|
|
|
|
|
"2": ("http://127.0.0.1:6333", "localhost IP")
|
|
|
|
|
}, "1", True),
|
|
|
|
|
|
|
|
|
|
("ollama_url", "Ollama URL", {
|
|
|
|
|
"1": ("http://localhost:11434", "localhost hostname"),
|
|
|
|
|
"2": ("http://127.0.0.1:11434", "localhost IP")
|
|
|
|
|
}, "1", True),
|
|
|
|
|
|
|
|
|
|
("curator_model", "Curator LLM", {
|
|
|
|
|
"1": ("qwen3:4b-instruct", "fast ~10s, basic quality"),
|
|
|
|
|
"2": ("qwen3:30b-a3b-instruct", "quality ~3s, recommended"),
|
|
|
|
|
"3": ("qwen3:30b-a3b-instruct-2507-q8_0", "quality, specific version")
|
|
|
|
|
}, "2", True),
|
|
|
|
|
|
|
|
|
|
("user_id", "User ID", {
|
|
|
|
|
"1": ("rob", "default")
|
|
|
|
|
}, "1", True),
|
|
|
|
|
]
|
|
|
|
|
|
2026-02-24 20:59:59 -06:00
|
|
|
# Plugin config prompts (full mode only)
|
|
|
|
|
plugin_prompts = [
|
|
|
|
|
("max_recall_results", "Max gems per turn (injection)", {
|
|
|
|
|
"1": ("2", "conservative, less context bloat"),
|
|
|
|
|
"2": ("5", "balanced, recommended"),
|
|
|
|
|
"3": ("10", "more context, higher recall")
|
|
|
|
|
}, "2", True),
|
|
|
|
|
|
|
|
|
|
("min_recall_score", "Minimum similarity score", {
|
|
|
|
|
"1": ("0.5", "loose matching, more gems"),
|
|
|
|
|
"2": ("0.7", "balanced, recommended"),
|
|
|
|
|
"3": ("0.9", "strict matching, only strong matches")
|
|
|
|
|
}, "2", True),
|
|
|
|
|
]
|
|
|
|
|
|
2026-02-24 20:55:29 -06:00
|
|
|
idx = 0
|
|
|
|
|
while idx < len(prompts):
|
|
|
|
|
key, question, options, default_key, allow_custom = prompts[idx]
|
|
|
|
|
|
|
|
|
|
if key in ["qdrant_url", "ollama_url", "user_id"] and allow_custom:
|
|
|
|
|
# These use custom prompt
|
|
|
|
|
value, go_back = prompt_custom(question, options[default_key][0])
|
|
|
|
|
else:
|
|
|
|
|
value, go_back = prompt_inline(question, options, default_key, allow_custom)
|
|
|
|
|
|
|
|
|
|
if go_back and idx > 0:
|
|
|
|
|
idx -= 1
|
|
|
|
|
continue
|
|
|
|
|
if go_back and idx == 0:
|
|
|
|
|
return False # Go back to mode selection
|
|
|
|
|
|
|
|
|
|
config[key] = value
|
|
|
|
|
idx += 1
|
|
|
|
|
|
2026-02-24 20:59:59 -06:00
|
|
|
# Process plugin prompts
|
|
|
|
|
idx = 0
|
|
|
|
|
while idx < len(plugin_prompts):
|
|
|
|
|
key, question, options, default_key, allow_custom = plugin_prompts[idx]
|
|
|
|
|
value, go_back = prompt_inline(question, options, default_key, allow_custom)
|
|
|
|
|
|
|
|
|
|
if go_back:
|
|
|
|
|
if idx > 0:
|
|
|
|
|
idx -= 1
|
|
|
|
|
continue
|
|
|
|
|
else:
|
|
|
|
|
# Go back to main prompts
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
config[key] = value
|
|
|
|
|
idx += 1
|
|
|
|
|
|
2026-02-24 20:55:29 -06:00
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
def install_simple():
|
|
|
|
|
"""Simple installation prompts (watcher only)."""
|
|
|
|
|
prompts = [
|
|
|
|
|
("embedding_model", "Embedding model", {
|
|
|
|
|
"1": ("snowflake-arctic-embed2", "fast, MTEB ~60, high-volume"),
|
|
|
|
|
"2": ("mxbai-embed-large", "accurate, MTEB 66.5, recommended")
|
|
|
|
|
}, "1", True),
|
|
|
|
|
|
|
|
|
|
("qdrant_url", "Qdrant URL", {
|
|
|
|
|
"1": ("http://localhost:6333", "localhost hostname"),
|
|
|
|
|
"2": ("http://127.0.0.1:6333", "localhost IP")
|
|
|
|
|
}, "1", True),
|
|
|
|
|
|
|
|
|
|
("user_id", "User ID", {
|
|
|
|
|
"1": ("rob", "default")
|
|
|
|
|
}, "1", True),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
idx = 0
|
|
|
|
|
while idx < len(prompts):
|
|
|
|
|
key, question, options, default_key, allow_custom = prompts[idx]
|
|
|
|
|
|
|
|
|
|
if allow_custom:
|
|
|
|
|
value, go_back = prompt_custom(question, options[default_key][0])
|
|
|
|
|
else:
|
|
|
|
|
value, go_back = prompt_inline(question, options, default_key, allow_custom)
|
|
|
|
|
|
|
|
|
|
if go_back and idx > 0:
|
|
|
|
|
idx -= 1
|
|
|
|
|
continue
|
|
|
|
|
if go_back and idx == 0:
|
|
|
|
|
return False # Go back to mode selection
|
|
|
|
|
|
|
|
|
|
config[key] = value
|
|
|
|
|
idx += 1
|
|
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
def write_config():
|
|
|
|
|
"""Write configuration files."""
|
|
|
|
|
script_dir = Path(__file__).parent
|
|
|
|
|
config_path = script_dir / "tr-continuous" / "curator_config.json"
|
|
|
|
|
|
|
|
|
|
# Ensure directory exists
|
|
|
|
|
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
output = {
|
|
|
|
|
"timer_minutes": int(config["timer_minutes"]),
|
|
|
|
|
"batch_size": int(config["batch_size"]),
|
|
|
|
|
"user_id": config["user_id"],
|
|
|
|
|
"source_collection": config["source_collection"],
|
|
|
|
|
"target_collection": config["target_collection"],
|
|
|
|
|
"embedding_model": config["embedding_model"],
|
|
|
|
|
"curator_model": config["curator_model"],
|
|
|
|
|
"qdrant_url": config["qdrant_url"],
|
|
|
|
|
"ollama_url": config["ollama_url"]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Remove curator settings for simple mode
|
|
|
|
|
if config["mode"] == "simple":
|
|
|
|
|
output.pop("timer_minutes", None)
|
|
|
|
|
output.pop("batch_size", None)
|
|
|
|
|
output.pop("curator_model", None)
|
|
|
|
|
output.pop("ollama_url", None)
|
|
|
|
|
output["target_collection"] = None # No gems in simple mode
|
|
|
|
|
|
|
|
|
|
with open(config_path, 'w') as f:
|
|
|
|
|
json.dump(output, f, indent=2)
|
|
|
|
|
|
2026-02-24 20:59:59 -06:00
|
|
|
# Write plugin config for full mode
|
|
|
|
|
if config["mode"] == "full":
|
|
|
|
|
plugin_config = {
|
|
|
|
|
"collectionName": config["target_collection"],
|
|
|
|
|
"captureCollection": config["source_collection"],
|
|
|
|
|
"maxRecallResults": int(config.get("max_recall_results", 5)),
|
|
|
|
|
"minRecallScore": float(config.get("min_recall_score", 0.7)),
|
|
|
|
|
"qdrantUrl": config["qdrant_url"]
|
|
|
|
|
}
|
|
|
|
|
plugin_path = script_dir / "plugin-config.json"
|
|
|
|
|
with open(plugin_path, 'w') as f:
|
|
|
|
|
json.dump(plugin_config, f, indent=2)
|
|
|
|
|
|
2026-02-24 20:55:29 -06:00
|
|
|
return config_path
|
|
|
|
|
|
2026-02-24 21:20:37 -06:00
|
|
|
def detect_v1():
|
|
|
|
|
"""Detect if TrueRecall v1 is installed and warn user."""
|
|
|
|
|
v1_indicators = []
|
|
|
|
|
|
|
|
|
|
# Check for v1 folder
|
|
|
|
|
v1_paths = [
|
|
|
|
|
Path.home() / ".openclaw" / "workspace" / ".projects" / "true-recall",
|
|
|
|
|
Path.home() / ".openclaw" / "workspace" / ".projects" / "true-recall" / "tr-daily",
|
|
|
|
|
]
|
|
|
|
|
for p in v1_paths:
|
|
|
|
|
if p.exists():
|
|
|
|
|
v1_indicators.append(f"Found v1 folder: {p}")
|
|
|
|
|
|
|
|
|
|
# Check for v1 cron jobs
|
|
|
|
|
cron_file = Path.home() / ".openclaw" / "tr-daily" / "crontab.txt"
|
|
|
|
|
if cron_file.exists():
|
|
|
|
|
v1_indicators.append(f"Found v1 cron file: {cron_file}")
|
|
|
|
|
|
|
|
|
|
# Check for v1 systemd service
|
|
|
|
|
v1_service_paths = [
|
|
|
|
|
Path("/etc/systemd/system/memories-qdrant.service"),
|
|
|
|
|
Path.home() / ".config" / "systemd" / "user" / "memories-qdrant.service",
|
|
|
|
|
]
|
|
|
|
|
for s in v1_service_paths:
|
|
|
|
|
if s.exists():
|
|
|
|
|
v1_indicators.append(f"Found v1 service: {s}")
|
|
|
|
|
|
|
|
|
|
if not v1_indicators:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
print("\n" + "=" * 60)
|
|
|
|
|
print("⚠️ TRUE-RECALL V1 DETECTED")
|
|
|
|
|
print("=" * 60)
|
|
|
|
|
print("\nTrueRecall v1 is already installed on this system:")
|
|
|
|
|
for indicator in v1_indicators:
|
|
|
|
|
print(f" • {indicator}")
|
|
|
|
|
|
|
|
|
|
print("\n⚠️ UPGRADE NOT SUPPORTED")
|
|
|
|
|
print("-" * 60)
|
|
|
|
|
print("TrueRecall v2 cannot upgrade from v1 automatically.")
|
|
|
|
|
print("You MUST remove v1 completely before installing v2.")
|
|
|
|
|
print("\nTo remove v1:")
|
|
|
|
|
print(" 1. Stop v1 services: sudo systemctl stop memories-qdrant")
|
|
|
|
|
print(" 2. Remove v1 cron jobs from crontab")
|
|
|
|
|
print(" 3. Archive or delete v1 folder: ~/.openclaw/workspace/.projects/true-recall/")
|
|
|
|
|
print(" 4. v1 data in Qdrant 'memories' collection will remain (separate from v2)")
|
|
|
|
|
|
|
|
|
|
print("\n" + "-" * 60)
|
|
|
|
|
print("If you continue WITHOUT removing v1:")
|
|
|
|
|
print(" • Both v1 and v2 may run simultaneously")
|
|
|
|
|
print(" • Duplicate cron jobs may cause conflicts")
|
|
|
|
|
print(" • System resource usage will be doubled")
|
|
|
|
|
print(" • Data integrity is NOT guaranteed")
|
|
|
|
|
print("=" * 60)
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
choice = input("\nContinue installation at your own risk? [y/N]: ").strip().lower()
|
|
|
|
|
if choice in ("y", "yes"):
|
|
|
|
|
print("\n⚠️ Proceeding with installation — v1 conflicts possible.")
|
|
|
|
|
return True
|
|
|
|
|
elif choice in ("n", "no", ""):
|
|
|
|
|
print("\n❌ Installation cancelled.")
|
|
|
|
|
print("Please remove v1 first, then run this installer again.")
|
|
|
|
|
return False
|
|
|
|
|
else:
|
|
|
|
|
print("Invalid choice. Enter 'y' to continue or 'n' to abort.")
|
|
|
|
|
|
2026-02-24 20:55:29 -06:00
|
|
|
def show_summary():
|
|
|
|
|
"""Display configuration summary."""
|
|
|
|
|
print("\n" + "=" * 60)
|
|
|
|
|
print("Configuration Summary")
|
|
|
|
|
print("=" * 60)
|
|
|
|
|
print(f"Mode: {config['mode']}")
|
|
|
|
|
print(f"Embedding model: {config['embedding_model']}")
|
|
|
|
|
print(f"Qdrant URL: {config['qdrant_url']}")
|
|
|
|
|
print(f"User ID: {config['user_id']}")
|
|
|
|
|
print(f"Source collection: {config['source_collection']}")
|
|
|
|
|
|
|
|
|
|
if config["mode"] == "full":
|
|
|
|
|
print(f"Timer interval: {config['timer_minutes']} minutes")
|
|
|
|
|
print(f"Batch size: {config['batch_size']}")
|
|
|
|
|
print(f"Ollama URL: {config['ollama_url']}")
|
|
|
|
|
print(f"Curator LLM: {config['curator_model']}")
|
|
|
|
|
print(f"Target collection: {config['target_collection']}")
|
2026-02-24 20:59:59 -06:00
|
|
|
print(f"\nPlugin settings:")
|
|
|
|
|
print(f" Max gems per turn: {config.get('max_recall_results', 5)}")
|
|
|
|
|
print(f" Min similarity score: {config.get('min_recall_score', 0.7)}")
|
2026-02-24 20:55:29 -06:00
|
|
|
else:
|
|
|
|
|
print("Target collection: (none - simple mode)")
|
2026-02-24 20:59:59 -06:00
|
|
|
print("Plugin injection: (disabled - simple mode)")
|
2026-02-24 20:55:29 -06:00
|
|
|
|
|
|
|
|
print("=" * 60)
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
print("=" * 60)
|
|
|
|
|
print("TrueRecall v2 - Interactive Installation")
|
|
|
|
|
print("=" * 60)
|
|
|
|
|
|
2026-02-24 21:20:37 -06:00
|
|
|
# Check for v1
|
|
|
|
|
if not detect_v1():
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
2026-02-24 20:55:29 -06:00
|
|
|
# Mode selection
|
|
|
|
|
while True:
|
|
|
|
|
print("\nSelect installation mode:")
|
|
|
|
|
print(" 0. exit (cancel installation)")
|
|
|
|
|
print(" 1. full (watcher + curator + gems)")
|
|
|
|
|
print(" 2. simple (watcher only - memories)")
|
|
|
|
|
|
|
|
|
|
choice = input("\nSelect [0-2]: ").strip()
|
|
|
|
|
|
|
|
|
|
if choice == "0":
|
|
|
|
|
print("\nInstallation cancelled.")
|
|
|
|
|
return
|
|
|
|
|
if choice == "1":
|
|
|
|
|
config["mode"] = "full"
|
|
|
|
|
if install_full():
|
|
|
|
|
break
|
|
|
|
|
elif choice == "2":
|
|
|
|
|
config["mode"] = "simple"
|
|
|
|
|
if install_simple():
|
|
|
|
|
break
|
|
|
|
|
else:
|
|
|
|
|
print("Invalid choice. Try again.")
|
|
|
|
|
|
|
|
|
|
# Show summary
|
|
|
|
|
show_summary()
|
|
|
|
|
|
|
|
|
|
# Confirm
|
|
|
|
|
confirm = input("\nApply this configuration? [Y/n]: ").strip().lower()
|
|
|
|
|
if confirm and confirm not in ("y", "yes"):
|
|
|
|
|
print("\nInstallation cancelled.")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Write config
|
|
|
|
|
config_path = write_config()
|
|
|
|
|
print(f"\n✅ Configuration written to: {config_path}")
|
|
|
|
|
|
|
|
|
|
# Next steps
|
|
|
|
|
print("\n" + "=" * 60)
|
|
|
|
|
print("Next Steps")
|
|
|
|
|
print("=" * 60)
|
|
|
|
|
|
|
|
|
|
if config["mode"] == "full":
|
|
|
|
|
cron_expr = f"*/{config['timer_minutes']} * * * *"
|
|
|
|
|
print(f"1. Add to crontab:")
|
|
|
|
|
print(f" {cron_expr} cd {config_path.parent} && python3 curator_timer.py >> /var/log/true-recall-timer.log 2>&1")
|
|
|
|
|
print(f"\n2. Start the watcher:")
|
|
|
|
|
print(f" sudo systemctl start mem-qdrant-watcher")
|
|
|
|
|
print(f" sudo systemctl enable mem-qdrant-watcher")
|
|
|
|
|
else:
|
|
|
|
|
print(f"1. Start the watcher:")
|
|
|
|
|
print(f" sudo systemctl start mem-qdrant-watcher")
|
|
|
|
|
print(f" sudo systemctl enable mem-qdrant-watcher")
|
|
|
|
|
print(f"\nNote: Simple mode has no curator. Memories are captured only.")
|
|
|
|
|
|
|
|
|
|
print(f"\n3. Check logs:")
|
|
|
|
|
print(f" tail -f /var/log/true-recall-timer.log")
|
|
|
|
|
print(f" sudo journalctl -u mem-qdrant-watcher -f")
|
|
|
|
|
|
|
|
|
|
print(f"\n4. Run validation:")
|
|
|
|
|
print(f" cat checklist.md")
|
|
|
|
|
|
|
|
|
|
print("\n✅ Installation complete!")
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|