system.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. import json
  2. import os
  3. import platform
  4. import subprocess
  5. import sys
  6. from importlib.metadata import version as get_version
  7. import asyncclick as click
  8. from asyncclick import pass_context
  9. from dotenv import load_dotenv
  10. from cli.command_group import cli
  11. from cli.utils.docker_utils import (
  12. bring_down_docker_compose,
  13. remove_r2r_network,
  14. run_docker_serve,
  15. run_local_serve,
  16. wait_for_container_health,
  17. )
  18. from cli.utils.timer import timer
  19. from r2r import R2RAsyncClient, R2RException
  20. @click.group()
  21. def system():
  22. """System commands."""
  23. pass
  24. @cli.command()
  25. @pass_context
  26. async def health(ctx: click.Context):
  27. """Check the health of the server."""
  28. client: R2RAsyncClient = ctx.obj
  29. try:
  30. with timer():
  31. response = await client.system.health()
  32. click.echo(json.dumps(response, indent=2))
  33. except R2RException as e:
  34. click.echo(str(e), err=True)
  35. raise
  36. except Exception as e:
  37. click.echo(str(f"An unexpected error occurred: {e}"), err=True)
  38. raise
  39. @system.command()
  40. @pass_context
  41. async def settings(ctx: click.Context):
  42. """Retrieve application settings."""
  43. client: R2RAsyncClient = ctx.obj
  44. try:
  45. with timer():
  46. response = await client.system.settings()
  47. click.echo(json.dumps(response, indent=2))
  48. except R2RException as e:
  49. click.echo(str(e), err=True)
  50. raise
  51. except Exception as e:
  52. click.echo(str(f"An unexpected error occurred: {e}"), err=True)
  53. raise
  54. @system.command()
  55. @pass_context
  56. async def status(ctx: click.Context):
  57. """Get statistics about the server, including the start time, uptime, CPU usage, and memory usage."""
  58. client: R2RAsyncClient = ctx.obj
  59. try:
  60. with timer():
  61. response = await client.system.status()
  62. click.echo(json.dumps(response, indent=2))
  63. except R2RException as e:
  64. click.echo(str(e), err=True)
  65. raise
  66. except Exception as e:
  67. click.echo(str(f"An unexpected error occurred: {e}"), err=True)
  68. raise
  69. @cli.command()
  70. @click.option("--host", default=None, help="Host to run the server on")
  71. @click.option(
  72. "--port", default=None, type=int, help="Port to run the server on"
  73. )
  74. @click.option("--docker", is_flag=True, help="Run using Docker")
  75. @click.option(
  76. "--full",
  77. is_flag=True,
  78. help="Run the full R2R compose? This includes Hatchet and Unstructured.",
  79. )
  80. @click.option(
  81. "--project-name", default=None, help="Project name for Docker deployment"
  82. )
  83. @click.option(
  84. "--config-name", default=None, help="Name of the R2R configuration to use"
  85. )
  86. @click.option(
  87. "--config-path",
  88. default=None,
  89. help="Path to a custom R2R configuration file",
  90. )
  91. @click.option(
  92. "--build",
  93. is_flag=True,
  94. default=False,
  95. help="Run in debug mode. Only for development.",
  96. )
  97. @click.option("--image", help="Docker image to use")
  98. @click.option(
  99. "--image-env",
  100. default="prod",
  101. help="Which dev environment to pull the image from?",
  102. )
  103. @click.option(
  104. "--scale",
  105. default=None,
  106. help="How many instances of the R2R service to run",
  107. )
  108. @click.option(
  109. "--exclude-postgres",
  110. is_flag=True,
  111. default=False,
  112. help="Excludes creating a Postgres container in the Docker setup.",
  113. )
  114. async def serve(
  115. host,
  116. port,
  117. docker,
  118. full,
  119. project_name,
  120. config_name,
  121. config_path,
  122. build,
  123. image,
  124. image_env,
  125. scale,
  126. exclude_postgres,
  127. ):
  128. """Start the R2R server."""
  129. load_dotenv()
  130. click.echo("Spinning up an R2R deployment...")
  131. if host is None:
  132. host = os.getenv("R2R_HOST", "0.0.0.0")
  133. if port is None:
  134. port = int(os.getenv("R2R_PORT", (os.getenv("PORT", "7272"))))
  135. click.echo(f"Running on {host}:{port}, with docker={docker}")
  136. if full:
  137. click.echo(
  138. "Running the full R2R setup which includes `Hatchet` and `Unstructured.io`."
  139. )
  140. if config_path and config_name:
  141. raise click.UsageError(
  142. "Both `config-path` and `config-name` were provided. Please provide only one."
  143. )
  144. if config_name and os.path.isfile(config_name):
  145. click.echo(
  146. "Warning: `config-name` corresponds to an existing file. If you intended a custom config, use `config-path`."
  147. )
  148. if build:
  149. click.echo(
  150. "`build` flag detected. Building Docker image from local repository..."
  151. )
  152. if image and image_env:
  153. click.echo(
  154. "WARNING: Both `image` and `image_env` were provided. Using `image`."
  155. )
  156. if not image and docker:
  157. r2r_version = get_version("r2r")
  158. version_specific_image = f"ragtoriches/{image_env}:{r2r_version}"
  159. latest_image = f"ragtoriches/{image_env}:latest"
  160. def image_exists(img):
  161. try:
  162. subprocess.run(
  163. ["docker", "manifest", "inspect", img],
  164. check=True,
  165. capture_output=True,
  166. text=True,
  167. )
  168. return True
  169. except subprocess.CalledProcessError:
  170. return False
  171. if image_exists(version_specific_image):
  172. click.echo(f"Using image: {version_specific_image}")
  173. image = version_specific_image
  174. elif image_exists(latest_image):
  175. click.echo(
  176. f"Version-specific image not found. Using latest: {latest_image}"
  177. )
  178. image = latest_image
  179. else:
  180. click.echo(
  181. f"Neither {version_specific_image} nor {latest_image} found in remote registry. Confirm the sanity of your output for `docker manifest inspect ragtoriches/{version_specific_image}` and `docker manifest inspect ragtoriches/{latest_image}`."
  182. )
  183. click.echo(
  184. "Please pull the required image or build it using the --build flag."
  185. )
  186. raise click.Abort()
  187. if docker:
  188. os.environ["R2R_IMAGE"] = image
  189. if build:
  190. subprocess.run(
  191. ["docker", "build", "-t", image, "-f", "Dockerfile", "."],
  192. check=True,
  193. )
  194. if config_path:
  195. config_path = os.path.abspath(config_path)
  196. # For Windows, convert backslashes to forward slashes and prepend /host_mnt/
  197. if platform.system() == "Windows":
  198. drive, path = os.path.splitdrive(config_path)
  199. config_path = f"/host_mnt/{drive[0].lower()}" + path.replace(
  200. "\\", "/"
  201. )
  202. if docker:
  203. run_docker_serve(
  204. host,
  205. port,
  206. full,
  207. project_name,
  208. image,
  209. config_name,
  210. config_path,
  211. exclude_postgres,
  212. scale,
  213. )
  214. if (
  215. "pytest" in sys.modules
  216. or "unittest" in sys.modules
  217. or os.environ.get("PYTEST_CURRENT_TEST")
  218. ):
  219. click.echo("Test environment detected. Skipping browser open.")
  220. else:
  221. # Open browser after Docker setup is complete
  222. import webbrowser
  223. click.echo("Waiting for all services to become healthy...")
  224. if not wait_for_container_health(
  225. project_name or ("r2r-full" if full else "r2r"), "r2r"
  226. ):
  227. click.secho(
  228. "r2r container failed to become healthy.", fg="red"
  229. )
  230. return
  231. traefik_port = os.environ.get("R2R_DASHBOARD_PORT", "80")
  232. url = f"http://localhost:{traefik_port}"
  233. click.secho(f"Navigating to R2R application at {url}.", fg="blue")
  234. webbrowser.open(url)
  235. else:
  236. await run_local_serve(host, port, config_name, config_path, full)
  237. @cli.command()
  238. @click.option(
  239. "--volumes",
  240. is_flag=True,
  241. help="Remove named volumes declared in the `volumes` section of the Compose file",
  242. )
  243. @click.option(
  244. "--remove-orphans",
  245. is_flag=True,
  246. help="Remove containers for services not defined in the Compose file",
  247. )
  248. @click.option(
  249. "--project-name",
  250. default=None,
  251. help="Which Docker Compose project to bring down",
  252. )
  253. def docker_down(volumes, remove_orphans, project_name):
  254. """Bring down the Docker Compose setup and attempt to remove the network if necessary."""
  255. if not project_name:
  256. print("Bringing down the default R2R Docker setup(s)...")
  257. try:
  258. result = bring_down_docker_compose(
  259. project_name or "r2r", volumes, remove_orphans
  260. )
  261. except:
  262. pass
  263. try:
  264. result = bring_down_docker_compose(
  265. project_name or "r2r-full", volumes, remove_orphans
  266. )
  267. except:
  268. pass
  269. else:
  270. print(f"Bringing down the `{project_name}` R2R Docker setup...")
  271. result = bring_down_docker_compose(
  272. project_name, volumes, remove_orphans
  273. )
  274. if result != 0:
  275. click.echo(
  276. f"An error occurred while bringing down the {project_name} Docker Compose setup. Attempting to remove the network..."
  277. )
  278. else:
  279. click.echo(
  280. f"{project_name} Docker Compose setup has been successfully brought down."
  281. )
  282. remove_r2r_network()
  283. @cli.command()
  284. def generate_report():
  285. """Generate a system report including R2R version, Docker info, and OS details."""
  286. # Get R2R version
  287. from importlib.metadata import version
  288. report = {"r2r_version": version("r2r")}
  289. # Get Docker info
  290. try:
  291. subprocess.run(
  292. ["docker", "version"], check=True, capture_output=True, timeout=5
  293. )
  294. docker_ps_output = subprocess.check_output(
  295. ["docker", "ps", "--format", "{{.ID}}\t{{.Names}}\t{{.Status}}"],
  296. text=True,
  297. timeout=5,
  298. ).strip()
  299. report["docker_ps"] = [
  300. dict(zip(["id", "name", "status"], line.split("\t")))
  301. for line in docker_ps_output.split("\n")
  302. if line
  303. ]
  304. docker_network_output = subprocess.check_output(
  305. ["docker", "network", "ls", "--format", "{{.ID}}\t{{.Name}}"],
  306. text=True,
  307. timeout=5,
  308. ).strip()
  309. networks = [
  310. dict(zip(["id", "name"], line.split("\t")))
  311. for line in docker_network_output.split("\n")
  312. if line
  313. ]
  314. report["docker_subnets"] = []
  315. for network in networks:
  316. inspect_output = subprocess.check_output(
  317. [
  318. "docker",
  319. "network",
  320. "inspect",
  321. network["id"],
  322. "--format",
  323. "{{range .IPAM.Config}}{{.Subnet}}{{end}}",
  324. ],
  325. text=True,
  326. timeout=5,
  327. ).strip()
  328. if subnet := inspect_output:
  329. network["subnet"] = subnet
  330. report["docker_subnets"].append(network)
  331. except subprocess.CalledProcessError as e:
  332. report["docker_error"] = f"Error running Docker command: {e}"
  333. except FileNotFoundError:
  334. report["docker_error"] = (
  335. "Docker command not found. Is Docker installed and in PATH?"
  336. )
  337. except subprocess.TimeoutExpired:
  338. report["docker_error"] = (
  339. "Docker command timed out. Docker might be unresponsive."
  340. )
  341. # Get OS information
  342. report["os_info"] = {
  343. "system": platform.system(),
  344. "release": platform.release(),
  345. "version": platform.version(),
  346. "machine": platform.machine(),
  347. "processor": platform.processor(),
  348. }
  349. click.echo("System Report:")
  350. click.echo(json.dumps(report, indent=2))
  351. @cli.command()
  352. def update():
  353. """Update the R2R package to the latest version."""
  354. try:
  355. cmd = [sys.executable, "-m", "pip", "install", "--upgrade", "r2r"]
  356. click.echo("Updating R2R...")
  357. result = subprocess.run(
  358. cmd, check=True, capture_output=True, text=True
  359. )
  360. click.echo(result.stdout)
  361. click.echo("R2R has been successfully updated.")
  362. except subprocess.CalledProcessError as e:
  363. click.echo(f"An error occurred while updating R2R: {e}")
  364. click.echo(e.stderr)
  365. except Exception as e:
  366. click.echo(f"An unexpected error occurred: {e}")
  367. @cli.command()
  368. def version():
  369. """Reports the SDK version."""
  370. from importlib.metadata import version
  371. try:
  372. r2r_version = version("r2r")
  373. click.echo(json.dumps(r2r_version, indent=2))
  374. except Exception as e:
  375. click.echo(str(f"An unexpected error occurred: {e}"), err=True)
  376. raise