Skip to content

feat: context grounding cli WIP#1523

Open
DragosBobolea wants to merge 8 commits intomainfrom
feature/context-grounding-cli
Open

feat: context grounding cli WIP#1523
DragosBobolea wants to merge 8 commits intomainfrom
feature/context-grounding-cli

Conversation

@DragosBobolea
Copy link
Member

Add uipath context-grounding CLI commands

Summary

Adds a new uipath context-grounding command group to the CLI, giving developers direct access to the Context Grounding (ECS) service from the terminal

Commands

uipath context-grounding retrieve --index <name> --folder-path <path>
uipath context-grounding search <query> --index <name> --folder-path <path> [--results N]
uipath context-grounding ingest --index <name> --folder-path <path>
uipath context-grounding delete --index <name> --folder-path <path> [--confirm] [--dry-run]

All commands support --folder-path, --folder-key, --format [json|table|csv], and -o <file> — consistent with uipath buckets.

Examples

# Retrieve index details
uipath context-grounding retrieve --index "Athena_Chatbot" --folder-path "Shared"

# Semantic search — same call agents make at runtime
uipath context-grounding search "who knows RPA" --index "Athena_Chatbot" --folder-path "Shared"
uipath context-grounding search "who knows RPA" --index "Athena_Chatbot" --folder-path "Shared" --results 5
uipath context-grounding search "who knows RPA" --index "Athena_Chatbot" --folder-path "Shared" --format json
uipath context-grounding search "who knows RPA" --index "Athena_Chatbot" --folder-path "Shared" -o results.json

# Re-trigger ingestion
uipath context-grounding ingest --index "Athena_Chatbot" --folder-path "Shared"

# Delete (with safety prompts)
uipath context-grounding delete --index "Athena_Chatbot" --folder-path "Shared" --dry-run
uipath context-grounding delete --index "Athena_Chatbot" --folder-path "Shared" --confirm

# Use env var to avoid repeating --folder-path
export UIPATH_FOLDER_PATH="Shared"
uipath context-grounding search "who knows RPA" --index "Athena_Chatbot"

Verified against live environment

Tested against https://alpha.uipath.com/goldenagents/DefaultTenant, Athena_Chatbot index in Shared folder:

$ uipath context-grounding retrieve --index "Athena_Chatbot" --folder-path "Shared"
# → shows index metadata: last ingested, status Successful, backed by StorageBucket

$ uipath context-grounding search "who knows RPA" --index "Athena_Chatbot" --folder-path "Shared"
# → returns Natasha Kim, Business Analyst, RPA specialist (score 0.548)

Adds uipath context-grounding command group with four subcommands:

  retrieve  --index NAME --folder PATH
  search    QUERY --index NAME --folder PATH [--results N]
  ingest    --index NAME --folder PATH
  delete    --index NAME --folder PATH [--confirm] [--dry-run]

All commands use --index (named option) and --folder (alias for
--folder-path) consistently. Strictly wraps the existing
ContextGroundingService without any service-layer changes.

search truncates content to 120 chars in table format for readability;
JSON and CSV output receives full content. IngestionInProgressException
is surfaced as a clean CLI error on both search and ingest.
Integration tests (tests/cli/integration/test_context_grounding_commands.py):
- 29 tests covering retrieve, search, ingest, delete commands
- Tests for all options (--index, --folder, --folder-key, --results, --format)
- Error paths: not-found, ingestion-in-progress, missing required flags
- Table truncation vs JSON full-content for search output
- Confirmation prompt and --dry-run for delete
- Help text for all commands

Contract tests (tests/cli/contract/test_sdk_cli_alignment.py):
- 4 new parametrized cases for context-grounding retrieve/search/ingest/delete
- Param mappings: CLI --index (index_name) → SDK name
- SDK exclusions: ingest/delete take an index object, not a name
- CLI exclusions: index_name on ingest/delete is a lookup key with no SDK counterpart

Fix: use spec=ContextGroundingIndex / spec=ContextGroundingQueryResponse on
mocks so MagicMock.__iter__ doesn't cause format_output to treat a single
model as an empty iterator.
- Use --folder-path consistently (matches buckets; was --folder)
  and reuse common_service_options directly instead of a duplicate decorator
- Replace bare 'raise' after 'except Exception' with _not_found() helper
  that always produces a ClickException (clean message or not-found),
  no raw tracebacks ever escape to the user
- Extract _not_found() helper used by retrieve/search/ingest/delete
- Remove redundant confirm/dry_run from CLI_EXCLUSIONS (already in
  global CLI_ONLY_OPTIONS)
- Add test_ingest_with_folder_key and test_delete_with_folder_key
  integration tests (ingest/delete were the only commands missing
  folder-key coverage)
- Update all integration test invocations to use --folder-path
The ECS search API rejects numberOfResults=0 with a 400. Add
click.IntRange(min=1) on the --results option so the error is caught
immediately with a clean usage message instead of an HTTP 400.
- ruff format all three changed files
- Add complete type annotations to all command functions:
  ctx: click.Context, typed params, -> Any / -> None return types
- Import Optional and Any from typing
- context_grounding group annotated -> None
- ingest/delete return bare (no 'return None') for cleaner style
- Under project mypy config: 0 errors
- Under strict mypy: only 4 pre-existing get_client() untyped-call
  errors (same infrastructure gap as cli_buckets.py which has 17)
The full model_dump() produced an unreadably wide table with all 9 fields
(metadata dict, reference URL, source_document_id, caption, id).

Table mode now projects to {score, source, page, content} only — the
four columns useful for human inspection at the terminal. JSON and CSV
still return the full objects so no data is lost for scripting.

Also rounds score to 3 decimal places in table mode for cleaner output.
@DragosBobolea DragosBobolea changed the title feat: context grounding cli feat: context grounding cli WIP Mar 27, 2026
_not_found:
- Change return type -> NoReturn (unconditionally raises; None was a
  false-negative for type checkers and caused 'index possibly unbound'
  warnings at call sites in ingest/delete)
- Update docstring to accurately describe the single responsibility:
  classify not-found vs other, re-raise as ClickException

search:
- Rename --results to --limit (-n) — more conventional (az/gcloud/aws
  all use --limit for a max-count option)
- Update all examples in docstring and help text

ingest:
- Check index.in_progress_ingestion() immediately after retrieve() to
  fail fast without a redundant HTTP round-trip (issue #4)
- Add test_ingest_already_in_progress_fast_fail covering this path and
  asserting ingest_data is never called

delete:
- Wrap delete_index() in try/except to surface 403/404 race conditions
  as clean ClickException rather than raw error (issue #5)

contract tests:
- Replace fragile 'not service.startswith(context)' heuristic with an
  explicit NESTED_SERVICES set — adding future services can't silently
  break the navigation logic (issue #6)

tests:
- Remove dead mock_client.context_grounding.retrieve.return_value from
  test_search_basic and test_search_with_results_option (search doesn't
  call retrieve; issue #11)
- Rename test_search_with_results_option -> test_search_with_limit_option
- Add comment in test_search_empty_results explaining err=True / CliRunner
  stderr mixing (issue #12)
- _make_index now configures in_progress_ingestion.return_value = False
  so the new fast-fail guard doesn't trip in unrelated tests
ingest: merge retrieve + ingest_data into a single try block so index
is always bound when used. Exception ordering:
  1. click.ClickException re-raised immediately (from in_progress check)
  2. IngestionInProgressException → clean ClickException
  3. Any other exception → _not_found() (NoReturn, converts to
     ClickException with not-found or generic message)
This also ensures the success echo is only reached on a clean path —
previously a generic exception from ingest_data would bubble past the
echo through service_command's handler, giving the user an error
message but no success confirmation either.

delete: keep retrieve and delete_index in separate try blocks (because
dry-run/confirm prompts must happen in between), but remove the
type: ignore comment — mypy correctly infers index is bound because
_not_found is -> NoReturn, so no suppression needed.

Also fix page key name: 'page' -> 'page_number' in table rows so
column names are identical across table/json/csv formats.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant