Spaces:
Sleeping
Sleeping
File size: 6,500 Bytes
901e5fa 933331c 901e5fa 933331c 901e5fa 933331c 901e5fa 933331c 901e5fa 933331c 901e5fa 933331c 901e5fa | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 | # src/ai_agent/cli.py
from __future__ import annotations
import argparse
import os
import sys
import threading
import time
from dotenv import load_dotenv
import logging
load_dotenv()
log = logging.getLogger("ai_agent.cli")
from ai_agent.catalog.sync import sync_once
from ai_agent.utils.shutdown import register as _register_shutdown_hooks
def _ui_funcs():
# Lazy import avoids loading agent/model modules for non-UI commands.
from ai_agent.ui.app import (
get_pipeline,
refresh_ui_docs_from_index,
launch,
ensure_logging_initialized,
)
return get_pipeline, refresh_ui_docs_from_index, launch, ensure_logging_initialized
# --------------------------- catalog background refresher ---------------------------
def _background_refresh():
"""If SYNC_EVERY_HOURS > 0, refresh in the background while UI runs."""
hours = float(os.getenv("SYNC_EVERY_HOURS", "0") or 0)
if hours <= 0:
log.info("[auto-refresh] disabled")
return
def _loop():
# Startup already performs one sync in run_chat(); wait one full interval
# before the first background refresh to avoid duplicate work.
interval_s = max(60.0, hours * 3600.0)
time.sleep(interval_s)
while True:
try:
res = sync_once()
log.info(
"[auto-refresh] %s → %s",
res.get("count", "?"),
res.get("jsonl_path"),
)
get_pipeline, refresh_ui_docs_from_index, _, _ = _ui_funcs()
pipe = get_pipeline()
if res.get("changed"):
ok = pipe.reload_index()
if ok:
log.info("[auto-refresh] reloaded FAISS index")
refresh_ui_docs_from_index()
else:
log.warning(
"[auto-refresh] reload failed; serving previous index"
)
else:
log.info("[auto-refresh] catalog unchanged; FAISS not touched")
except Exception:
log.exception("[auto-refresh] error")
try:
time.sleep(interval_s)
except Exception:
time.sleep(3600.0)
t = threading.Thread(target=_loop, daemon=True)
t.start()
def _startup_sync_async():
"""Run the initial catalog sync + index reload in a daemon thread so it never
blocks server/UI startup. The app comes up immediately (serving whatever
index already exists, if any); recommendations populate once the potentially
slow remote embedding finishes."""
def _run():
try:
res = sync_once()
log.info(
"[startup-sync] %s → %s", res.get("count", "?"), res.get("jsonl_path")
)
if res.get("changed"):
get_pipeline, refresh_ui_docs_from_index, _, _ = _ui_funcs()
if get_pipeline().reload_index():
log.info("[startup-sync] reloaded FAISS index")
try:
refresh_ui_docs_from_index()
except Exception:
pass
else:
log.warning("[startup-sync] reload failed; keeping current index")
else:
log.info("[startup-sync] catalog unchanged; keeping current index")
except Exception:
log.exception("[startup-sync] failed")
threading.Thread(target=_run, daemon=True).start()
# --------------------------- custom tasks ---------------------------
def run_chat():
"""Launch the chat-based UI."""
try:
_, _, _, ensure_logging_initialized = _ui_funcs()
ensure_logging_initialized()
_register_shutdown_hooks()
# Bring up the pipeline against any existing on-disk index without
# blocking on a remote catalog sync (that runs in the background below),
# so the UI is available immediately even on a cold start.
get_pipeline, _, _, _ = _ui_funcs()
get_pipeline()
except Exception:
log.exception("[startup] pipeline init failed")
_startup_sync_async()
_background_refresh()
try:
_, _, launch, _ = _ui_funcs()
launch()
except Exception:
log.exception("[chat-launch] failed")
raise
def run_sync():
try:
_register_shutdown_hooks()
r = sync_once()
log.info("[sync] %s → %s", r.get("count", "?"), r.get("jsonl_path"))
except Exception:
log.exception("[sync] failed")
raise
def run_serve():
"""Launch the FastAPI backend with uvicorn.
The FastAPI app reuses the same pipeline singleton as the Gradio path,
so a one-time catalog sync keeps both surfaces consistent. That sync runs in
a background thread so uvicorn binds the port immediately (a cold sync embeds
the whole catalog remotely and can take minutes).
"""
_startup_sync_async()
_background_refresh()
import uvicorn
host = os.getenv("HOST", "0.0.0.0")
port = int(os.getenv("PORT", "8000"))
reload_flag = os.getenv("UVICORN_RELOAD", "0").lower() in ("1", "true", "yes", "on")
log.info("Starting FastAPI on %s:%d (reload=%s)", host, port, reload_flag)
uvicorn.run(
"ai_agent.api.server:app",
host=host,
port=port,
reload=reload_flag,
proxy_headers=True,
forwarded_allow_ips="*",
)
# --------------------------- main entry ---------------------------
def main():
p = argparse.ArgumentParser(description="AI Agent CLI")
p.add_argument(
"mode",
choices=["chat", "sync", "serve"],
help=(
"'chat' launches the legacy Gradio UI; "
"'sync' runs one catalog refresh; "
"'serve' starts the FastAPI backend (used by the React frontend)."
),
)
args = p.parse_args()
if args.mode == "chat":
run_chat()
elif args.mode == "sync":
run_sync()
elif args.mode == "serve":
run_serve()
else:
p.print_help()
sys.exit(f"Unsupported mode: {args.mode}")
if __name__ == "__main__":
main()
|