diff --git a/py/cli/__init__.py b/py/cli/__init__.py index bc5458115..e69de29bb 100644 --- a/py/cli/__init__.py +++ b/py/cli/__init__.py @@ -1,18 +0,0 @@ -from .command_group import cli as command_group_cli -from .commands import auth, database, ingestion, management, retrieval, server -from .main import main - -__all__ = [ - # From cli.py - "main", - # From Command Collection - "command_group_cli", - # From Commands - "auth", - "ingestion", - "management", - "kg", - "database", - "retrieval", - "server", -] diff --git a/py/cli/commands/collections.py b/py/cli/commands/collections.py new file mode 100644 index 000000000..c84c5a885 --- /dev/null +++ b/py/cli/commands/collections.py @@ -0,0 +1,141 @@ +import json + +import asyncclick as click +from asyncclick import pass_context + +from r2r import R2RAsyncClient +from cli.utils.timer import timer + + +@click.group() +def collections(): + """Collections commands.""" + pass + + +@collections.command() +@click.argument("name", required=True, type=str) +@click.option("--description", type=str) +@pass_context +async def create(ctx, name, description): + """Create a collection.""" + client: R2RAsyncClient = ctx.obj + + with timer(): + response = await client.collections.create( + name=name, + description=description, + ) + + click.echo(json.dumps(response, indent=2)) + + +@collections.command() +@click.option("--ids", multiple=True, help="Collection IDs to fetch") +@click.option( + "--offset", + default=0, + help="The offset to start from. Defaults to 0.", +) +@click.option( + "--limit", + default=100, + help="The maximum number of nodes to return. Defaults to 100.", +) +@pass_context +async def list(ctx, ids, offset, limit): + """Get an overview of collections.""" + client: R2RAsyncClient = ctx.obj + ids = list(ids) if ids else None + + with timer(): + response = await client.collections.list( + ids=ids, + offset=offset, + limit=limit, + ) + + for user in response["results"]: + click.echo(json.dumps(user, indent=2)) + + +@collections.command() +@click.argument("id", required=True, type=str) +@pass_context +async def retrieve(ctx, id): + """Retrieve a collection by ID.""" + client: R2RAsyncClient = ctx.obj + + with timer(): + response = await client.collections.retrieve(id=id) + + click.echo(json.dumps(response, indent=2)) + + +@collections.command() +@click.argument("id", required=True, type=str) +@pass_context +async def delete(ctx, id): + """Delete a collection by ID.""" + client: R2RAsyncClient = ctx.obj + + with timer(): + response = await client.collections.delete(id=id) + + click.echo(json.dumps(response, indent=2)) + + +@collections.command() +@click.argument("id", required=True, type=str) +@click.option( + "--offset", + default=0, + help="The offset to start from. Defaults to 0.", +) +@click.option( + "--limit", + default=100, + help="The maximum number of nodes to return. Defaults to 100.", +) +@pass_context +async def list_documents(ctx, id, offset, limit): + """Get an overview of collections.""" + client: R2RAsyncClient = ctx.obj + + with timer(): + response = await client.collections.list_documents( + id=id, + offset=offset, + limit=limit, + ) + + for user in response["results"]: + click.echo(json.dumps(user, indent=2)) + + +@collections.command() +@click.argument("id", required=True, type=str) +@click.option( + "--offset", + default=0, + help="The offset to start from. Defaults to 0.", +) +@click.option( + "--limit", + default=100, + help="The maximum number of nodes to return. Defaults to 100.", +) +@pass_context +async def list_users(ctx, id, offset, limit): + """Get an overview of collections.""" + client: R2RAsyncClient = ctx.obj + + with timer(): + response = await client.collections.list_users( + id=id, + offset=offset, + limit=limit, + ) + + for user in response["results"]: + click.echo(json.dumps(user, indent=2)) diff --git a/py/cli/commands/conversations.py b/py/cli/commands/conversations.py new file mode 100644 index 000000000..87c61164f --- /dev/null +++ b/py/cli/commands/conversations.py @@ -0,0 +1,124 @@ +import json + +import asyncclick as click +from asyncclick import pass_context + +from r2r import R2RAsyncClient +from cli.utils.timer import timer + + +@click.group() +def conversations(): + """Conversations commands.""" + pass + + +@conversations.command() +@pass_context +async def create(ctx): + """Create a conversation.""" + client: R2RAsyncClient = ctx.obj + + with timer(): + response = await client.conversations.create() + + click.echo(json.dumps(response, indent=2)) + + +@conversations.command() +@click.option("--ids", multiple=True, help="Conversation IDs to fetch") +@click.option( + "--offset", + default=0, + help="The offset to start from. Defaults to 0.", +) +@click.option( + "--limit", + default=100, + help="The maximum number of nodes to return. Defaults to 100.", +) +@pass_context +async def list(ctx, ids, offset, limit): + """Get an overview of conversations.""" + client: R2RAsyncClient = ctx.obj + ids = list(ids) if ids else None + + with timer(): + response = await client.conversations.list( + ids=ids, + offset=offset, + limit=limit, + ) + + for user in response["results"]: + click.echo(json.dumps(user, indent=2)) + + +@conversations.command() +@click.argument("id", required=True, type=str) +@pass_context +async def retrieve(ctx, id): + """Retrieve a collection by ID.""" + client: R2RAsyncClient = ctx.obj + + with timer(): + response = await client.conversations.retrieve(id=id) + + click.echo(json.dumps(response, indent=2)) + + +@conversations.command() +@click.argument("id", required=True, type=str) +@pass_context +async def delete(ctx, id): + """Delete a collection by ID.""" + client: R2RAsyncClient = ctx.obj + + with timer(): + response = await client.conversations.delete(id=id) + + click.echo(json.dumps(response, indent=2)) + + +@conversations.command() +@click.argument("id", required=True, type=str) +@pass_context +async def list_branches(ctx, id): + """List all branches in a conversation.""" + client: R2RAsyncClient = ctx.obj + + with timer(): + response = await client.conversations.list_branches( + id=id, + ) + + for user in response["results"]: + click.echo(json.dumps(user, indent=2)) + + +@conversations.command() +@click.argument("id", required=True, type=str) +@click.option( + "--offset", + default=0, + help="The offset to start from. Defaults to 0.", +) +@click.option( + "--limit", + default=100, + help="The maximum number of nodes to return. Defaults to 100.", +) +@pass_context +async def list_users(ctx, id, offset, limit): + """Get an overview of collections.""" + client: R2RAsyncClient = ctx.obj + + with timer(): + response = await client.collections.list_users( + id=id, + offset=offset, + limit=limit, + ) + + for user in response["results"]: + click.echo(json.dumps(user, indent=2)) diff --git a/py/cli/commands/documents.py b/py/cli/commands/documents.py new file mode 100644 index 000000000..039c2344e --- /dev/null +++ b/py/cli/commands/documents.py @@ -0,0 +1,196 @@ +import json + +import asyncclick as click +from asyncclick import pass_context + +from r2r import R2RAsyncClient + +from cli.utils.param_types import JSON +from cli.utils.timer import timer + + +@click.group() +def documents(): + """Documents commands.""" + pass + + +@documents.command() +@click.argument( + "file_paths", nargs=-1, required=True, type=click.Path(exists=True) +) +@click.option("--ids", multiple=True, help="Document IDs for ingestion") +@click.option( + "--metadatas", type=JSON, help="Metadatas for ingestion as a JSON string" +) +@click.option( + "--run-without-orchestration", is_flag=True, help="Run with orchestration" +) +@pass_context +async def create(ctx, file_paths, ids, metadatas, run_without_orchestration): + """Ingest files into R2R.""" + client: R2RAsyncClient = ctx.obj + run_with_orchestration = not run_without_orchestration + responses = [] + + for idx, file_path in enumerate(file_paths): + with timer(): + current_id = [ids[idx]] if ids and idx < len(ids) else None + current_metadata = ( + metadatas[idx] if metadatas and idx < len(metadatas) else None + ) + + click.echo( + f"Processing file {idx + 1}/{len(file_paths)}: {file_path}" + ) + response = await client.documents.create( + file_path=file_path, + metadata=current_metadata, + id=current_id, + run_with_orchestration=run_with_orchestration, + ) + responses.append(response) + click.echo(json.dumps(response, indent=2)) + click.echo("-" * 40) + + click.echo(f"\nProcessed {len(responses)} files successfully.") + + +@documents.command() +@click.argument("file_path", required=True, type=click.Path(exists=True)) +@click.option("--id", help="Existing document ID to update") +@click.option( + "--metadata", type=JSON, help="Metadatas for ingestion as a JSON string" +) +@click.option( + "--run-without-orchestration", is_flag=True, help="Run with orchestration" +) +@pass_context +async def update(ctx, file_path, id, metadata, run_without_orchestration): + """Update an existing file in R2R.""" + client: R2RAsyncClient = ctx.obj + run_with_orchestration = not run_without_orchestration + responses = [] + + with timer(): + click.echo(f"Updating file {id}: {file_path}") + response = await client.documents.update( + file_path=file_path, + metadata=metadata, + id=id, + run_with_orchestration=run_with_orchestration, + ) + responses.append(response) + click.echo(json.dumps(response, indent=2)) + click.echo("-" * 40) + + click.echo(f"Updated file {id} file successfully.") + + +@documents.command() +@click.option("--ids", multiple=True, help="Document IDs to fetch") +@click.option( + "--offset", + default=0, + help="The offset to start from. Defaults to 0.", +) +@click.option( + "--limit", + default=100, + help="The maximum number of nodes to return. Defaults to 100.", +) +@pass_context +async def list(ctx, ids, offset, limit): + """Get an overview of documents.""" + client: R2RAsyncClient = ctx.obj + ids = list(ids) if ids else None + + with timer(): + response = await client.documents.list( + ids=ids, + offset=offset, + limit=limit, + ) + + for document in response["results"]: + click.echo(document) + + +@documents.command() +@click.argument("id", required=True, type=str) +@pass_context +async def retrieve(ctx, id): + """Retrieve a document by ID.""" + client: R2RAsyncClient = ctx.obj + + with timer(): + response = await client.documents.retrieve(id=id) + + click.echo(json.dumps(response, indent=2)) + + +@documents.command() +@click.argument("id", required=True, type=str) +@pass_context +async def delete(ctx, id): + """Delete a document by ID.""" + client: R2RAsyncClient = ctx.obj + + with timer(): + response = await client.documents.delete(id=id) + + click.echo(json.dumps(response, indent=2)) + + +@documents.command() +@click.argument("id", required=True, type=str) +@click.option( + "--offset", + default=0, + help="The offset to start from. Defaults to 0.", +) +@click.option( + "--limit", + default=100, + help="The maximum number of nodes to return. Defaults to 100.", +) +@pass_context +async def list_chunks(ctx, id, offset, limit): + """List collections for a specific document.""" + client: R2RAsyncClient = ctx.obj + + with timer(): + response = await client.documents.list_chunks( + id=id, + offset=offset, + limit=limit, + ) + + click.echo(json.dumps(response, indent=2)) + + +@documents.command() +@click.argument("id", required=True, type=str) +@click.option( + "--offset", + default=0, + help="The offset to start from. Defaults to 0.", +) +@click.option( + "--limit", + default=100, + help="The maximum number of nodes to return. Defaults to 100.", +) +@pass_context +async def list_collections(ctx, id, offset, limit): + """List collections for a specific document.""" + client: R2RAsyncClient = ctx.obj + + with timer(): + response = await client.documents.list_collections( + id=id, + offset=offset, + limit=limit, + ) + + click.echo(json.dumps(response, indent=2)) diff --git a/py/cli/commands/indices.py b/py/cli/commands/indices.py new file mode 100644 index 000000000..7f3d5b9f1 --- /dev/null +++ b/py/cli/commands/indices.py @@ -0,0 +1,89 @@ +import json + +import asyncclick as click +from asyncclick import pass_context + +from r2r import R2RAsyncClient +from cli.utils.timer import timer + + +@click.group() +def indices(): + """Indices commands.""" + pass + + +@indices.command() +@click.option( + "--offset", + default=0, + help="The offset to start from. Defaults to 0.", +) +@click.option( + "--limit", + default=100, + help="The maximum number of nodes to return. Defaults to 100.", +) +@pass_context +async def list(ctx, offset, limit): + """Get an overview of indices.""" + client: R2RAsyncClient = ctx.obj + + with timer(): + response = await client.indices.list( + offset=offset, + limit=limit, + ) + + for user in response["results"]: + click.echo(json.dumps(user, indent=2)) + + +@indices.command() +@click.argument("index_name", required=True, type=str) +@click.argument("table_name", required=True, type=str) +@pass_context +async def retrieve(ctx, index_name, table_name): + """Retrieve an index by name.""" + client: R2RAsyncClient = ctx.obj + + with timer(): + response = await client.indices.retrieve( + index_name=index_name, + table_name=table_name, + ) + + click.echo(json.dumps(response, indent=2)) + + +@indices.command() +@click.argument("index_name", required=True, type=str) +@click.argument("table_name", required=True, type=str) +@pass_context +async def delete(ctx, index_name, table_name): + """Delete an index by name.""" + client: R2RAsyncClient = ctx.obj + + with timer(): + response = await client.indices.retrieve( + index_name=index_name, + table_name=table_name, + ) + + click.echo(json.dumps(response, indent=2)) + + +@indices.command() +@click.argument("id", required=True, type=str) +@pass_context +async def list_branches(ctx, id): + """List all branches in a conversation.""" + client: R2RAsyncClient = ctx.obj + + with timer(): + response = await client.indices.list_branches( + id=id, + ) + + for user in response["results"]: + click.echo(json.dumps(user, indent=2)) diff --git a/py/cli/commands/prompts.py b/py/cli/commands/prompts.py new file mode 100644 index 000000000..7c864b6a3 --- /dev/null +++ b/py/cli/commands/prompts.py @@ -0,0 +1,60 @@ +import json + +import asyncclick as click +from asyncclick import pass_context + +from r2r import R2RAsyncClient +from cli.utils.timer import timer + + +@click.group() +def prompts(): + """Prompts commands.""" + pass + + +@prompts.command() +@pass_context +async def list(ctx): + """Get an overview of prompts.""" + client: R2RAsyncClient = ctx.obj + + with timer(): + response = await client.prompts.list() + + for prompt in response["results"]: + click.echo(json.dumps(prompt, indent=2)) + + +@prompts.command() +@click.argument("name", type=str) +@click.option("--inputs", default=None, type=str) +@click.option("--prompt-override", default=None, type=str) +@pass_context +async def retrieve(ctx, name, inputs, prompt_override): + """Retrieve an prompts by name.""" + client: R2RAsyncClient = ctx.obj + + with timer(): + response = await client.prompts.retrieve( + name=name, + inputs=inputs, + prompt_override=prompt_override, + ) + + click.echo(json.dumps(response, indent=2)) + + +@prompts.command() +@click.argument("name", required=True, type=str) +@pass_context +async def delete(ctx, name): + """Delete an index by name.""" + client: R2RAsyncClient = ctx.obj + + with timer(): + response = await client.prompts.delete( + name=name, + ) + + click.echo(json.dumps(response, indent=2)) diff --git a/py/cli/commands/retrieval.py b/py/cli/commands/retrieval.py index f0d5604d4..3defc411f 100644 --- a/py/cli/commands/retrieval.py +++ b/py/cli/commands/retrieval.py @@ -1,12 +1,20 @@ +import json + import asyncclick as click from asyncclick import pass_context -from cli.command_group import cli -from cli.utils.param_types import JSON +from r2r import R2RAsyncClient from cli.utils.timer import timer +from cli.utils.param_types import JSON + + +@click.group() +def retrieval(): + """Retrieval commands.""" + pass -@cli.command() +@retrieval.command() @click.option( "--query", prompt="Enter your search query", help="The search query" ) @@ -64,7 +72,7 @@ @pass_context async def search(ctx, query, **kwargs): """Perform a search query.""" - client = ctx.obj + client: R2RAsyncClient = ctx.obj vector_search_settings = { k: v for k, v in kwargs.items() @@ -98,7 +106,7 @@ async def search(ctx, query, **kwargs): } with timer(): - results = await client.search( + results = await client.retrieval.search( query, vector_search_settings, kg_search_settings, @@ -110,15 +118,15 @@ async def search(ctx, query, **kwargs): if "vector_search_results" in results: click.echo("Vector search results:") for result in results["vector_search_results"]: - click.echo(result) + click.echo(json.dumps(result, indent=2)) if "kg_search_results" in results and results["kg_search_results"]: click.echo("KG search results:") for result in results["kg_search_results"]: - click.echo(result) + click.echo(json.dumps(result, indent=2)) -@cli.command() +@retrieval.command() @click.option("--query", prompt="Enter your query", help="The query for RAG") # RAG Generation Config @click.option("--stream", is_flag=True, help="Stream the RAG response") @@ -171,7 +179,7 @@ async def search(ctx, query, **kwargs): @pass_context async def rag(ctx, query, **kwargs): """Perform a RAG query.""" - client = ctx.obj + client: R2RAsyncClient = ctx.obj rag_generation_config = { "stream": kwargs.get("stream", False), } @@ -216,11 +224,11 @@ async def rag(ctx, query, **kwargs): } with timer(): - response = await client.rag( - query, - rag_generation_config, - vector_search_settings, - kg_search_settings, + response = await client.retrieval.rag( + query=query, + rag_generation_config=rag_generation_config, + vector_search_settings=vector_search_settings, + kg_search_settings=kg_search_settings, ) if rag_generation_config.get("stream"): @@ -228,7 +236,4 @@ async def rag(ctx, query, **kwargs): click.echo(chunk, nl=False) click.echo() else: - click.echo(response) - - -# TODO: Implement agent + click.echo(json.dumps(response, indent=2)) diff --git a/py/cli/commands/users.py b/py/cli/commands/users.py new file mode 100644 index 000000000..9b0c7a7ef --- /dev/null +++ b/py/cli/commands/users.py @@ -0,0 +1,131 @@ +import json + +import asyncclick as click +from asyncclick import pass_context + +from r2r import R2RAsyncClient +from cli.utils.timer import timer + + +@click.group() +def users(): + """Users commands.""" + pass + + +@users.command() +@click.argument("email", required=True, type=str) +@click.argument("password", required=True, type=str) +@pass_context +async def register(ctx, email, password): + """Create a new user.""" + client: R2RAsyncClient = ctx.obj + + with timer(): + response = await client.users.register(email=email, password=password) + + click.echo(json.dumps(response, indent=2)) + + +@users.command() +@click.option("--ids", multiple=True, help="Document IDs to fetch") +@click.option( + "--offset", + default=0, + help="The offset to start from. Defaults to 0.", +) +@click.option( + "--limit", + default=100, + help="The maximum number of nodes to return. Defaults to 100.", +) +@pass_context +async def list(ctx, ids, offset, limit): + """Get an overview of users.""" + client: R2RAsyncClient = ctx.obj + ids = list(ids) if ids else None + + with timer(): + response = await client.users.list( + ids=ids, + offset=offset, + limit=limit, + ) + + for user in response["results"]: + click.echo(json.dumps(user, indent=2)) + + +@users.command() +@click.argument("id", required=True, type=str) +@pass_context +async def retrieve(ctx, id): + """Retrieve a user by ID.""" + client: R2RAsyncClient = ctx.obj + + with timer(): + response = await client.users.retrieve(id=id) + + click.echo(json.dumps(response, indent=2)) + + +@users.command() +@click.argument("id", required=True, type=str) +@click.option( + "--offset", + default=0, + help="The offset to start from. Defaults to 0.", +) +@click.option( + "--limit", + default=100, + help="The maximum number of nodes to return. Defaults to 100.", +) +@pass_context +async def list_collections(ctx, id, offset, limit): + """List collections for a specific user.""" + client: R2RAsyncClient = ctx.obj + + with timer(): + response = await client.users.list_collections( + id=id, + offset=offset, + limit=limit, + ) + + for collection in response["results"]: + click.echo(json.dumps(collection, indent=2)) + + +@users.command() +@click.argument("id", required=True, type=str) +@click.argument("collection_id", required=True, type=str) +@pass_context +async def add_to_collection(ctx, id, collection_id): + """Retrieve a user by ID.""" + client: R2RAsyncClient = ctx.obj + + with timer(): + response = await client.users.add_to_collection( + id=id, + collection_id=collection_id, + ) + + click.echo(json.dumps(response, indent=2)) + + +@users.command() +@click.argument("id", required=True, type=str) +@click.argument("collection_id", required=True, type=str) +@pass_context +async def remove_from_collection(ctx, id, collection_id): + """Retrieve a user by ID.""" + client: R2RAsyncClient = ctx.obj + + with timer(): + response = await client.users.remove_from_collection( + id=id, + collection_id=collection_id, + ) + + click.echo(json.dumps(response, indent=2)) diff --git a/py/cli/commands/auth.py b/py/cli/commands/v2/auth.py similarity index 100% rename from py/cli/commands/auth.py rename to py/cli/commands/v2/auth.py diff --git a/py/cli/commands/ingestion.py b/py/cli/commands/v2/ingestion.py similarity index 94% rename from py/cli/commands/ingestion.py rename to py/cli/commands/v2/ingestion.py index 0d4d3a091..9307cc0a4 100644 --- a/py/cli/commands/ingestion.py +++ b/py/cli/commands/v2/ingestion.py @@ -2,18 +2,20 @@ import os import tempfile import uuid -from urllib.parse import urlparse - import asyncclick as click import requests + +from urllib.parse import urlparse from asyncclick import pass_context -from cli.command_group import cli +from r2r import R2RAsyncClient +from cli.command_group import cli, deprecated_command from cli.utils.param_types import JSON from cli.utils.timer import timer from shared.abstractions import IndexMeasure, IndexMethod, VectorTableName +# TODO async def ingest_files_from_urls(client, urls): """Download and ingest files from given URLs.""" files_to_ingest = [] @@ -71,11 +73,12 @@ async def ingest_files_from_urls(client, urls): "--run-without-orchestration", is_flag=True, help="Run with orchestration" ) @pass_context +@deprecated_command("r2r documents create /path/to/file.txt") async def ingest_files( ctx, file_paths, document_ids, metadatas, run_without_orchestration ): """Ingest files into R2R.""" - client = ctx.obj + client: R2RAsyncClient = ctx.obj with timer(): file_paths = list(file_paths) document_ids = list(document_ids) if document_ids else None @@ -105,11 +108,14 @@ async def ingest_files( "--run-without-orchestration", is_flag=True, help="Run with orchestration" ) @pass_context +@deprecated_command( + "r2r documents update /path/to/file.txt --id=" +) async def update_files( ctx, file_paths, document_ids, metadatas, run_without_orchestration ): """Update existing files in R2R.""" - client = ctx.obj + client: R2RAsyncClient = ctx.obj with timer(): file_paths = list(file_paths) @@ -145,7 +151,7 @@ async def update_files( async def ingest_sample_file(ctx, v2=False, v3=False): """Ingest the first sample file into R2R.""" sample_file_url = f"https://raw.githubusercontent.com/SciPhi-AI/R2R/main/py/core/examples/data/aristotle{'_v2' if v2 else ''}{'_v3' if v3 else ''}.txt" - client = ctx.obj + client: R2RAsyncClient = ctx.obj with timer(): response = await ingest_files_from_urls(client, [sample_file_url]) @@ -158,7 +164,7 @@ async def ingest_sample_file(ctx, v2=False, v3=False): @pass_context async def ingest_sample_files(ctx): """Ingest multiple sample files into R2R.""" - client = ctx.obj + client: R2RAsyncClient = ctx.obj urls = [ "https://raw.githubusercontent.com/SciPhi-AI/R2R/main/py/core/examples/data/pg_essay_3.html", "https://raw.githubusercontent.com/SciPhi-AI/R2R/main/py/core/examples/data/pg_essay_4.html", @@ -182,7 +188,7 @@ async def ingest_sample_files(ctx): @pass_context async def ingest_sample_files_from_unstructured(ctx): """Ingest multiple sample files from URLs into R2R.""" - client = ctx.obj + client: R2RAsyncClient = ctx.obj # Get the absolute path of the current script current_script_path = os.path.abspath(__file__) @@ -250,7 +256,7 @@ async def create_vector_index( no_concurrent, ): """Create a vector index for similarity search.""" - client = ctx.obj + client: R2RAsyncClient = ctx.obj with timer(): response = await client.create_vector_index( table_name=table_name, @@ -274,7 +280,7 @@ async def create_vector_index( @pass_context async def list_vector_indices(ctx, table_name): """List all vector indices for a table.""" - client = ctx.obj + client: R2RAsyncClient = ctx.obj with timer(): response = await client.list_vector_indices(table_name=table_name) click.echo(json.dumps(response, indent=2)) @@ -296,7 +302,7 @@ async def list_vector_indices(ctx, table_name): @pass_context async def delete_vector_index(ctx, index_name, table_name, no_concurrent): """Delete a vector index.""" - client = ctx.obj + client: R2RAsyncClient = ctx.obj with timer(): response = await client.delete_vector_index( index_name=index_name, diff --git a/py/cli/commands/kg.py b/py/cli/commands/v2/kg.py similarity index 95% rename from py/cli/commands/kg.py rename to py/cli/commands/v2/kg.py index cb989dc25..e46c9c86c 100644 --- a/py/cli/commands/kg.py +++ b/py/cli/commands/v2/kg.py @@ -3,10 +3,12 @@ import asyncclick as click from asyncclick import pass_context +from r2r import R2RAsyncClient from cli.command_group import cli from cli.utils.timer import timer +# TODO @cli.command() @click.option( "--collection-id", @@ -33,7 +35,7 @@ async def create_graph( ctx, collection_id, run, kg_creation_settings, force_kg_creation ): - client = ctx.obj + client: R2RAsyncClient = ctx.obj if kg_creation_settings: try: @@ -61,6 +63,7 @@ async def create_graph( click.echo(json.dumps(response, indent=2)) +# TODO @cli.command() @click.option( "--collection-id", @@ -89,7 +92,7 @@ async def deduplicate_entities( """ Deduplicate entities in the knowledge graph. """ - client = ctx.obj + client: R2RAsyncClient = ctx.obj if deduplication_settings: try: @@ -115,6 +118,7 @@ async def deduplicate_entities( click.echo(json.dumps(response, indent=2)) +# TODO @cli.command() @click.option( "--collection-id", @@ -144,7 +148,7 @@ async def enrich_graph( """ Enrich an existing graph. """ - client = ctx.obj + client: R2RAsyncClient = ctx.obj if kg_enrichment_settings: try: @@ -170,6 +174,7 @@ async def enrich_graph( click.echo(json.dumps(response, indent=2)) +# TODO @cli.command() @click.option( "--collection-id", @@ -205,7 +210,7 @@ async def get_entities( """ Retrieve entities from the knowledge graph. """ - client = ctx.obj + client: R2RAsyncClient = ctx.obj with timer(): response = await client.get_entities( @@ -219,6 +224,7 @@ async def get_entities( click.echo(json.dumps(response, indent=2)) +# TODO @cli.command() @click.option( "--collection-id", @@ -254,7 +260,7 @@ async def get_triples( """ Retrieve triples from the knowledge graph. """ - client = ctx.obj + client: R2RAsyncClient = ctx.obj with timer(): response = await client.get_triples( @@ -268,6 +274,7 @@ async def get_triples( click.echo(json.dumps(response, indent=2)) +# TODO @cli.command() @click.option( "--collection-id", @@ -286,7 +293,7 @@ async def delete_graph_for_collection(ctx, collection_id, cascade): NOTE: Setting the cascade flag to true will delete entities and triples for documents that are shared across multiple collections. Do not set this flag unless you are absolutely sure that you want to delete the entities and triples for all documents in the collection. """ - client = ctx.obj + client: R2RAsyncClient = ctx.obj with timer(): response = await client.delete_graph_for_collection( diff --git a/py/cli/commands/management.py b/py/cli/commands/v2/management.py similarity index 92% rename from py/cli/commands/management.py rename to py/cli/commands/v2/management.py index f5ce54499..db9580605 100644 --- a/py/cli/commands/management.py +++ b/py/cli/commands/v2/management.py @@ -3,11 +3,13 @@ import asyncclick as click from asyncclick import pass_context +from r2r import R2RAsyncClient from cli.command_group import cli, deprecated_command from cli.utils.param_types import JSON from cli.utils.timer import timer +# TODO @cli.command() @click.option("--filters", type=JSON, help="Filters for analytics as JSON") @click.option("--analysis-types", type=JSON, help="Analysis types as JSON") @@ -15,7 +17,7 @@ async def analytics( ctx, filters: dict[str, Any], analysis_types: dict[str, Any] ): - client = ctx.obj + client: R2RAsyncClient = ctx.obj """Retrieve analytics data.""" with timer(): response = await client.analytics(filters, analysis_types) @@ -23,11 +25,12 @@ async def analytics( click.echo(response) +# TODO @cli.command() @pass_context async def app_settings(ctx): """Retrieve application settings.""" - client = ctx.obj + client: R2RAsyncClient = ctx.obj with timer(): response = await client.app_settings() @@ -47,9 +50,10 @@ async def app_settings(ctx): help="The maximum number of nodes to return. Defaults to 100.", ) @pass_context +@deprecated_command("r2r users list") async def users_overview(ctx, user_ids, offset, limit): """Get an overview of users.""" - client = ctx.obj + client: R2RAsyncClient = ctx.obj user_ids = list(user_ids) if user_ids else None with timer(): @@ -76,9 +80,10 @@ async def users_overview(ctx, user_ids, offset, limit): help="Filters for deletion in the format key:operator:value", ) @pass_context +@deprecated_command("r2r delete ") async def delete(ctx, filter): """Delete documents based on filters.""" - client = ctx.obj + client: R2RAsyncClient = ctx.obj filters = {} for f in filter: key, operator, value = f.split(":", 2) @@ -105,9 +110,10 @@ async def delete(ctx, filter): help="The maximum number of nodes to return. Defaults to 100.", ) @pass_context +@deprecated_command("r2r documents list") async def documents_overview(ctx, document_ids, offset, limit): """Get an overview of documents.""" - client = ctx.obj + client: R2RAsyncClient = ctx.obj document_ids = list(document_ids) if document_ids else None with timer(): @@ -136,11 +142,12 @@ async def documents_overview(ctx, document_ids, offset, limit): help="Should the vector be included in the response chunks", ) @pass_context +@deprecated_command("r2r documents list-chunks ") async def list_document_chunks( ctx, document_id, offset, limit, include_vectors ): """Get chunks of a specific document.""" - client = ctx.obj + client: R2RAsyncClient = ctx.obj if not document_id: click.echo("Error: Document ID is required.") return @@ -191,7 +198,7 @@ async def list_document_chunks( @deprecated_command("document_chunks") async def document_chunks(ctx, document_id, offset, limit, include_vectors): """Get chunks of a specific document.""" - client = ctx.obj + client: R2RAsyncClient = ctx.obj if not document_id: click.echo("Error: Document ID is required.") return diff --git a/py/cli/commands/v2/retrieval.py b/py/cli/commands/v2/retrieval.py new file mode 100644 index 000000000..a64e4b796 --- /dev/null +++ b/py/cli/commands/v2/retrieval.py @@ -0,0 +1,237 @@ +import asyncclick as click +from asyncclick import pass_context + +from r2r import R2RAsyncClient +from cli.command_group import cli, deprecated_command +from cli.utils.param_types import JSON +from cli.utils.timer import timer + + +@cli.command() +@click.option( + "--query", prompt="Enter your search query", help="The search query" +) +# SearchSettings +@click.option( + "--use-vector-search", + is_flag=True, + default=True, + help="Whether to use vector search", +) +@click.option( + "--filters", + type=JSON, + help="""Filters to apply to the vector search as a JSON, e.g. --filters='{"document_id":{"$in":["9fbe403b-c11c-5aae-8ade-ef22980c3ad1", "3e157b3a-8469-51db-90d9-52e7d896b49b"]}}'""", +) +@click.option( + "--search-limit", default=None, help="Number of search results to return" +) +@click.option( + "--use-hybrid-search", is_flag=True, help="Perform hybrid search" +) +@click.option( + "--selected-collection-ids", + type=JSON, + help="Collection IDs to search for as a JSON", +) +# KGSearchSettings +@click.option( + "--use-kg-search", is_flag=True, help="Use knowledge graph search" +) +@click.option("--kg-search-type", default=None, help="Local or Global") +@click.option("--kg-search-level", default=None, help="Level of KG search") +@click.option( + "--kg-search-generation-config", + type=JSON, + help="KG search generation config", +) +@click.option( + "--entity-types", type=JSON, help="Entity types to search for as a JSON" +) +@click.option( + "--relationships", type=JSON, help="Relationships to search for as a JSON" +) +@click.option( + "--max-community-description-length", + type=JSON, + help="Max community description length", +) +@click.option( + "--search-strategy", + type=str, + help="Vanilla search or complex search method like query fusion or HyDE.", +) +@click.option("--local-search-limits", type=JSON, help="Local search limits") +@pass_context +@deprecated_command("r2r retrieval search") +async def search(ctx, query, **kwargs): + """Perform a search query.""" + client: R2RAsyncClient = ctx.obj + vector_search_settings = { + k: v + for k, v in kwargs.items() + if k + in [ + "use_vector_search", + "filters", + "search_limit", + "use_hybrid_search", + "selected_collection_ids", + "search_strategy", + ] + and v is not None + } + + kg_search_settings = { + k: v + for k, v in kwargs.items() + if k + in [ + "use_kg_search", + "kg_search_type", + "kg_search_level", + "generation_config", + "entity_types", + "relationships", + "max_community_description_length", + "local_search_limits", + ] + and v is not None + } + + with timer(): + results = await client.search( + query, + vector_search_settings, + kg_search_settings, + ) + + if isinstance(results, dict) and "results" in results: + results = results["results"] + + if "vector_search_results" in results: + click.echo("Vector search results:") + for result in results["vector_search_results"]: + click.echo(result) + + if "kg_search_results" in results and results["kg_search_results"]: + click.echo("KG search results:") + for result in results["kg_search_results"]: + click.echo(result) + + +@cli.command() +@click.option("--query", prompt="Enter your query", help="The query for RAG") +# RAG Generation Config +@click.option("--stream", is_flag=True, help="Stream the RAG response") +@click.option("--rag-model", default=None, help="Model for RAG") +# Vector Search Settings +@click.option( + "--use-vector-search", is_flag=True, default=True, help="Use vector search" +) +@click.option("--filters", type=JSON, help="Search filters as JSON") +@click.option( + "--search-limit", default=10, help="Number of search results to return" +) +@click.option( + "--use-hybrid-search", is_flag=True, help="Perform hybrid search" +) +@click.option( + "--selected-collection-ids", + type=JSON, + help="Collection IDs to search for as a JSON", +) +# KG Search Settings +@click.option( + "--use-kg-search", is_flag=True, help="Use knowledge graph search" +) +@click.option("--kg-search-type", default="local", help="Local or Global") +@click.option( + "--kg-search-level", + default=None, + help="Level of cluster to use for Global KG search", +) +@click.option("--kg-search-model", default=None, help="Model for KG agent") +@click.option( + "--entity-types", type=JSON, help="Entity types to search for as a JSON" +) +@click.option( + "--relationships", type=JSON, help="Relationships to search for as a JSON" +) +@click.option( + "--max-community-description-length", + type=int, + help="Max community description length", +) +@click.option( + "--search-strategy", + type=str, + default="vanilla", + help="Vanilla RAG or complex method like query fusion or HyDE.", +) +@click.option("--local-search-limits", type=JSON, help="Local search limits") +@deprecated_command("r2r retrieval rag") +@pass_context +async def rag(ctx, query, **kwargs): + """Perform a RAG query.""" + client: R2RAsyncClient = ctx.obj + rag_generation_config = { + "stream": kwargs.get("stream", False), + } + if kwargs.get("rag_model"): + rag_generation_config["model"] = kwargs["rag_model"] + + vector_search_settings = { + k: v + for k, v in kwargs.items() + if k + in [ + "use_vector_search", + "filters", + "search_limit", + "use_hybrid_search", + "selected_collection_ids", + "search_strategy", + ] + and v is not None + } + + kg_search_settings = { + k: v + for k, v in kwargs.items() + if k + in [ + "use_kg_search", + "kg_search_type", + "kg_search_level", + "kg_search_model", + "entity_types", + "relationships", + "max_community_description_length", + "local_search_limits", + ] + and v is not None + } + + if kg_search_settings.get("kg_search_model"): + kg_search_settings["generation_config"] = { + "model": kg_search_settings.pop("kg_search_model") + } + + with timer(): + response = await client.rag( + query, + rag_generation_config, + vector_search_settings, + kg_search_settings, + ) + + if rag_generation_config.get("stream"): + async for chunk in response: + click.echo(chunk, nl=False) + click.echo() + else: + click.echo(response) + + +# TODO: Implement agent diff --git a/py/cli/commands/server.py b/py/cli/commands/v2/server.py similarity index 98% rename from py/cli/commands/server.py rename to py/cli/commands/v2/server.py index 3d3a9e52a..f7a97fac6 100644 --- a/py/cli/commands/server.py +++ b/py/cli/commands/v2/server.py @@ -9,6 +9,7 @@ from asyncclick import pass_context from dotenv import load_dotenv +from r2r import R2RAsyncClient from cli.command_group import cli from cli.utils.docker_utils import ( bring_down_docker_compose, @@ -20,21 +21,23 @@ from cli.utils.timer import timer +# TODO @cli.command() @pass_context async def health(ctx): """Check the health of the server.""" - client = ctx.obj + client: R2RAsyncClient = ctx.obj with timer(): response = await client.health() click.echo(response) +# TODO @cli.command() @pass_context async def server_stats(ctx): - client = ctx.obj + client: R2RAsyncClient = ctx.obj """Check the server stats.""" with timer(): response = await client.server_stats() @@ -42,6 +45,7 @@ async def server_stats(ctx): click.echo(response) +# TODO @cli.command() @click.option( "--offset", default=None, help="Pagination offset. Default is None." @@ -53,7 +57,7 @@ async def server_stats(ctx): @pass_context async def logs(ctx, run_type_filter, offset, limit): """Retrieve logs with optional type filter.""" - client = ctx.obj + client: R2RAsyncClient = ctx.obj with timer(): response = await client.logs( offset=offset, limit=limit, run_type_filter=run_type_filter @@ -72,6 +76,7 @@ async def logs(ctx, run_type_filter, offset, limit): click.echo(f"Total runs: {len(response['results'])}") +# TODO @cli.command() @click.option( "--volumes", @@ -122,6 +127,7 @@ def docker_down(volumes, remove_orphans, project_name): remove_r2r_network() +# TODO @cli.command() def generate_report(): """Generate a system report including R2R version, Docker info, and OS details.""" @@ -201,6 +207,7 @@ def generate_report(): click.echo(json.dumps(report, indent=2)) +# TODO @cli.command() @click.option("--host", default=None, help="Host to run the server on") @click.option( @@ -381,6 +388,7 @@ def image_exists(img): await run_local_serve(host, port, config_name, config_path, full) +# TODO @cli.command() def update(): """Update the R2R package to the latest version.""" @@ -400,6 +408,7 @@ def update(): click.echo(f"An unexpected error occurred: {e}") +# TODO @cli.command() def version(): """Print the version of R2R.""" diff --git a/py/cli/main.py b/py/cli/main.py index 6fcac40c0..39c662781 100644 --- a/py/cli/main.py +++ b/py/cli/main.py @@ -1,12 +1,22 @@ from cli.command_group import cli -from cli.commands import ( +from cli.commands.v2 import ( auth, - database, ingestion, kg, management, - retrieval, server, + retrieval as v2_retrieval, +) +from cli.commands import ( + database, + # V3 methods + collections, + conversations, + documents, + indices, + prompts, + retrieval, + users, ) from cli.utils.telemetry import posthog, telemetry @@ -15,45 +25,69 @@ def add_command_with_telemetry(command): cli.add_command(telemetry(command)) +# Chunks +add_command_with_telemetry(collections.collections) +add_command_with_telemetry(conversations.conversations) +add_command_with_telemetry(documents.documents) +# Graph +add_command_with_telemetry(indices.indices) +add_command_with_telemetry(prompts.prompts) +add_command_with_telemetry(retrieval.retrieval) +add_command_with_telemetry(users.users) + +# v2 commands # Auth +# TODO: Remove, this is garbage add_command_with_telemetry(auth.generate_private_key) # Ingestion -add_command_with_telemetry(ingestion.ingest_files) -add_command_with_telemetry(ingestion.update_files) -add_command_with_telemetry(ingestion.ingest_sample_file) -add_command_with_telemetry(ingestion.ingest_sample_files) -add_command_with_telemetry(ingestion.ingest_sample_files_from_unstructured) +add_command_with_telemetry(ingestion.ingest_files) # Deprecated +add_command_with_telemetry(ingestion.update_files) # Deprecated +add_command_with_telemetry( + ingestion.ingest_sample_file +) # TODO: migrate to new schema +add_command_with_telemetry( + ingestion.ingest_sample_files +) # TODO: migrate to new schema +add_command_with_telemetry( + ingestion.ingest_sample_files_from_unstructured +) # TODO: migrate to new schema # Management -add_command_with_telemetry(management.analytics) -add_command_with_telemetry(management.app_settings) -add_command_with_telemetry(management.users_overview) -add_command_with_telemetry(management.documents_overview) -add_command_with_telemetry(management.list_document_chunks) -add_command_with_telemetry(management.document_chunks) +add_command_with_telemetry(management.analytics) # TODO: migrate to new schema +add_command_with_telemetry( + management.app_settings +) # TODO: migrate to new schema +add_command_with_telemetry(management.users_overview) # Deprecated +add_command_with_telemetry(management.documents_overview) # Deprecated +add_command_with_telemetry(management.list_document_chunks) # Deprecated +add_command_with_telemetry(management.document_chunks) # Deprecated # Knowledge Graph -add_command_with_telemetry(kg.create_graph) -add_command_with_telemetry(kg.enrich_graph) -add_command_with_telemetry(kg.deduplicate_entities) +add_command_with_telemetry(kg.create_graph) # TODO: migrate to new schema +add_command_with_telemetry(kg.enrich_graph) # TODO: migrate to new schema +add_command_with_telemetry( + kg.deduplicate_entities +) # TODO: migrate to new schema # Retrieval -add_command_with_telemetry(retrieval.search) -add_command_with_telemetry(retrieval.rag) +add_command_with_telemetry(v2_retrieval.search) # TODO: migrate to new schema +add_command_with_telemetry(v2_retrieval.rag) # TODO: migrate to new schema # Server -add_command_with_telemetry(server.health) -add_command_with_telemetry(server.server_stats) -add_command_with_telemetry(server.logs) -add_command_with_telemetry(server.docker_down) -add_command_with_telemetry(server.generate_report) -add_command_with_telemetry(server.serve) -add_command_with_telemetry(server.update) -add_command_with_telemetry(server.version) +add_command_with_telemetry(server.health) # TODO: migrate to new schema +add_command_with_telemetry(server.server_stats) # TODO: migrate to new schema +add_command_with_telemetry(server.logs) # TODO: migrate to new schema +add_command_with_telemetry(server.docker_down) # TODO: migrate to new schema +add_command_with_telemetry( + server.generate_report +) # TODO: migrate to new schema +add_command_with_telemetry(server.serve) # TODO: migrate to new schema +add_command_with_telemetry(server.update) # TODO: migrate to new schema +add_command_with_telemetry(server.version) # TODO: migrate to new schema # Database -add_command_with_telemetry(database.db) # Add the main db group +add_command_with_telemetry(database.db) add_command_with_telemetry(database.upgrade) add_command_with_telemetry(database.downgrade) add_command_with_telemetry(database.current) diff --git a/py/core/main/api/v3/collections_router.py b/py/core/main/api/v3/collections_router.py index f5f258a91..3d2fa885e 100644 --- a/py/core/main/api/v3/collections_router.py +++ b/py/core/main/api/v3/collections_router.py @@ -77,6 +77,14 @@ def _setup_routes(self): """ ), }, + { + "lang": "CLI", + "source": textwrap.dedent( + """ + r2r collections create "My New Collection" --description="This is a sample collection" + """ + ), + }, { "lang": "cURL", "source": textwrap.dedent( @@ -153,6 +161,14 @@ async def create_collection( """ ), }, + { + "lang": "CLI", + "source": textwrap.dedent( + """ + r2r collections list + """ + ), + }, { "lang": "cURL", "source": textwrap.dedent( @@ -250,6 +266,14 @@ async def list_collections( """ ), }, + { + "lang": "CLI", + "source": textwrap.dedent( + """ + r2r collections retrieve 123e4567-e89b-12d3-a456-426614174000 + """ + ), + }, { "lang": "cURL", "source": textwrap.dedent( @@ -417,6 +441,14 @@ async def update_collection( """ ), }, + { + "lang": "CLI", + "source": textwrap.dedent( + """ + r2r collections delete 123e4567-e89b-12d3-a456-426614174000 + """ + ), + }, { "lang": "cURL", "source": textwrap.dedent( @@ -569,6 +601,14 @@ async def add_document_to_collection( """ ), }, + { + "lang": "CLI", + "source": textwrap.dedent( + """ + r2r collections list-documents 123e4567-e89b-12d3-a456-426614174000 + """ + ), + }, { "lang": "cURL", "source": textwrap.dedent( @@ -747,6 +787,14 @@ async def remove_document_from_collection( """ ), }, + { + "lang": "CLI", + "source": textwrap.dedent( + """ + r2r collections list-users 123e4567-e89b-12d3-a456-426614174000 + """ + ), + }, { "lang": "cURL", "source": textwrap.dedent( diff --git a/py/core/main/api/v3/conversations_router.py b/py/core/main/api/v3/conversations_router.py index 684b0bb59..04d517e28 100644 --- a/py/core/main/api/v3/conversations_router.py +++ b/py/core/main/api/v3/conversations_router.py @@ -72,6 +72,14 @@ def _setup_routes(self): """ ), }, + { + "lang": "CLI", + "source": textwrap.dedent( + """ + r2r conversations create + """ + ), + }, { "lang": "cURL", "source": textwrap.dedent( @@ -132,6 +140,14 @@ async def create_conversation( """ ), }, + { + "lang": "CLI", + "source": textwrap.dedent( + """ + r2r conversations list + """ + ), + }, { "lang": "cURL", "source": textwrap.dedent( @@ -198,8 +214,7 @@ async def list_conversations( # when using auth, do client.login(...) result = client.conversations.get( - "123e4567-e89b-12d3-a456-426614174000", - branch_id="branch_1" + "123e4567-e89b-12d3-a456-426614174000" ) """ ), @@ -215,7 +230,6 @@ async def list_conversations( function main() { const response = await client.conversations.retrieve({ id: "123e4567-e89b-12d3-a456-426614174000", - branch_id: "branch_1" }); } @@ -223,6 +237,14 @@ async def list_conversations( """ ), }, + { + "lang": "CLI", + "source": textwrap.dedent( + """ + r2r conversations retrieve 123e4567-e89b-12d3-a456-426614174000 + """ + ), + }, { "lang": "cURL", "source": textwrap.dedent( @@ -291,6 +313,14 @@ async def get_conversation( """ ), }, + { + "lang": "CLI", + "source": textwrap.dedent( + """ + r2r conversations delete 123e4567-e89b-12d3-a456-426614174000 + """ + ), + }, { "lang": "cURL", "source": textwrap.dedent( @@ -528,6 +558,14 @@ async def update_message( """ ), }, + { + "lang": "CLI", + "source": textwrap.dedent( + """ + r2r conversations list-branches 123e4567-e89b-12d3-a456-426614174000 + """ + ), + }, { "lang": "cURL", "source": textwrap.dedent( diff --git a/py/core/main/api/v3/documents_router.py b/py/core/main/api/v3/documents_router.py index 49f473382..91dd8c070 100644 --- a/py/core/main/api/v3/documents_router.py +++ b/py/core/main/api/v3/documents_router.py @@ -88,6 +88,14 @@ def _setup_routes(self): """ ), }, + { + "lang": "CLI", + "source": textwrap.dedent( + """ + r2r documents create /path/to/file.txt + """ + ), + }, { "lang": "cURL", "source": textwrap.dedent( @@ -267,6 +275,7 @@ async def create_document( function main() { const response = await client.documents.update({ file: { path: "pg_essay_1.html", name: "pg_essay_1.html" }, + id: "9fbe403b-c11c-5aae-8ade-ef22980c3ad1", }); } @@ -274,6 +283,14 @@ async def create_document( """ ), }, + { + "lang": "CLI", + "source": textwrap.dedent( + """ + r2r documents update /path/to/file.txt --id=9fbe403b-c11c-5aae-8ade-ef22980c3ad1 + """ + ), + }, { "lang": "cURL", "source": textwrap.dedent( @@ -479,6 +496,14 @@ async def update_document( """ ), }, + { + "lang": "CLI", + "source": textwrap.dedent( + """ + r2r documents create /path/to/file.txt + """ + ), + }, { "lang": "cURL", "source": textwrap.dedent( @@ -583,6 +608,14 @@ async def get_documents( """ ), }, + { + "lang": "CLI", + "source": textwrap.dedent( + """ + r2r documents retrieve 9fbe403b-c11c-5aae-8ade-ef22980c3ad1 + """ + ), + }, { "lang": "cURL", "source": textwrap.dedent( @@ -672,6 +705,14 @@ async def get_document( """ ), }, + { + "lang": "CLI", + "source": textwrap.dedent( + """ + r2r documents list-chunks 9fbe403b-c11c-5aae-8ade-ef22980c3ad1 + """ + ), + }, { "lang": "cURL", "source": textwrap.dedent( @@ -732,8 +773,10 @@ async def list_chunks( ) == str(auth_user.id) document_collections = await self.services[ "management" - ].get_collections_overview( - offset=0, limit=-1, filter_document_ids=[id] + ].collections_overview( + offset=0, + limit=-1, + document_ids=[id], ) user_has_access = ( @@ -897,6 +940,14 @@ async def file_stream(): """ ), }, + { + "lang": "CLI", + "source": textwrap.dedent( + """ + r2r documents delete 9fbe403b-c11c-5aae-8ade-ef22980c3ad1 + """ + ), + }, { "lang": "cURL", "source": textwrap.dedent( @@ -1032,6 +1083,14 @@ async def delete_document_by_filter( """ ), }, + { + "lang": "CLI", + "source": textwrap.dedent( + """ + r2r documents list-collections 9fbe403b-c11c-5aae-8ade-ef22980c3ad1 + """ + ), + }, { "lang": "cURL", "source": textwrap.dedent( @@ -1080,10 +1139,10 @@ async def get_document_collections( collections_response = await self.services[ "management" - ].get_collections_overview( + ].collections_overview( offset=offset, limit=limit, - filter_document_ids=[UUID(id)], # Convert string ID to UUID + document_ids=[UUID(id)], # Convert string ID to UUID ) return collections_response["results"], { # type: ignore diff --git a/py/core/main/api/v3/indices_router.py b/py/core/main/api/v3/indices_router.py index ad095b8f4..e16a10ad7 100644 --- a/py/core/main/api/v3/indices_router.py +++ b/py/core/main/api/v3/indices_router.py @@ -291,6 +291,14 @@ async def create_index( """ ), }, + { + "lang": "CLI", + "source": textwrap.dedent( + """ + r2r indices list + """ + ), + }, { "lang": "Shell", "source": textwrap.dedent( @@ -311,7 +319,7 @@ async def create_index( ) @self.base_endpoint async def list_indices( - filters: Optional[dict] = Depends(), + filters: list[str] = Query([]), offset: int = Query( 0, ge=0, @@ -388,6 +396,14 @@ async def list_indices( """ ), }, + { + "lang": "CLI", + "source": textwrap.dedent( + """ + r2r indices retrieve index_1 vectors + """ + ), + }, { "lang": "Shell", "source": textwrap.dedent( @@ -540,6 +556,14 @@ async def get_index( """ ), }, + { + "lang": "CLI", + "source": textwrap.dedent( + """ + r2r indices delete index_1 vectors + """ + ), + }, { "lang": "Shell", "source": textwrap.dedent( diff --git a/py/core/main/api/v3/prompts_router.py b/py/core/main/api/v3/prompts_router.py index 70d631eca..99c0e5933 100644 --- a/py/core/main/api/v3/prompts_router.py +++ b/py/core/main/api/v3/prompts_router.py @@ -150,6 +150,14 @@ async def create_prompt( """ ), }, + { + "lang": "CLI", + "source": textwrap.dedent( + """ + r2r prompts list + """ + ), + }, { "lang": "cURL", "source": textwrap.dedent( @@ -229,6 +237,14 @@ async def get_prompts( """ ), }, + { + "lang": "CLI", + "source": textwrap.dedent( + """ + r2r prompts retrieve greeting_prompt --inputs '{"name": "John"}' --prompt-override "Hi, {name}!" + """ + ), + }, { "lang": "cURL", "source": textwrap.dedent( @@ -387,6 +403,14 @@ async def update_prompt( """ ), }, + { + "lang": "CLI", + "source": textwrap.dedent( + """ + r2r prompts delete greeting_prompt + """ + ), + }, { "lang": "cURL", "source": textwrap.dedent( diff --git a/py/core/main/api/v3/retrieval_router.py b/py/core/main/api/v3/retrieval_router.py index 2c6827387..cb1c103e5 100644 --- a/py/core/main/api/v3/retrieval_router.py +++ b/py/core/main/api/v3/retrieval_router.py @@ -130,6 +130,14 @@ def _setup_routes(self): """ ), }, + { + "lang": "CLI", + "source": textwrap.dedent( + """ + r2r retrieval search --query "Who is Aristotle?" + """ + ), + }, { "lang": "Shell", "source": textwrap.dedent( @@ -248,6 +256,14 @@ async def search_app( """ ), }, + { + "lang": "CLI", + "source": textwrap.dedent( + """ + r2r retrieval search --query "Who is Aristotle?" --stream + """ + ), + }, { "lang": "Shell", "source": textwrap.dedent( diff --git a/py/core/main/api/v3/users_router.py b/py/core/main/api/v3/users_router.py index 81e89aba7..c4c1ad9d9 100644 --- a/py/core/main/api/v3/users_router.py +++ b/py/core/main/api/v3/users_router.py @@ -1,6 +1,4 @@ -import logging import textwrap -from typing import Optional from uuid import UUID from fastapi import Body, Depends, Path, Query @@ -22,7 +20,6 @@ from .base_router import BaseRouterV3 -logger = logging.getLogger() oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") @@ -72,6 +69,14 @@ def _setup_routes(self): """ ), }, + { + "lang": "CLI", + "source": textwrap.dedent( + """ + r2r users register jane.doe@example.com secure_password123 + """ + ), + }, { "lang": "cURL", "source": textwrap.dedent( @@ -552,6 +557,14 @@ async def reset_password( """ ), }, + { + "lang": "CLI", + "source": textwrap.dedent( + """ + r2r users list + """ + ), + }, { "lang": "Shell", "source": textwrap.dedent( @@ -574,8 +587,8 @@ async def list_users( # is_active: Optional[bool] = Query(None, example=True), # is_superuser: Optional[bool] = Query(None, example=False), # auth_user=Depends(self.providers.auth.auth_wrapper), - user_ids: Optional[list[UUID]] = Query( - None, description="List of user IDs to filter by" + ids: list[str] = Query( + [], description="List of user IDs to filter by" ), offset: int = Query( 0, @@ -601,9 +614,11 @@ async def list_users( 403, ) + user_uuids = [UUID(user_id) for user_id in ids] + users_overview_response = await self.services[ "management" - ].users_overview(user_ids=user_ids, offset=offset, limit=limit) + ].users_overview(user_ids=user_uuids, offset=offset, limit=limit) return users_overview_response["results"], { # type: ignore "total_entries": users_overview_response["total_entries"] } @@ -647,6 +662,14 @@ async def list_users( """ ), }, + { + "lang": "CLI", + "source": textwrap.dedent( + """ + r2r users retrieve b4ac4dd6-5f27-596e-a55b-7cf242ca30aa + """ + ), + }, { "lang": "Shell", "source": textwrap.dedent( @@ -729,6 +752,14 @@ async def get_user( """ ), }, + { + "lang": "CLI", + "source": textwrap.dedent( + """ + r2r users list-collections 550e8400-e29b-41d4-a716-446655440000 + """ + ), + }, { "lang": "Shell", "source": textwrap.dedent( @@ -821,6 +852,14 @@ async def get_user_collections( """ ), }, + { + "lang": "CLI", + "source": textwrap.dedent( + """ + r2r users add-to-collection 550e8400-e29b-41d4-a716-446655440000 750e8400-e29b-41d4-a716-446655440000 + """ + ), + }, { "lang": "Shell", "source": textwrap.dedent( @@ -896,6 +935,14 @@ async def add_user_to_collection( """ ), }, + { + "lang": "CLI", + "source": textwrap.dedent( + """ + r2r users remove-from-collection 550e8400-e29b-41d4-a716-446655440000 750e8400-e29b-41d4-a716-446655440000 + """ + ), + }, { "lang": "Shell", "source": textwrap.dedent( diff --git a/py/sdk/base/base_client.py b/py/sdk/base/base_client.py index 0d39e09e3..320188f6e 100644 --- a/py/sdk/base/base_client.py +++ b/py/sdk/base/base_client.py @@ -58,7 +58,7 @@ def _ensure_authenticated(self): ) def _get_full_url(self, endpoint: str, version: str = "v2") -> str: - return f"{self.base_url}{version}/{endpoint}" + return f"{self.base_url}/{version}/{endpoint}" def _prepare_request_args(self, endpoint: str, **kwargs) -> dict: headers = kwargs.pop("headers", {}) diff --git a/py/sdk/v3/collections.py b/py/sdk/v3/collections.py index f6e860ddd..902af2d64 100644 --- a/py/sdk/v3/collections.py +++ b/py/sdk/v3/collections.py @@ -4,7 +4,7 @@ class CollectionsSDK: def __init__(self, client): - self._client = client + self.client = client async def create( self, @@ -22,7 +22,7 @@ async def create( dict: Created collection information """ data = {"name": name, "description": description} - return await self._make_request( # type: ignore + return await self.client._make_request( "POST", "collections", json=data, # {"config": data} @@ -53,7 +53,7 @@ async def list( if ids: params["ids"] = ids - return await self._make_request( # type: ignore + return await self.client._make_request( "GET", "collections", params=params, version="v3" ) @@ -70,7 +70,9 @@ async def retrieve( Returns: dict: Detailed collection information """ - return await self._make_request("GET", f"collections/{str(id)}", version="v3") # type: ignore + return await self.client._make_request( + "GET", f"collections/{str(id)}", version="v3" + ) async def update( self, @@ -95,7 +97,7 @@ async def update( if description is not None: data["description"] = description - return await self._make_request( # type: ignore + return await self.client._make_request( "POST", f"collections/{str(id)}", json=data, # {"config": data} @@ -115,7 +117,7 @@ async def delete( Returns: bool: True if deletion was successful """ - result = await self._make_request( # type: ignore + result = await self.client._make_request( "DELETE", f"collections/{str(id)}", version="v3" ) return result.get("results", True) @@ -142,7 +144,7 @@ async def list_documents( "limit": limit, } - return await self._make_request( # type: ignore + return await self.client._make_request( "GET", f"collections/{str(id)}/documents", params=params, @@ -164,7 +166,7 @@ async def add_document( Returns: dict: Result of the operation """ - return await self._make_request( # type: ignore + return await self.client._make_request( "POST", f"collections/{str(id)}/documents/{str(document_id)}", version="v3", @@ -185,7 +187,7 @@ async def remove_document( Returns: bool: True if removal was successful """ - result = await self._make_request( # type: ignore + result = await self.client._make_request( "DELETE", f"collections/{str(id)}/documents/{str(document_id)}", version="v3", @@ -214,7 +216,7 @@ async def list_users( "limit": limit, } - return await self._make_request( # type: ignore + return await self.client._make_request( "GET", f"collections/{str(id)}/users", params=params, version="v3" ) @@ -233,7 +235,7 @@ async def add_user( Returns: dict: Result of the operation """ - return await self._make_request( # type: ignore + return await self.client._make_request( "POST", f"collections/{str(id)}/users/{str(user_id)}", version="v3" ) @@ -252,7 +254,7 @@ async def remove_user( Returns: bool: True if removal was successful """ - result = await self._make_request( # type: ignore + result = await self.client._make_request( "DELETE", f"collections/{str(id)}/users/{str(user_id)}", version="v3", diff --git a/py/sdk/v3/documents.py b/py/sdk/v3/documents.py index 2364212b2..d96aab52d 100644 --- a/py/sdk/v3/documents.py +++ b/py/sdk/v3/documents.py @@ -65,7 +65,10 @@ async def create( else: data["content"] = content # type: ignore return await self.client._make_request( - "POST", "documents", data=data + "POST", + "documents", + data=data, + version="v3", ) async def update( @@ -130,7 +133,10 @@ async def update( else: data["content"] = content # type: ignore return await self.client._make_request( - "POST", f"documents/{str(id)}", data=data + "POST", + f"documents/{str(id)}", + data=data, + version="v3", ) async def retrieve( @@ -146,7 +152,11 @@ async def retrieve( Returns: dict: Document information """ - return await self.client._make_request("GET", f"documents/{str(id)}") + return await self.client._make_request( + "GET", + f"documents/{str(id)}", + version="v3", + ) async def list( self, diff --git a/py/sdk/v3/prompts.py b/py/sdk/v3/prompts.py index 76285ccf5..8c71a0f28 100644 --- a/py/sdk/v3/prompts.py +++ b/py/sdk/v3/prompts.py @@ -4,7 +4,7 @@ class PromptsSDK: def __init__(self, client): - self._client = client + self.client = client async def create( self, name: str, template: str, input_types: dict @@ -19,7 +19,7 @@ async def create( dict: Created prompt information """ data = {"name": name, "template": template, "input_types": input_types} - return await self._make_request( + return await self.client._make_request( "POST", "prompts", json=data, @@ -32,7 +32,7 @@ async def list(self) -> dict: Returns: dict: List of all available prompts """ - return await self._make_request( + return await self.client._make_request( "GET", "prompts", version="v3", @@ -58,7 +58,7 @@ async def retrieve( params["inputs"] = json.dumps(inputs) if prompt_override: params["prompt_override"] = prompt_override - return await self._make_request( + return await self.client._make_request( "POST", f"prompts/{name}", params=params, @@ -85,7 +85,7 @@ async def update( data["template"] = template if input_types: data["input_types"] = json.dumps(input_types) - return await self._make_request( + return await self.client._make_request( "PUT", f"prompts/{name}", json=data, @@ -100,7 +100,7 @@ async def delete(self, name: str) -> bool: Returns: bool: True if deletion was successful """ - return await self._make_request( + return await self.client._make_request( "DELETE", f"prompts/{name}", version="v3", diff --git a/py/sdk/v3/retrieval.py b/py/sdk/v3/retrieval.py index f91e198c8..e925cdf86 100644 --- a/py/sdk/v3/retrieval.py +++ b/py/sdk/v3/retrieval.py @@ -125,7 +125,7 @@ async def rag( if rag_generation_config and rag_generation_config.get( # type: ignore "stream", False ): - return self._make_streaming_request( + return self.client._make_streaming_request( "POST", "retrieval/rag", json=data, @@ -194,7 +194,7 @@ async def agent( if rag_generation_config and rag_generation_config.get( # type: ignore "stream", False ): - return self._make_streaming_request( + return self.client._make_streaming_request( "POST", "retrieval/agent", json=data, diff --git a/py/sdk/v3/users.py b/py/sdk/v3/users.py index 8cb595392..6c83472c3 100644 --- a/py/sdk/v3/users.py +++ b/py/sdk/v3/users.py @@ -187,9 +187,7 @@ async def reset_password( async def list( self, - email: Optional[str] = None, - is_active: Optional[bool] = None, - is_superuser: Optional[bool] = None, + ids: Optional[list[str | UUID]] = None, offset: Optional[int] = 0, limit: Optional[int] = 100, ) -> dict: @@ -197,26 +195,18 @@ async def list( List users with pagination and filtering options. Args: - email (Optional[str]): Email to filter by (partial match) - is_active (Optional[bool]): Filter by active status - is_superuser (Optional[bool]): Filter by superuser status offset (int, optional): Specifies the number of objects to skip. Defaults to 0. limit (int, optional): Specifies a limit on the number of objects to return, ranging between 1 and 100. Defaults to 100. Returns: dict: List of users and pagination information """ - params: dict = { + params = { "offset": offset, "limit": limit, } - - if email: - params["email"] = email - if is_active is not None: - params["is_active"] = is_active - if is_superuser is not None: - params["is_superuser"] = is_superuser + if ids: + params["ids"] = [str(user_id) for user_id in ids] # type: ignore return await self.client._make_request( "GET",