cli.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  1. import json
  2. import os
  3. import re
  4. import shutil
  5. import signal
  6. import subprocess
  7. import sys
  8. import tempfile
  9. import zipfile
  10. from pathlib import Path
  11. import click
  12. import pkg_resources
  13. import requests
  14. from rich.console import Console
  15. from embedchain.telemetry.posthog import AnonymousTelemetry
  16. console = Console()
  17. @click.group()
  18. def cli():
  19. pass
  20. anonymous_telemetry = AnonymousTelemetry()
  21. api_process = None
  22. ui_process = None
  23. def signal_handler(sig, frame):
  24. """Signal handler to catch termination signals and kill server processes."""
  25. global api_process, ui_process
  26. console.print("\n🛑 [bold yellow]Stopping servers...[/bold yellow]")
  27. if api_process:
  28. api_process.terminate()
  29. console.print("🛑 [bold yellow]API server stopped.[/bold yellow]")
  30. if ui_process:
  31. ui_process.terminate()
  32. console.print("🛑 [bold yellow]UI server stopped.[/bold yellow]")
  33. sys.exit(0)
  34. def get_pkg_path_from_name(template: str):
  35. try:
  36. # Determine the installation location of the embedchain package
  37. package_path = pkg_resources.resource_filename("embedchain", "")
  38. except ImportError:
  39. console.print("❌ [bold red]Failed to locate the 'embedchain' package. Is it installed?[/bold red]")
  40. return
  41. # Construct the source path from the embedchain package
  42. src_path = os.path.join(package_path, "deployment", template)
  43. if not os.path.exists(src_path):
  44. console.print(f"❌ [bold red]Template '{template}' not found.[/bold red]")
  45. return
  46. return src_path
  47. def setup_fly_io_app(extra_args):
  48. fly_launch_command = ["fly", "launch", "--region", "sjc", "--no-deploy"] + list(extra_args)
  49. try:
  50. console.print(f"🚀 [bold cyan]Running: {' '.join(fly_launch_command)}[/bold cyan]")
  51. shutil.move(".env.example", ".env")
  52. subprocess.run(fly_launch_command, check=True)
  53. console.print("✅ [bold green]'fly launch' executed successfully.[/bold green]")
  54. except subprocess.CalledProcessError as e:
  55. console.print(f"❌ [bold red]An error occurred: {e}[/bold red]")
  56. except FileNotFoundError:
  57. console.print(
  58. "❌ [bold red]'fly' command not found. Please ensure Fly CLI is installed and in your PATH.[/bold red]"
  59. )
  60. def setup_modal_com_app(extra_args):
  61. modal_setup_file = os.path.join(os.path.expanduser("~"), ".modal.toml")
  62. if os.path.exists(modal_setup_file):
  63. console.print(
  64. """✅ [bold green]Modal setup already done. You can now install the dependencies by doing \n
  65. `pip install -r requirements.txt`[/bold green]"""
  66. )
  67. else:
  68. modal_setup_cmd = ["modal", "setup"] + list(extra_args)
  69. console.print(f"🚀 [bold cyan]Running: {' '.join(modal_setup_cmd)}[/bold cyan]")
  70. subprocess.run(modal_setup_cmd, check=True)
  71. shutil.move(".env.example", ".env")
  72. console.print(
  73. """Great! Now you can install the dependencies by doing: \n
  74. `pip install -r requirements.txt`\n
  75. \n
  76. To run your app locally:\n
  77. `ec dev`
  78. """
  79. )
  80. def setup_render_com_app():
  81. render_setup_file = os.path.join(os.path.expanduser("~"), ".render/config.yaml")
  82. if os.path.exists(render_setup_file):
  83. console.print(
  84. """✅ [bold green]Render setup already done. You can now install the dependencies by doing \n
  85. `pip install -r requirements.txt`[/bold green]"""
  86. )
  87. else:
  88. render_setup_cmd = ["render", "config", "init"]
  89. console.print(f"🚀 [bold cyan]Running: {' '.join(render_setup_cmd)}[/bold cyan]")
  90. subprocess.run(render_setup_cmd, check=True)
  91. shutil.move(".env.example", ".env")
  92. console.print(
  93. """Great! Now you can install the dependencies by doing: \n
  94. `pip install -r requirements.txt`\n
  95. \n
  96. To run your app locally:\n
  97. `ec dev`
  98. """
  99. )
  100. def setup_streamlit_io_app():
  101. # nothing needs to be done here
  102. console.print("Great! Now you can install the dependencies by doing `pip install -r requirements.txt`")
  103. def setup_gradio_app():
  104. # nothing needs to be done here
  105. console.print("Great! Now you can install the dependencies by doing `pip install -r requirements.txt`")
  106. def setup_hf_app():
  107. subprocess.run(["pip", "install", "huggingface_hub[cli]"], check=True)
  108. hf_setup_file = os.path.join(os.path.expanduser("~"), ".cache/huggingface/token")
  109. if os.path.exists(hf_setup_file):
  110. console.print(
  111. """✅ [bold green]HuggingFace setup already done. You can now install the dependencies by doing \n
  112. `pip install -r requirements.txt`[/bold green]"""
  113. )
  114. else:
  115. console.print(
  116. """🚀 [cyan]Running: huggingface-cli login \n
  117. Please provide a [bold]WRITE[/bold] token so that we can directly deploy\n
  118. your apps from the terminal.[/cyan]
  119. """
  120. )
  121. subprocess.run(["huggingface-cli", "login"], check=True)
  122. console.print("Great! Now you can install the dependencies by doing `pip install -r requirements.txt`")
  123. @cli.command()
  124. @click.option("--template", default="fly.io", help="The template to use.")
  125. @click.argument("extra_args", nargs=-1, type=click.UNPROCESSED)
  126. def create(template, extra_args):
  127. anonymous_telemetry.capture(event_name="ec_create", properties={"template_used": template})
  128. template_dir = template
  129. if "/" in template_dir:
  130. template_dir = template.split("/")[1]
  131. src_path = get_pkg_path_from_name(template_dir)
  132. shutil.copytree(src_path, os.getcwd(), dirs_exist_ok=True)
  133. console.print(f"✅ [bold green]Successfully created app from template '{template}'.[/bold green]")
  134. if template == "fly.io":
  135. setup_fly_io_app(extra_args)
  136. elif template == "modal.com":
  137. setup_modal_com_app(extra_args)
  138. elif template == "render.com":
  139. setup_render_com_app()
  140. elif template == "streamlit.io":
  141. setup_streamlit_io_app()
  142. elif template == "gradio.app":
  143. setup_gradio_app()
  144. elif template == "hf/gradio.app" or template == "hf/streamlit.app":
  145. setup_hf_app()
  146. else:
  147. raise ValueError(f"Unknown template '{template}'.")
  148. embedchain_config = {"provider": template}
  149. with open("embedchain.json", "w") as file:
  150. json.dump(embedchain_config, file, indent=4)
  151. console.print(
  152. f"🎉 [green]All done! Successfully created `embedchain.json` with '{template}' as provider.[/green]"
  153. )
  154. def run_dev_fly_io(debug, host, port):
  155. uvicorn_command = ["uvicorn", "app:app"]
  156. if debug:
  157. uvicorn_command.append("--reload")
  158. uvicorn_command.extend(["--host", host, "--port", str(port)])
  159. try:
  160. console.print(f"🚀 [bold cyan]Running FastAPI app with command: {' '.join(uvicorn_command)}[/bold cyan]")
  161. subprocess.run(uvicorn_command, check=True)
  162. except subprocess.CalledProcessError as e:
  163. console.print(f"❌ [bold red]An error occurred: {e}[/bold red]")
  164. except KeyboardInterrupt:
  165. console.print("\n🛑 [bold yellow]FastAPI server stopped[/bold yellow]")
  166. def run_dev_modal_com():
  167. modal_run_cmd = ["modal", "serve", "app"]
  168. try:
  169. console.print(f"🚀 [bold cyan]Running FastAPI app with command: {' '.join(modal_run_cmd)}[/bold cyan]")
  170. subprocess.run(modal_run_cmd, check=True)
  171. except subprocess.CalledProcessError as e:
  172. console.print(f"❌ [bold red]An error occurred: {e}[/bold red]")
  173. except KeyboardInterrupt:
  174. console.print("\n🛑 [bold yellow]FastAPI server stopped[/bold yellow]")
  175. def run_dev_streamlit_io():
  176. streamlit_run_cmd = ["streamlit", "run", "app.py"]
  177. try:
  178. console.print(f"🚀 [bold cyan]Running Streamlit app with command: {' '.join(streamlit_run_cmd)}[/bold cyan]")
  179. subprocess.run(streamlit_run_cmd, check=True)
  180. except subprocess.CalledProcessError as e:
  181. console.print(f"❌ [bold red]An error occurred: {e}[/bold red]")
  182. except KeyboardInterrupt:
  183. console.print("\n🛑 [bold yellow]Streamlit server stopped[/bold yellow]")
  184. def run_dev_render_com(debug, host, port):
  185. uvicorn_command = ["uvicorn", "app:app"]
  186. if debug:
  187. uvicorn_command.append("--reload")
  188. uvicorn_command.extend(["--host", host, "--port", str(port)])
  189. try:
  190. console.print(f"🚀 [bold cyan]Running FastAPI app with command: {' '.join(uvicorn_command)}[/bold cyan]")
  191. subprocess.run(uvicorn_command, check=True)
  192. except subprocess.CalledProcessError as e:
  193. console.print(f"❌ [bold red]An error occurred: {e}[/bold red]")
  194. except KeyboardInterrupt:
  195. console.print("\n🛑 [bold yellow]FastAPI server stopped[/bold yellow]")
  196. def run_dev_gradio():
  197. gradio_run_cmd = ["gradio", "app.py"]
  198. try:
  199. console.print(f"🚀 [bold cyan]Running Gradio app with command: {' '.join(gradio_run_cmd)}[/bold cyan]")
  200. subprocess.run(gradio_run_cmd, check=True)
  201. except subprocess.CalledProcessError as e:
  202. console.print(f"❌ [bold red]An error occurred: {e}[/bold red]")
  203. except KeyboardInterrupt:
  204. console.print("\n🛑 [bold yellow]Gradio server stopped[/bold yellow]")
  205. @cli.command()
  206. @click.option("--debug", is_flag=True, help="Enable or disable debug mode.")
  207. @click.option("--host", default="127.0.0.1", help="The host address to run the FastAPI app on.")
  208. @click.option("--port", default=8000, help="The port to run the FastAPI app on.")
  209. def dev(debug, host, port):
  210. template = ""
  211. with open("embedchain.json", "r") as file:
  212. embedchain_config = json.load(file)
  213. template = embedchain_config["provider"]
  214. anonymous_telemetry.capture(event_name="ec_dev", properties={"template_used": template})
  215. if template == "fly.io":
  216. run_dev_fly_io(debug, host, port)
  217. elif template == "modal.com":
  218. run_dev_modal_com()
  219. elif template == "render.com":
  220. run_dev_render_com(debug, host, port)
  221. elif template == "streamlit.io" or template == "hf/streamlit.app":
  222. run_dev_streamlit_io()
  223. elif template == "gradio.app" or template == "hf/gradio.app":
  224. run_dev_gradio()
  225. else:
  226. raise ValueError(f"Unknown template '{template}'.")
  227. def read_env_file(env_file_path):
  228. """
  229. Reads an environment file and returns a dictionary of key-value pairs.
  230. Args:
  231. env_file_path (str): The path to the .env file.
  232. Returns:
  233. dict: Dictionary of environment variables.
  234. """
  235. env_vars = {}
  236. with open(env_file_path, "r") as file:
  237. for line in file:
  238. # Ignore comments and empty lines
  239. if line.strip() and not line.strip().startswith("#"):
  240. # Assume each line is in the format KEY=VALUE
  241. key_value_match = re.match(r"(\w+)=(.*)", line.strip())
  242. if key_value_match:
  243. key, value = key_value_match.groups()
  244. env_vars[key] = value
  245. return env_vars
  246. def deploy_fly():
  247. app_name = ""
  248. with open("fly.toml", "r") as file:
  249. for line in file:
  250. if line.strip().startswith("app ="):
  251. app_name = line.split("=")[1].strip().strip('"')
  252. if not app_name:
  253. console.print("❌ [bold red]App name not found in fly.toml[/bold red]")
  254. return
  255. env_vars = read_env_file(".env")
  256. secrets_command = ["flyctl", "secrets", "set", "-a", app_name] + [f"{k}={v}" for k, v in env_vars.items()]
  257. deploy_command = ["fly", "deploy"]
  258. try:
  259. # Set secrets
  260. console.print(f"🔐 [bold cyan]Setting secrets for {app_name}[/bold cyan]")
  261. subprocess.run(secrets_command, check=True)
  262. # Deploy application
  263. console.print(f"🚀 [bold cyan]Running: {' '.join(deploy_command)}[/bold cyan]")
  264. subprocess.run(deploy_command, check=True)
  265. console.print("✅ [bold green]'fly deploy' executed successfully.[/bold green]")
  266. except subprocess.CalledProcessError as e:
  267. console.print(f"❌ [bold red]An error occurred: {e}[/bold red]")
  268. except FileNotFoundError:
  269. console.print(
  270. "❌ [bold red]'fly' command not found. Please ensure Fly CLI is installed and in your PATH.[/bold red]"
  271. )
  272. def deploy_modal():
  273. modal_deploy_cmd = ["modal", "deploy", "app"]
  274. try:
  275. console.print(f"🚀 [bold cyan]Running: {' '.join(modal_deploy_cmd)}[/bold cyan]")
  276. subprocess.run(modal_deploy_cmd, check=True)
  277. console.print("✅ [bold green]'modal deploy' executed successfully.[/bold green]")
  278. except subprocess.CalledProcessError as e:
  279. console.print(f"❌ [bold red]An error occurred: {e}[/bold red]")
  280. except FileNotFoundError:
  281. console.print(
  282. "❌ [bold red]'modal' command not found. Please ensure Modal CLI is installed and in your PATH.[/bold red]"
  283. )
  284. def deploy_streamlit():
  285. streamlit_deploy_cmd = ["streamlit", "run", "app.py"]
  286. try:
  287. console.print(f"🚀 [bold cyan]Running: {' '.join(streamlit_deploy_cmd)}[/bold cyan]")
  288. console.print(
  289. """\n\n✅ [bold yellow]To deploy a streamlit app, you can directly it from the UI.\n
  290. Click on the 'Deploy' button on the top right corner of the app.\n
  291. For more information, please refer to https://docs.embedchain.ai/deployment/streamlit_io
  292. [/bold yellow]
  293. \n\n"""
  294. )
  295. subprocess.run(streamlit_deploy_cmd, check=True)
  296. except subprocess.CalledProcessError as e:
  297. console.print(f"❌ [bold red]An error occurred: {e}[/bold red]")
  298. except FileNotFoundError:
  299. console.print(
  300. """❌ [bold red]'streamlit' command not found.\n
  301. Please ensure Streamlit CLI is installed and in your PATH.[/bold red]"""
  302. )
  303. def deploy_render():
  304. render_deploy_cmd = ["render", "blueprint", "launch"]
  305. try:
  306. console.print(f"🚀 [bold cyan]Running: {' '.join(render_deploy_cmd)}[/bold cyan]")
  307. subprocess.run(render_deploy_cmd, check=True)
  308. console.print("✅ [bold green]'render blueprint launch' executed successfully.[/bold green]")
  309. except subprocess.CalledProcessError as e:
  310. console.print(f"❌ [bold red]An error occurred: {e}[/bold red]")
  311. except FileNotFoundError:
  312. console.print(
  313. "❌ [bold red]'render' command not found. Please ensure Render CLI is installed and in your PATH.[/bold red]" # noqa:E501
  314. )
  315. def deploy_gradio_app():
  316. gradio_deploy_cmd = ["gradio", "deploy"]
  317. try:
  318. console.print(f"🚀 [bold cyan]Running: {' '.join(gradio_deploy_cmd)}[/bold cyan]")
  319. subprocess.run(gradio_deploy_cmd, check=True)
  320. console.print("✅ [bold green]'gradio deploy' executed successfully.[/bold green]")
  321. except subprocess.CalledProcessError as e:
  322. console.print(f"❌ [bold red]An error occurred: {e}[/bold red]")
  323. except FileNotFoundError:
  324. console.print(
  325. "❌ [bold red]'gradio' command not found. Please ensure Gradio CLI is installed and in your PATH.[/bold red]" # noqa:E501
  326. )
  327. def deploy_hf_spaces(ec_app_name):
  328. if not ec_app_name:
  329. console.print("❌ [bold red]'name' not found in embedchain.json[/bold red]")
  330. return
  331. hf_spaces_deploy_cmd = ["huggingface-cli", "upload", ec_app_name, ".", ".", "--repo-type=space"]
  332. try:
  333. console.print(f"🚀 [bold cyan]Running: {' '.join(hf_spaces_deploy_cmd)}[/bold cyan]")
  334. subprocess.run(hf_spaces_deploy_cmd, check=True)
  335. console.print("✅ [bold green]'huggingface-cli upload' executed successfully.[/bold green]")
  336. except subprocess.CalledProcessError as e:
  337. console.print(f"❌ [bold red]An error occurred: {e}[/bold red]")
  338. @cli.command()
  339. def deploy():
  340. # Check for platform-specific files
  341. template = ""
  342. ec_app_name = ""
  343. with open("embedchain.json", "r") as file:
  344. embedchain_config = json.load(file)
  345. ec_app_name = embedchain_config["name"] if "name" in embedchain_config else None
  346. template = embedchain_config["provider"]
  347. anonymous_telemetry.capture(event_name="ec_deploy", properties={"template_used": template})
  348. if template == "fly.io":
  349. deploy_fly()
  350. elif template == "modal.com":
  351. deploy_modal()
  352. elif template == "render.com":
  353. deploy_render()
  354. elif template == "streamlit.io":
  355. deploy_streamlit()
  356. elif template == "gradio.app":
  357. deploy_gradio_app()
  358. elif template.startswith("hf/"):
  359. deploy_hf_spaces(ec_app_name)
  360. else:
  361. console.print("❌ [bold red]No recognized deployment platform found.[/bold red]")
  362. @cli.command()
  363. def runserver():
  364. # Set up signal handling
  365. signal.signal(signal.SIGINT, signal_handler)
  366. signal.signal(signal.SIGTERM, signal_handler)
  367. # Check if 'api' and 'ui' directories exist
  368. if os.path.exists("api") and os.path.exists("ui"):
  369. pass
  370. else:
  371. # Step 1: Download the zip file
  372. zip_url = "http://github.com/embedchain/ec-admin/archive/main.zip"
  373. try:
  374. response = requests.get(zip_url)
  375. response.raise_for_status()
  376. with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
  377. tmp_file.write(response.content)
  378. zip_file_path = tmp_file.name
  379. console.print("✅ [bold green]Downloaded zip file successfully.[/bold green]")
  380. except requests.RequestException as e:
  381. console.print(f"❌ [bold red]Failed to download zip file: {e}[/bold red]")
  382. return
  383. # Step 2: Extract the zip file
  384. try:
  385. with zipfile.ZipFile(zip_file_path, "r") as zip_ref:
  386. # Get the name of the root directory inside the zip file
  387. root_dir = Path(zip_ref.namelist()[0])
  388. for member in zip_ref.infolist():
  389. # Build the path to extract the file to, skipping the root directory
  390. target_file = Path(member.filename).relative_to(root_dir)
  391. source_file = zip_ref.open(member, "r")
  392. if member.is_dir():
  393. # Create directory if it doesn't exist
  394. os.makedirs(target_file, exist_ok=True)
  395. else:
  396. with open(target_file, "wb") as file:
  397. # Write the file
  398. shutil.copyfileobj(source_file, file)
  399. console.print("✅ [bold green]Extracted zip file successfully.[/bold green]")
  400. except zipfile.BadZipFile:
  401. console.print("❌ [bold red]Error in extracting zip file. The file might be corrupted.[/bold red]")
  402. return
  403. # Step 3: Install API requirements
  404. try:
  405. os.chdir("api")
  406. subprocess.run(["pip", "install", "-r", "requirements.txt"], check=True)
  407. os.chdir("..")
  408. console.print("✅ [bold green]Installed API requirements successfully.[/bold green]")
  409. except Exception as e:
  410. console.print(f"❌ [bold red]Failed to install API requirements: {e}[/bold red]")
  411. return
  412. # Step 4: Start the API server
  413. try:
  414. os.chdir("api")
  415. api_process = subprocess.Popen(
  416. ["uvicorn", "main:app", "--reload", "--host", "127.0.0.1", "--port", "8000"], stdout=None, stderr=None
  417. )
  418. os.chdir("..")
  419. console.print("✅ [bold green]API server started successfully.[/bold green]")
  420. except Exception as e:
  421. console.print(f"❌ [bold red]Failed to start the API server: {e}[/bold red]")
  422. return
  423. # Step 5: Install UI requirements and start the UI server
  424. try:
  425. os.chdir("ui")
  426. subprocess.run(["yarn"], check=True)
  427. ui_process = subprocess.Popen(["yarn", "dev"], stdout=None, stderr=None)
  428. console.print("✅ [bold green]UI server started successfully.[/bold green]")
  429. except Exception as e:
  430. console.print(f"❌ [bold red]Failed to start the UI server: {e}[/bold red]")
  431. # Wait for the subprocesses to complete
  432. api_process.wait()
  433. ui_process.wait()
  434. # Step 6: Install UI requirements and start the UI server
  435. try:
  436. os.chdir("ui")
  437. subprocess.run(["yarn"], check=True)
  438. subprocess.Popen(["yarn", "dev"])
  439. console.print("✅ [bold green]UI server started successfully.[/bold green]")
  440. except Exception as e:
  441. console.print(f"❌ [bold red]Failed to start the UI server: {e}[/bold red]")
  442. # Keep the script running until it receives a kill signal
  443. try:
  444. api_process.wait()
  445. ui_process.wait()
  446. except KeyboardInterrupt:
  447. console.print("\n🛑 [bold yellow]Stopping server...[/bold yellow]")