feat: Add interactive install.py and validation checklist.md

- Install script with full/simple mode selection
- Inline prompt format with 'go back' navigation
- Custom input option on all prompts
- Configuration summary and validation
- Comprehensive installation checklist
This commit is contained in:
root
2026-02-24 20:55:29 -06:00
parent faccc4b651
commit 79fb9c6cb3
2 changed files with 499 additions and 0 deletions

318
install.py Executable file
View File

@@ -0,0 +1,318 @@
#!/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),
]
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
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)
return config_path
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']}")
else:
print("Target collection: (none - simple mode)")
print("=" * 60)
def main():
print("=" * 60)
print("TrueRecall v2 - Interactive Installation")
print("=" * 60)
# 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()