"""Orsync Scenarist — Local Run Script. Prerequisites: Run ``python setup.py`` first to install dependencies and configure .env. Usage: python run.py # Start API server on port 7860 python run.py --port 8000 # Custom port python run.py --reload # Auto-reload on code changes (dev mode) python run.py --no-redis # Skip embedded Redis startup python run.py --no-neo4j # Skip embedded Neo4j startup python run.py --no-chroma # Skip embedded ChromaDB startup """ from __future__ import annotations import argparse import errno import os import shutil import socket import subprocess import sys import time ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) PROJECT_PARENT = os.path.dirname(ROOT_DIR) if PROJECT_PARENT not in sys.path: sys.path.insert(0, PROJECT_PARENT) SETUP_MARKER = os.path.join(ROOT_DIR, ".setup_done") _redis_proc: subprocess.Popen | None = None _neo4j_proc: subprocess.Popen | None = None _chroma_proc: subprocess.Popen | None = None def _is_neo4j_ready(uri: str, username: str, password: str) -> bool: try: from neo4j import GraphDatabase driver = GraphDatabase.driver(uri, auth=(username, password)) try: driver.verify_connectivity() return True finally: driver.close() except Exception: return False def _is_chroma_ready(host: str, port: int) -> bool: try: import chromadb client = chromadb.HttpClient(host=host, port=port) client.heartbeat() return True except Exception: return False def _is_port_available(host: str, port: int) -> bool: """Return True when the requested bind address can accept a TCP listener.""" family = socket.AF_INET6 if ":" in host else socket.AF_INET probe = socket.socket(family, socket.SOCK_STREAM) try: probe.bind((host, port)) except OSError as exc: if exc.errno in {errno.EADDRINUSE, errno.EACCES, 10013, 10048}: return False raise finally: probe.close() return True def _display_host(host: str) -> str: if host in {"0.0.0.0", "::"}: return "127.0.0.1" return host def _ensure_setup() -> None: """Run setup.py automatically if it hasn't been run yet.""" if os.path.isfile(SETUP_MARKER): return print("[run] First run detected — launching setup ...\n") result = subprocess.run([sys.executable, os.path.join(ROOT_DIR, "setup.py")]) if result.returncode != 0: print("\n[run] Setup failed. Fix the issues above and try again.") sys.exit(1) print() def _start_redis() -> bool: """Start an embedded Redis server if redis-server is available locally.""" global _redis_proc # First check if Redis is already running try: import redis as _redis r = _redis.from_url("redis://localhost:6379/0", socket_connect_timeout=1) r.ping() print("[run] Redis: already running on localhost:6379") return True except Exception: pass # Try to start redis-server redis_bin = shutil.which("redis-server") if not redis_bin: print("[run] Redis: redis-server not found — using in-memory fallback") print("[run] Install Redis: https://redis.io/download") return False try: _redis_proc = subprocess.Popen( [redis_bin, "--daemonize", "no", "--save", "", "--appendonly", "no", "--maxmemory", "256mb", "--maxmemory-policy", "allkeys-lru"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) # Wait for it to be ready for _ in range(20): try: import redis as _redis r = _redis.from_url("redis://localhost:6379/0", socket_connect_timeout=1) r.ping() print("[run] Redis: started embedded server on localhost:6379") return True except Exception: time.sleep(0.25) print("[run] Redis: server started but not responding — using in-memory fallback") return False except Exception as exc: print(f"[run] Redis: failed to start — {exc}") return False def _start_neo4j() -> bool: """Start an embedded Neo4j server if neo4j is available locally.""" global _neo4j_proc from backend.app.core.config import settings neo4j_uri = settings.neo4j_uri neo4j_username = settings.neo4j_username neo4j_password = settings.neo4j_password # First check if Neo4j is already running if _is_neo4j_ready(neo4j_uri, neo4j_username, neo4j_password): print(f"[run] Neo4j: already running on {neo4j_uri}") return True # Find neo4j binary — check common locations neo4j_bin = shutil.which("neo4j") if not neo4j_bin: # Check common install paths for candidate in [ "/opt/neo4j/bin/neo4j", os.path.expanduser("~/neo4j/bin/neo4j"), r"C:\Program Files\Neo4j\bin\neo4j.bat", ]: if os.path.isfile(candidate): neo4j_bin = candidate break if not neo4j_bin: print("[run] Neo4j: not found — graph queries will use no-op stub") print("[run] Install: https://neo4j.com/download/") return False try: # Use 'neo4j console' which runs in foreground (suitable for subprocess) _neo4j_proc = subprocess.Popen( [neo4j_bin, "console"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) # Wait for bolt port to be ready print("[run] Neo4j: starting ...") for _ in range(60): if _is_neo4j_ready(neo4j_uri, neo4j_username, neo4j_password): print(f"[run] Neo4j: started on {neo4j_uri}") return True time.sleep(0.5) print("[run] Neo4j: started but not responding — using no-op stub") return False except Exception as exc: print(f"[run] Neo4j: failed to start — {exc}") return False def _start_chroma() -> bool: """Start an embedded ChromaDB server if the chroma CLI is available.""" global _chroma_proc from backend.app.core.config import settings chroma_bind_host = settings.chroma_host chroma_probe_host = _display_host(chroma_bind_host) chroma_port = settings.chroma_port # First check if ChromaDB is already running if _is_chroma_ready(chroma_probe_host, chroma_port): print(f"[run] ChromaDB: already running on {chroma_probe_host}:{chroma_port}") return True # Find chroma binary chroma_bin = shutil.which("chroma") if not chroma_bin: print("[run] ChromaDB: chroma CLI not found — using no-op stub") print("[run] Install: pip install chromadb") return False try: data_dir = os.path.join(ROOT_DIR, ".chroma_data") os.makedirs(data_dir, exist_ok=True) _chroma_proc = subprocess.Popen( [chroma_bin, "run", "--path", data_dir, "--host", chroma_bind_host, "--port", str(chroma_port)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) print("[run] ChromaDB: starting ...") for _ in range(10): if _is_chroma_ready(chroma_probe_host, chroma_port): print(f"[run] ChromaDB: started on {chroma_probe_host}:{chroma_port}") return True if _chroma_proc.poll() is not None: print("[run] ChromaDB: process exited before becoming ready — using no-op stub") return False time.sleep(0.5) if _chroma_proc.poll() is None: print( "[run] ChromaDB: still warming up in background — " "vector search will connect automatically once ready" ) return True print("[run] ChromaDB: process exited before becoming ready — using no-op stub") return False except Exception as exc: print(f"[run] ChromaDB: failed to start — {exc}") return False def main() -> None: parser = argparse.ArgumentParser(description="Run Orsync Scenarist backend locally") default_host = "127.0.0.1" if os.name == "nt" else "0.0.0.0" parser.add_argument("--host", default=default_host, help=f"Bind address (default: {default_host})") parser.add_argument("--port", type=int, default=None, help="Port (default: from .env or 7860)") parser.add_argument("--reload", action="store_true", help="Enable auto-reload for development") parser.add_argument("--no-redis", action="store_true", help="Skip embedded Redis startup") parser.add_argument("--no-neo4j", action="store_true", help="Skip embedded Neo4j startup") parser.add_argument("--no-chroma", action="store_true", help="Skip embedded ChromaDB startup") args = parser.parse_args() os.chdir(ROOT_DIR) _ensure_setup() from backend.app.core.config import settings port = args.port or settings.port if not _is_port_available(args.host, port): display_host = _display_host(args.host) print(f"[run] Port {port} is already in use on {args.host}.") print(f"[run] Check the existing server: http://{display_host}:{port}/healthz") print(f"[run] Or start another instance on a different port: python run.py --port {port + 1}") sys.exit(1) # Start embedded services unless --no-* or running inside Docker is_docker = os.path.isfile("/.dockerenv") if not args.no_redis and not is_docker: _start_redis() if not args.no_neo4j and not is_docker: _start_neo4j() if not args.no_chroma and not is_docker: _start_chroma() print(f"[run] Starting Orsync Scenarist on http://{args.host}:{port}") print(f"[run] LLM model : {settings.ollama_model}") print(f"[run] Embeddings: {settings.embedding_model}") print(f"[run] Ollama host: {settings.ollama_host}") import uvicorn try: uvicorn.run( "backend.app.main:app", host=args.host, port=port, reload=args.reload, log_level="info", log_config=None, access_log=False, timeout_keep_alive=0, ) finally: if _redis_proc is not None: _redis_proc.terminate() _redis_proc.wait(timeout=5) if _neo4j_proc is not None: _neo4j_proc.terminate() _neo4j_proc.wait(timeout=10) if _chroma_proc is not None: _chroma_proc.terminate() _chroma_proc.wait(timeout=5) if __name__ == "__main__": main()