command_group.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. # from .main import load_config
  2. import json
  3. import types
  4. from functools import wraps
  5. from pathlib import Path
  6. from typing import Any, Never
  7. import asyncclick as click
  8. from asyncclick import pass_context
  9. from asyncclick.exceptions import Exit
  10. from rich import box
  11. from rich.console import Console
  12. from rich.table import Table
  13. from sdk import R2RAsyncClient
  14. console = Console()
  15. CONFIG_DIR = Path.home() / ".r2r"
  16. CONFIG_FILE = CONFIG_DIR / "config.json"
  17. def load_config() -> dict[str, Any]:
  18. """
  19. Load the CLI config from ~/.r2r/config.json.
  20. Returns an empty dict if the file doesn't exist or is invalid.
  21. """
  22. if not CONFIG_FILE.is_file():
  23. return {}
  24. try:
  25. with open(CONFIG_FILE, "r", encoding="utf-8") as f:
  26. data = json.load(f)
  27. # Ensure we always have a dict
  28. if not isinstance(data, dict):
  29. return {}
  30. return data
  31. except (IOError, json.JSONDecodeError):
  32. return {}
  33. def silent_exit(ctx, code=0):
  34. if code != 0:
  35. raise Exit(code)
  36. def deprecated_command(new_name):
  37. def decorator(f):
  38. @wraps(f)
  39. async def wrapped(*args, **kwargs):
  40. click.secho(
  41. f"Warning: This command is deprecated. Please use '{new_name}' instead.",
  42. fg="yellow",
  43. err=True,
  44. )
  45. return await f(*args, **kwargs)
  46. return wrapped
  47. return decorator
  48. def custom_help_formatter(commands):
  49. """Create a nicely formatted help table using rich"""
  50. table = Table(
  51. box=box.ROUNDED,
  52. border_style="blue",
  53. pad_edge=False,
  54. collapse_padding=True,
  55. )
  56. table.add_column("Command", style="cyan", no_wrap=True)
  57. table.add_column("Description", style="white")
  58. command_groups = {
  59. "Document Management": [
  60. ("documents", "Document ingestion and management commands"),
  61. ("collections", "Collections management commands"),
  62. ],
  63. "Knowledge Graph": [
  64. ("graphs", "Graph creation and management commands"),
  65. ("prompts", "Prompt template management"),
  66. ],
  67. "Interaction": [
  68. ("conversations", "Conversation management commands"),
  69. ("retrieval", "Knowledge retrieval commands"),
  70. ],
  71. "System": [
  72. ("configure", "Configuration management commands"),
  73. ("users", "User management commands"),
  74. ("indices", "Index management commands"),
  75. ("system", "System administration commands"),
  76. ],
  77. "Database": [
  78. ("db", "Database management commands"),
  79. ("upgrade", "Upgrade database schema"),
  80. ("downgrade", "Downgrade database schema"),
  81. ("current", "Show current schema version"),
  82. ("history", "View schema migration history"),
  83. ],
  84. }
  85. for group_name, group_commands in command_groups.items():
  86. table.add_row(
  87. f"[bold yellow]{group_name}[/bold yellow]", "", style="dim"
  88. )
  89. for cmd_name, description in group_commands:
  90. if cmd_name in commands:
  91. table.add_row(f" {cmd_name}", commands[cmd_name].help or "")
  92. table.add_row("", "") # Add spacing between groups
  93. return table
  94. class CustomGroup(click.Group):
  95. def format_help(self, ctx, formatter):
  96. console.print("\n[bold blue]R2R Command Line Interface[/bold blue]")
  97. console.print("The most advanced AI retrieval system\n")
  98. if self.get_help_option(ctx) is not None:
  99. console.print("[bold cyan]Usage:[/bold cyan]")
  100. console.print(" r2r [OPTIONS] COMMAND [ARGS]...\n")
  101. console.print("[bold cyan]Options:[/bold cyan]")
  102. console.print(
  103. " --base-url TEXT Base URL for the API [default: https://api.cloud.sciphi.ai]"
  104. )
  105. console.print(" --help Show this message and exit.\n")
  106. console.print("[bold cyan]Commands:[/bold cyan]")
  107. console.print(custom_help_formatter(self.commands))
  108. console.print(
  109. "\nFor more details on a specific command, run: [bold]r2r COMMAND --help[/bold]\n"
  110. )
  111. class CustomContext(click.Context):
  112. def __init__(self, *args: Any, **kwargs: Any) -> None:
  113. super().__init__(*args, **kwargs)
  114. self.exit_func = types.MethodType(silent_exit, self)
  115. def exit(self, code: int = 0) -> Never:
  116. self.exit_func(code)
  117. raise SystemExit(code)
  118. def initialize_client(base_url: str) -> R2RAsyncClient:
  119. """Initialize R2R client with API key from config if available."""
  120. client = R2RAsyncClient()
  121. try:
  122. config = load_config()
  123. if api_key := config.get("api_key"):
  124. client.set_api_key(api_key)
  125. if not client.api_key:
  126. console.print(
  127. "[yellow]Warning: API key not properly set in client[/yellow]"
  128. )
  129. except Exception as e:
  130. console.print(
  131. "[yellow]Warning: Failed to load API key from config[/yellow]"
  132. )
  133. console.print_exception()
  134. return client
  135. @click.group(cls=CustomGroup)
  136. @click.option(
  137. "--base-url",
  138. default="https://cloud.sciphi.ai",
  139. help="Base URL for the API",
  140. )
  141. @pass_context
  142. async def cli(ctx: click.Context, base_url: str) -> None:
  143. """R2R CLI for all core operations."""
  144. ctx.obj = initialize_client(base_url)