system.py 12 KB

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