From f85514207d76c1e775958dc35a95a90eb320eefd Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Wed, 26 Aug 2026 13:44:59 -0700 Subject: [PATCH 01/31] Add A2A server support --- .gcloudignore | 6 + README.md | 34 ++- doc/source/user_guide/cli.rst | 2 +- doc/source/user_guide/installation.rst | 4 +- docker/Dockerfile | 22 ++ docker/a2a-entrypoint.sh | 33 +++ gcloud/Dockerfile | 25 -- gcloud/README.md | 175 +++++++++---- gcloud/bootstrap.sh | 86 ------- gcloud/build-image.sh | 23 -- gcloud/cloudbuild.yaml | 6 +- gcloud/deploy-cloud-run.sh | 93 ------- gcloud/deploy.sh | 300 +++++++++++++++++++++++ gcloud/run-a2a-server.sh | 12 - pyproject.toml | 6 +- samples/profile_create.py | 2 +- src/select_ai/a2a_server.py | 218 ---------------- src/select_ai/agent/a2a/__init__.py | 8 + src/select_ai/agent/a2a/context_store.py | 138 +++++++++++ src/select_ai/agent/a2a/server.py | 190 ++++++++++++++ src/select_ai/agent/a2a/task_store.py | 243 ++++++++++++++++++ src/select_ai/cli/a2a.py | 26 +- tests/a2a/test_agent_card.py | 55 +++++ 23 files changed, 1162 insertions(+), 545 deletions(-) create mode 100644 docker/Dockerfile create mode 100644 docker/a2a-entrypoint.sh delete mode 100644 gcloud/Dockerfile delete mode 100755 gcloud/bootstrap.sh delete mode 100755 gcloud/build-image.sh delete mode 100755 gcloud/deploy-cloud-run.sh create mode 100755 gcloud/deploy.sh delete mode 100755 gcloud/run-a2a-server.sh delete mode 100644 src/select_ai/a2a_server.py create mode 100644 src/select_ai/agent/a2a/__init__.py create mode 100644 src/select_ai/agent/a2a/context_store.py create mode 100644 src/select_ai/agent/a2a/server.py create mode 100644 src/select_ai/agent/a2a/task_store.py create mode 100644 tests/a2a/test_agent_card.py diff --git a/.gcloudignore b/.gcloudignore index cec864b..f2fd78d 100644 --- a/.gcloudignore +++ b/.gcloudignore @@ -19,6 +19,12 @@ __pycache__/ .ruff_cache/ build/ dist/ +doc/ docs/ tests/ samples/ +*.docx +*.pdf +*.png +*.jpg +*.jpeg diff --git a/README.md b/README.md index aa42688..5cbd863 100644 --- a/README.md +++ b/README.md @@ -20,11 +20,7 @@ Install the optional command line interface: python3 -m pip install 'select_ai[cli]' ``` -Install A2A server support: - -```bash -python3 -m pip install 'select_ai[a2a]' -``` +The CLI extra includes A2A server support. ## Documentation @@ -36,12 +32,15 @@ Examples can be found in the [/samples][samples] directory ## Command Line Interface -The optional `select-ai` command provides an interactive chat REPL for Select AI -profiles: +The optional `select-ai` command provides interactive chat, SQL, profile +management, and A2A server tools for Select AI: +### Chat ```bash select-ai chat --profile OCI_AI_PROFILE ``` +![Select AI CLI demo](doc/source/image/select_ai_cli_demo.gif) + ### A2A Server @@ -57,6 +56,11 @@ The command obtains database connection settings from its options or the `/a2a/jsonrpc/`. Set `--public-url` when the server is behind a proxy or load balancer so that clients receive its externally reachable URL. +For Autonomous Database mTLS, also set `SELECT_AI_WALLET_LOCATION` to the +directory containing the unzipped wallet and set `SELECT_AI_WALLET_PASSWORD`. +The CLI passes both values to the Select AI SDK as `wallet_location` and +`wallet_password`. + The server accepts both A2A 1.x and the A2A v0.3 JSON-RPC streaming protocol for compatibility with Gemini Enterprise. @@ -69,20 +73,12 @@ select-ai a2a agent-card \ --public-url https://YOUR-SERVICE.run.app ``` -### Cloud Run - -The repository includes an Oracle Linux 10 / Python 3.12 container image in -the `gcloud` directory for Cloud Run. Configure `A2A_TEAM`, `PUBLIC_URL`, and the standard -`SELECT_AI_*` connection environment variables at deployment. Inject -`SELECT_AI_PASSWORD` from Secret Manager; never add database credentials to -the image or source tree. +## Cloud Run -See [gcloud/README.md](gcloud/README.md) for one-time secret setup, image -build, and per-team deployment commands. - -![Select AI CLI demo](doc/source/image/select_ai_cli_demo.gif) +Deploy the A2A server to Cloud Run using the instructions in +[gcloud/README.md](https://github.com/oracle/python-select-ai/blob/main/gcloud/README.md). -### Basic Example +## Basic Example ```python import select_ai diff --git a/doc/source/user_guide/cli.rst b/doc/source/user_guide/cli.rst index d8d084e..76e2855 100644 --- a/doc/source/user_guide/cli.rst +++ b/doc/source/user_guide/cli.rst @@ -32,7 +32,7 @@ workflows will be added in upcoming releases as the CLI evolves. :width: 100% The package provides an optional ``select-ai`` command line tool. Install the -CLI extra to use it: +CLI extra to use it, including the A2A server commands: .. code-block:: bash diff --git a/doc/source/user_guide/installation.rst b/doc/source/user_guide/installation.rst index 078f39d..f338f5b 100644 --- a/doc/source/user_guide/installation.rst +++ b/doc/source/user_guide/installation.rst @@ -72,8 +72,8 @@ are isolated from your system Python installation. python -m pip install --upgrade "select_ai[cli]" - This installs the ``select-ai`` command. See :ref:`Command Line Interface - `. + This installs the ``select-ai`` command and its A2A server support. See + :ref:`Command Line Interface `. 6. If you are behind a proxy, use the ``--proxy`` option. For example: diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..ae074c3 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,22 @@ +FROM oraclelinux:10-slim + +ENV PATH=/opt/venv/bin:$PATH + +RUN microdnf update -y \ + && microdnf install -y python3 python3-pip ca-certificates unzip \ + && microdnf clean all + +WORKDIR /app + +COPY pyproject.toml README.md LICENSE.txt ./ +COPY src ./src + +RUN python3 -m venv /opt/venv \ + && python -m pip install --no-cache-dir --upgrade pip setuptools wheel \ + && python -m pip install --no-cache-dir '.[a2a]' + +COPY docker/a2a-entrypoint.sh /app/docker/a2a-entrypoint.sh + +RUN chmod 0555 /app/docker/a2a-entrypoint.sh + +ENTRYPOINT ["select-ai"] diff --git a/docker/a2a-entrypoint.sh b/docker/a2a-entrypoint.sh new file mode 100644 index 0000000..b4326d6 --- /dev/null +++ b/docker/a2a-entrypoint.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env sh + +# Cloud Run-only A2A launcher. It expands the optional wallet archive mounted +# by deploy.sh, then starts the generic select-ai CLI in A2A server mode. + +set -eu + +wallet_archive=/var/run/secrets/select-ai-wallet/wallet.zip +wallet_root=/tmp/select-ai-wallet + +if [ -f "$wallet_archive" ]; then + mkdir -p "$wallet_root" + chmod 700 "$wallet_root" + unzip -q "$wallet_archive" -d "$wallet_root" + + wallet_file="$(find "$wallet_root" -type f -name ewallet.pem -print -quit)" + if [ -z "$wallet_file" ]; then + echo "Wallet ZIP does not contain ewallet.pem" >&2 + exit 1 + fi + export SELECT_AI_WALLET_LOCATION="$(dirname "$wallet_file")" +fi + +: "${SELECT_AI_A2A_TEAM:?SELECT_AI_A2A_TEAM is required}" +: "${PUBLIC_URL:?PUBLIC_URL is required}" +: "${SELECT_AI_POOL_MAX_SIZE:=10}" + +exec select-ai a2a serve \ + --team "$SELECT_AI_A2A_TEAM" \ + --host 0.0.0.0 \ + --port "${PORT:-8080}" \ + --pool-max-size "$SELECT_AI_POOL_MAX_SIZE" \ + --public-url "$PUBLIC_URL" diff --git a/gcloud/Dockerfile b/gcloud/Dockerfile deleted file mode 100644 index ce96f02..0000000 --- a/gcloud/Dockerfile +++ /dev/null @@ -1,25 +0,0 @@ -FROM oraclelinux:10-slim - -ENV PYTHONDONTWRITEBYTECODE=1 \ - PYTHONUNBUFFERED=1 \ - PATH=/opt/venv/bin:$PATH - -RUN microdnf install -y python3 python3-pip ca-certificates \ - && microdnf clean all - -WORKDIR /app - -COPY pyproject.toml README.md LICENSE.txt ./ -COPY src ./src - -RUN python3 -m venv /opt/venv \ - && python -m pip install --no-cache-dir --upgrade pip setuptools wheel \ - && python -m pip install --no-cache-dir '.[a2a]' - -COPY gcloud/run-a2a-server.sh /app/gcloud/run-a2a-server.sh - -RUN chmod 0555 /app/gcloud/run-a2a-server.sh - -EXPOSE 8080 - -ENTRYPOINT ["/app/gcloud/run-a2a-server.sh"] diff --git a/gcloud/README.md b/gcloud/README.md index 6681b20..e075515 100644 --- a/gcloud/README.md +++ b/gcloud/README.md @@ -1,74 +1,159 @@ -# Google Cloud deployment +# Deploy the Select AI A2A server to Google Cloud -This directory separates the three deployment concerns: +`gcloud/deploy.sh` builds or selects a Select AI container image, creates or +updates a private Cloud Run service, and configures its database secrets. Run +it on a machine with the [Google Cloud CLI](https://cloud.google.com/sdk/docs/install) +installed and authenticated to the target project. -```text -bootstrap.sh one-time project and Secret Manager setup -build-image.sh source → one generic Artifact Registry image -deploy-cloud-run.sh existing image → one Cloud Run service/team -``` +## IAM permissions + +The scripts use the active `gcloud` identity. They do not elevate its access. + +### Deployer (the active gcloud identity) + +| Operation | Required permissions | +| --- | --- | +| Inspect and create the Artifact Registry repository | `artifactregistry.repositories.get`, `artifactregistry.repositories.create` | +| Inspect and create the default runtime service account | `iam.serviceAccounts.get`, `iam.serviceAccounts.create` | +| Deploy or update Cloud Run | `run.services.create`, `run.services.update`, `run.services.get`, `run.operations.get`; `iam.serviceAccounts.actAs` on the runtime service account; `artifactregistry.repositories.downloadArtifacts` on the image repository | +| With `--build`, upload local source, submit, and wait for a build | `storage.buckets.get`, `storage.objects.create` on the configured source-staging bucket; `cloudbuild.builds.create`, `cloudbuild.builds.get`, `serviceusage.services.use` | +| Inspect, create, and add versions to database or wallet secrets | `secretmanager.secrets.get`, `secretmanager.secrets.create`, `secretmanager.versions.add` | +| Grant the runtime account access to those secrets | `secretmanager.secrets.getIamPolicy`, `secretmanager.secrets.setIamPolicy` | +| Grant Gemini Enterprise and the active gcloud identity access to the service | `run.services.getIamPolicy`, `run.services.setIamPolicy` | +| Obtain the project number | `resourcemanager.projects.get` | + +### Runtime service account + +| Operation | Required permissions | +| --- | --- | +| Read database and wallet secrets while serving requests | `secretmanager.versions.access` | -## 1. One-time setup and secrets +### Other service identities -Run: +| Principal | Operation | Required permissions | +| --- | --- | --- | +| Cloud Build execution service account | With `--build`, push the built image | `artifactregistry.repositories.uploadArtifacts` | +| Gemini Enterprise service agent | Invoke the private Cloud Run service | `run.routes.invoke` | +| Active gcloud identity | Fetch the Agent Card after deployment | `run.routes.invoke` | + +The source-staging bucket is Cloud Build's default unless a custom bucket is +configured. Cloud Build also needs access to its build-log destination; the +default same-project build account has that access. If your organization uses +a custom build service account, source bucket, or log bucket, its administrator +must grant the equivalent Cloud Storage permissions on those resources. + +Google Cloud references: [Service Usage access control](https://cloud.google.com/service-usage/docs/access-control), [Cloud Run deployment permissions](https://cloud.google.com/run/docs/reference/iam/roles), [Secret Manager access control](https://cloud.google.com/secret-manager/docs/access-control), [Artifact Registry roles](https://cloud.google.com/artifact-registry/docs/access-control), and [Cloud Build roles](https://cloud.google.com/build/docs/iam-roles-permissions). + +The rows that set IAM policy are administrative mutations. They are present +because `deploy.sh` creates and rotates secrets and configures private-service +invocation. If your customer deployment identity must not change IAM, provision +the secrets and the `secretmanager.versions.access`/`run.routes.invoke` +permissions beforehand, then +remove those policy-setting commands from the deployment workflow. + +## Prerequisite: enable project APIs once + +An administrator must enable these APIs once for the project: ```bash -gcloud/bootstrap.sh +gcloud services enable \ + run.googleapis.com \ + cloudbuild.googleapis.com \ + artifactregistry.googleapis.com \ + secretmanager.googleapis.com \ + discoveryengine.googleapis.com \ + --project PROJECT_ID ``` -It enables the required APIs, creates the `select-ai` Artifact Registry Docker -repository, creates the `oracle-a2a-runtime` service account, prompts for the -ADB username/password/connect descriptor, and stores them as Secret Manager -secrets. It also grants only that runtime service account access to the -secrets. - -The deployed container receives those secrets as: +## Deploy (and update) the A2A server -```text -SELECT_AI_USER -SELECT_AI_PASSWORD -SELECT_AI_DB_CONNECT_STRING +```bash +gcloud/deploy.sh --build ``` -## 2. Build the generic image when code changes +On the first deployment, the script prompts for the ADB user, password, and +connect descriptor. It stores them in Secret Manager under names based on the +Cloud Run service, and grants only the runtime service account access. The +container receives the values as `SELECT_AI_USER`, `SELECT_AI_PASSWORD`, and +`SELECT_AI_DB_CONNECT_STRING`; they are never placed in the image or source +tree. + +### Optional: Autonomous Database mTLS wallet -Run: +The Select AI SDK already supports `wallet_location` and `wallet_password`. +For Cloud Run, pass the path to the downloaded Autonomous Database wallet ZIP +on the first deployment (or when replacing it): ```bash -gcloud/build-image.sh +gcloud/deploy.sh --wallet-archive /path/to/Wallet_database.zip ``` -Cloud Build receives the repository source (filtered by `.gcloudignore`) and -uses `gcloud/Dockerfile`. It builds the generic image: +The script prompts for the wallet password, stores the ZIP and password as +service-specific Secret Manager secrets, and grants access only to the runtime +service account. Cloud Run mounts the ZIP read-only; its A2A launcher expands it +into ephemeral `/tmp` storage before starting the SDK, verifies it contains +`ewallet.pem`, and sets `SELECT_AI_WALLET_LOCATION` to that file's directory. +Do not commit the wallet ZIP or put its contents in the image. + +Later deploys reuse the wallet. To replace it, pass `--wallet-archive` again. -```text -REGION-docker.pkg.dev/PROJECT_ID/select-ai/select-ai-a2a-server:IMAGE_TAG +The first deployment needs `--build` (or an explicit `--image-uri`). Later +deployments reuse the image already deployed to the service, so changing Cloud +Run configuration or secrets does not create another image. The command +deploys private Cloud Run, sets the final public URL in the Agent Card, grants your active +gcloud identity and Gemini Enterprise Discovery Engine service agent the +`run.routes.invoke` permission for this Cloud Run service. + +The default Cloud Run service is `oracle-a2a-agent`. Its default Agent Team, +installed in Oracle Database, is `ORACLE_AI_DATABASE_AGENT`. Override either +with explicit options: + +```bash +gcloud/deploy.sh --service sales-analyst-a2a --a2a-team SALES_ANALYST ``` -The database team name is not baked into the image. +Use a distinct `--service` value for each A2A team. Each service gets distinct Secret +Manager secret names by default, so credentials remain attached to that A2A +server. -## 3. Deploy one or more teams from the same image +`--max-instances` controls the number of Cloud Run containers. Each container +can use up to 10 Oracle connections by default; change that limit with +`--pool-max-size`, for example `gcloud/deploy.sh --pool-max-size 20`. -Use the image URI emitted by `build-image.sh`: +### Update the Select AI SDK or this repository + +Update the checkout (or modify its dependency version), then explicitly build +and deploy the new image: ```bash -IMAGE_URI=REGION-docker.pkg.dev/PROJECT_ID/select-ai/select-ai-a2a-server:IMAGE_TAG \ -SERVICE=oracle-database-a2a \ -A2A_TEAM=ORACLE_AI_DATABASE_AGENT \ -gcloud/deploy-cloud-run.sh +git pull +gcloud/deploy.sh --build ``` -Deploy another team without rebuilding: +`--build` creates a freshly tagged image from the current source; without it, +the existing image is reused. Existing database secrets are reused without +prompting. To rotate the ADB credentials, explicitly request it: ```bash -IMAGE_URI=REGION-docker.pkg.dev/PROJECT_ID/select-ai/select-ai-a2a-server:IMAGE_TAG \ -SERVICE=sales-analyst-a2a \ -A2A_TEAM=SALES_ANALYST \ -gcloud/deploy-cloud-run.sh +gcloud/deploy.sh --rotate-db-credentials ``` -The deploy script injects the Secret Manager values as Cloud Run environment -variables. It does not upload source code or build an image. After deployment, -it calls `/.well-known/agent-card.json` using -`gcloud auth print-identity-token` and pretty-prints the result. The active -gcloud user therefore needs `roles/run.invoker` on the service. +### What `cloudbuild.yaml` does + +`gcloud/deploy.sh --build` uses `gcloud/cloudbuild.yaml` to tell Cloud Build to build +`docker/Dockerfile` and push it to Artifact Registry. It is build configuration, +not a command you run. The build context is the repository root, so the image +can install the Select AI source from `pyproject.toml` and `src/`. + +### Cloud Build upload contents + +Before the build starts, `gcloud builds submit` archives and uploads the +repository root. The root `.gcloudignore` excludes local virtual environments, +generated documentation, test data, caches, credentials, and Git metadata. +Keep `src/`, `pyproject.toml`, `docker/`, and `gcloud/` in the upload; they are +required to build the image. If the upload is unexpectedly large, check local +directories against `.gcloudignore` before running `--build` again. + +After a successful deployment, the script prints the A2A Agent Card JSON. +Paste that JSON into Gemini Enterprise to register the private service. The +required Gemini Enterprise invocation permission has already been added. diff --git a/gcloud/bootstrap.sh b/gcloud/bootstrap.sh deleted file mode 100755 index c40d114..0000000 --- a/gcloud/bootstrap.sh +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env bash - -# One-time Google Cloud setup for the Select AI A2A server. -# Creates the Artifact Registry repository, runtime service account, and -# Secret Manager secrets. It prompts for database values and never writes -# them to source files. - -set -euo pipefail - -project_id="${PROJECT_ID:-$(gcloud config get-value project 2>/dev/null)}" -region="${REGION:-us-central1}" -repository="${REPOSITORY:-select-ai}" -runtime_sa_name="${RUNTIME_SA_NAME:-oracle-a2a-runtime}" -db_user_secret="${DB_USER_SECRET:-select-ai-db-user}" -db_password_secret="${DB_PASSWORD_SECRET:-select-ai-db-password}" -db_dsn_secret="${DB_DSN_SECRET:-select-ai-db-connect-string}" - -if [[ -z "$project_id" || "$project_id" == "(unset)" ]]; then - echo "Set PROJECT_ID or configure one with: gcloud config set project PROJECT_ID" >&2 - exit 1 -fi - -runtime_sa="${runtime_sa_name}@${project_id}.iam.gserviceaccount.com" - -gcloud services enable \ - run.googleapis.com \ - cloudbuild.googleapis.com \ - artifactregistry.googleapis.com \ - secretmanager.googleapis.com \ - --project="$project_id" - -if ! gcloud artifacts repositories describe "$repository" \ - --location="$region" --project="$project_id" >/dev/null 2>&1; then - gcloud artifacts repositories create "$repository" \ - --repository-format=docker \ - --location="$region" \ - --project="$project_id" -fi - -if ! gcloud iam service-accounts describe "$runtime_sa" \ - --project="$project_id" >/dev/null 2>&1; then - gcloud iam service-accounts create "$runtime_sa_name" \ - --project="$project_id" \ - --display-name="Oracle Select AI A2A runtime" -fi - -read -r -p "ADB user: " db_user -read -r -s -p "ADB password: " db_password -echo -read -r -p "ADB connect descriptor: " db_dsn -trap 'unset db_user db_password db_dsn' EXIT - -add_secret() { - local name="$1" - local value="$2" - - if gcloud secrets describe "$name" --project="$project_id" >/dev/null 2>&1; then - printf %s "$value" | gcloud secrets versions add "$name" \ - --project="$project_id" --data-file=- >/dev/null - else - printf %s "$value" | gcloud secrets create "$name" \ - --project="$project_id" \ - --replication-policy=automatic \ - --data-file=- >/dev/null - fi - - gcloud secrets add-iam-policy-binding "$name" \ - --project="$project_id" \ - --member="serviceAccount:$runtime_sa" \ - --role="roles/secretmanager.secretAccessor" >/dev/null -} - -add_secret "$db_user_secret" "$db_user" -add_secret "$db_password_secret" "$db_password" -add_secret "$db_dsn_secret" "$db_dsn" - -cat <&2 - exit 1 -fi - -gcloud builds submit "$repo_root" \ - --project="$project_id" \ - --config="$repo_root/gcloud/cloudbuild.yaml" \ - --substitutions="_REGION=$region,_REPOSITORY=$repository,_IMAGE_TAG=$image_tag" - -echo "$region-docker.pkg.dev/$project_id/$repository/select-ai-a2a-server:$image_tag" diff --git a/gcloud/cloudbuild.yaml b/gcloud/cloudbuild.yaml index cde90f7..c21d6fc 100644 --- a/gcloud/cloudbuild.yaml +++ b/gcloud/cloudbuild.yaml @@ -5,13 +5,13 @@ steps: args: - build - --file - - gcloud/Dockerfile + - docker/Dockerfile - --tag - - ${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/select-ai-a2a-server:${_IMAGE_TAG} + - ${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/select-ai:${_IMAGE_TAG} - . images: - - ${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/select-ai-a2a-server:${_IMAGE_TAG} + - ${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/select-ai:${_IMAGE_TAG} substitutions: _REGION: us-central1 diff --git a/gcloud/deploy-cloud-run.sh b/gcloud/deploy-cloud-run.sh deleted file mode 100755 index 9901867..0000000 --- a/gcloud/deploy-cloud-run.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash - -# Deploy an existing Select AI A2A server image to private Cloud Run. -# -# Prerequisites: -# * gcloud is authenticated and has deployment permissions. -# * Run bootstrap.sh to create the runtime service account and secrets. -# * Run build-image.sh to create IMAGE_URI when application code changes. -# -# Override any setting by exporting it before running this script. - -set -euo pipefail - -project_id="${PROJECT_ID:-$(gcloud config get-value project 2>/dev/null)}" -region="${REGION:-us-central1}" -service="${SERVICE:-oracle-a2a-agent}" -a2a_team="${A2A_TEAM:-ORACLE_AI_DATABASE_AGENT}" -runtime_sa="${RUNTIME_SA:-oracle-a2a-runtime@${project_id}.iam.gserviceaccount.com}" -image_uri="${IMAGE_URI:-}" -db_user_secret="${DB_USER_SECRET:-select-ai-db-user}" -db_password_secret="${DB_PASSWORD_SECRET:-select-ai-db-password}" -db_dsn_secret="${DB_DSN_SECRET:-select-ai-db-connect-string}" -memory="${MEMORY:-1Gi}" -timeout="${TIMEOUT:-900}" -max_instances="${MAX_INSTANCES:-1}" - -if [[ -z "$project_id" || "$project_id" == "(unset)" ]]; then - echo "Set PROJECT_ID or configure one with: gcloud config set project PROJECT_ID" >&2 - exit 1 -fi - -if [[ -z "$image_uri" ]]; then - echo "Set IMAGE_URI to an image created by gcloud/build-image.sh" >&2 - exit 1 -fi - -active_account="$(gcloud auth list --filter=status:ACTIVE --format='value(account)')" -if [[ -z "$active_account" ]]; then - echo "No active gcloud account. Run: gcloud auth login" >&2 - exit 1 -fi - -for secret in "$db_user_secret" "$db_password_secret" "$db_dsn_secret"; do - gcloud secrets describe "$secret" --project="$project_id" >/dev/null -done - -gcloud iam service-accounts describe "$runtime_sa" \ - --project="$project_id" >/dev/null - -# The entrypoint requires PUBLIC_URL. This first revision is immediately -# followed by an update using the actual URL returned by Cloud Run. -gcloud run deploy "$service" \ - --image="$image_uri" \ - --project="$project_id" \ - --region="$region" \ - --service-account="$runtime_sa" \ - --no-allow-unauthenticated \ - --port=8080 \ - --memory="$memory" \ - --timeout="$timeout" \ - --max-instances="$max_instances" \ - --set-env-vars="A2A_TEAM=$a2a_team,PUBLIC_URL=https://pending.invalid" \ - --update-secrets="SELECT_AI_USER=$db_user_secret:1,SELECT_AI_PASSWORD=$db_password_secret:1,SELECT_AI_DB_CONNECT_STRING=$db_dsn_secret:1" - -service_url="$(gcloud run services describe "$service" \ - --project="$project_id" \ - --region="$region" \ - --format='value(status.url)')" - -gcloud run services update "$service" \ - --project="$project_id" \ - --region="$region" \ - --update-env-vars="PUBLIC_URL=$service_url" - -echo "Fetching the authenticated A2A Agent Card..." -agent_card="$(curl --fail --silent --show-error \ - -H "Authorization: Bearer $(gcloud auth print-identity-token)" \ - "$service_url/.well-known/agent-card.json")" - -printf '%s\n' "$agent_card" | python3 -m json.tool - -cat <&2; usage >&2; exit 2 ;; + esac +done + +if [[ -z "$project_id" ]]; then + project_id="$(gcloud config get-value project 2>/dev/null || true)" +fi + +if [[ -z "$project_id" || "$project_id" == "(unset)" ]]; then + echo "Pass --project or configure one with: gcloud config set project PROJECT_ID" >&2 + exit 1 +fi + +runtime_sa="${runtime_sa:-${runtime_sa_name}@${project_id}.iam.gserviceaccount.com}" +db_user_secret="${db_user_secret:-${service}-db-user}" +db_password_secret="${db_password_secret:-${service}-db-password}" +db_dsn_secret="${db_dsn_secret:-${service}-db-connect-string}" +wallet_secret="${wallet_secret:-${service}-wallet}" +wallet_password_secret="${wallet_password_secret:-${service}-wallet-password}" + +if [[ "$build_image" == true && -n "$image_uri" ]]; then + echo "--build and --image-uri cannot be used together." >&2 + exit 2 +fi +if [[ "$build_image" == false && -n "$image_tag" ]]; then + echo "--image-tag requires --build." >&2 + exit 2 +fi +if ! [[ "$pool_max_size" =~ ^[1-9][0-9]*$ ]]; then + echo "--pool-max-size must be a positive integer." >&2 + exit 2 +fi + +if ! gcloud artifacts repositories describe "$repository" --location="$region" \ + --project="$project_id" >/dev/null 2>&1; then + gcloud artifacts repositories create "$repository" \ + --repository-format=docker --location="$region" --project="$project_id" +fi + +if ! gcloud iam service-accounts describe "$runtime_sa" --project="$project_id" >/dev/null 2>&1; then + if [[ "$runtime_sa_explicit" == true ]]; then + echo "Runtime service account does not exist: $runtime_sa" >&2 + exit 1 + fi + gcloud iam service-accounts create "$runtime_sa_name" --project="$project_id" \ + --display-name="Oracle Select AI A2A runtime" +fi + +# Deployments made with the former scripts used these shared secret names. +# Reuse them automatically so an existing service can be updated without +# re-entering credentials. New services receive service-specific names above. +service_exists=false +if gcloud run services describe "$service" --project="$project_id" --region="$region" >/dev/null 2>&1; then + service_exists=true +fi + +create_or_rotate_secrets=false +if [[ "$rotate_db_credentials" == true ]]; then + create_or_rotate_secrets=true +else + for secret in "$db_user_secret" "$db_password_secret" "$db_dsn_secret"; do + if ! gcloud secrets describe "$secret" --project="$project_id" >/dev/null 2>&1; then + create_or_rotate_secrets=true + break + fi + done +fi + +if [[ "$create_or_rotate_secrets" == true ]]; then + echo "Creating or rotating ADB credentials for Cloud Run service: $service" + read -r -p "ADB user: " db_user + read -r -s -p "ADB password: " db_password + echo + read -r -p "ADB connect descriptor: " db_dsn + trap 'unset db_user db_password db_dsn' EXIT + + add_secret() { + local name="$1" + local value="$2" + if gcloud secrets describe "$name" --project="$project_id" >/dev/null 2>&1; then + printf %s "$value" | gcloud secrets versions add "$name" --project="$project_id" --data-file=- >/dev/null + else + printf %s "$value" | gcloud secrets create "$name" --project="$project_id" --replication-policy=automatic --data-file=- >/dev/null + fi + gcloud secrets add-iam-policy-binding "$name" --project="$project_id" \ + --member="serviceAccount:$runtime_sa" --role="roles/secretmanager.secretAccessor" >/dev/null + } + + add_secret "$db_user_secret" "$db_user" + add_secret "$db_password_secret" "$db_password" + add_secret "$db_dsn_secret" "$db_dsn" +fi + +# An Oracle mTLS wallet is a ZIP archive containing several files, so it is +# mounted as a Secret Manager volume rather than exposed as an environment +# variable. Pass --wallet-archive to enable or replace this optional configuration. +wallet_enabled=false +if gcloud secrets describe "$wallet_secret" --project="$project_id" >/dev/null 2>&1 \ + && gcloud secrets describe "$wallet_password_secret" --project="$project_id" >/dev/null 2>&1; then + wallet_enabled=true +fi +if [[ -n "$wallet_archive" ]]; then + if [[ ! -f "$wallet_archive" ]]; then + echo "--wallet-archive must name an existing wallet ZIP file: $wallet_archive" >&2 + exit 1 + fi + read -r -s -p "ADB wallet password: " wallet_password + echo + trap 'unset db_user db_password db_dsn wallet_password' EXIT + + if gcloud secrets describe "$wallet_secret" --project="$project_id" >/dev/null 2>&1; then + gcloud secrets versions add "$wallet_secret" --project="$project_id" --data-file="$wallet_archive" >/dev/null + else + gcloud secrets create "$wallet_secret" --project="$project_id" --replication-policy=automatic --data-file="$wallet_archive" >/dev/null + fi + if gcloud secrets describe "$wallet_password_secret" --project="$project_id" >/dev/null 2>&1; then + printf %s "$wallet_password" | gcloud secrets versions add "$wallet_password_secret" --project="$project_id" --data-file=- >/dev/null + else + printf %s "$wallet_password" | gcloud secrets create "$wallet_password_secret" --project="$project_id" --replication-policy=automatic --data-file=- >/dev/null + fi + wallet_enabled=true +fi +if [[ "$wallet_enabled" == true ]]; then + for secret in "$wallet_secret" "$wallet_password_secret"; do + gcloud secrets add-iam-policy-binding "$secret" --project="$project_id" \ + --member="serviceAccount:$runtime_sa" --role="roles/secretmanager.secretAccessor" >/dev/null + done +fi + +if [[ "$build_image" == true ]]; then + image_tag="${image_tag:-$(git -C "$repo_root" rev-parse --short HEAD)-$(date -u +%Y%m%d%H%M%S)}" + image_uri="$region-docker.pkg.dev/$project_id/$repository/select-ai:$image_tag" + echo "Building $image_uri" + gcloud builds submit "$repo_root" --project="$project_id" \ + --config="$repo_root/gcloud/cloudbuild.yaml" \ + --substitutions="_REGION=$region,_REPOSITORY=$repository,_IMAGE_TAG=$image_tag" +elif [[ -z "$image_uri" && "$service_exists" == true ]]; then + image_uri="$(gcloud run services describe "$service" --project="$project_id" --region="$region" \ + --format='value(spec.template.spec.containers[0].image)')" +elif [[ -z "$image_uri" ]]; then + echo "First deployment requires --build or --image-uri." >&2 + exit 2 +fi + +# Cloud Run needs a URL before the server can construct its Agent Card. Deploy +# once with a placeholder, then update PUBLIC_URL with the assigned URL. +secret_mappings=( + "SELECT_AI_USER=$db_user_secret:latest" + "SELECT_AI_PASSWORD=$db_password_secret:latest" + "SELECT_AI_DB_CONNECT_STRING=$db_dsn_secret:latest" +) +if [[ "$wallet_enabled" == true ]]; then + secret_mappings+=( + "/var/run/secrets/select-ai-wallet/wallet.zip=$wallet_secret:latest" + "SELECT_AI_WALLET_PASSWORD=$wallet_password_secret:latest" + ) +fi +secret_mappings_csv="$(IFS=,; echo "${secret_mappings[*]}")" + +gcloud run deploy "$service" --image="$image_uri" --project="$project_id" --region="$region" \ + --service-account="$runtime_sa" --no-allow-unauthenticated --port=8080 \ + --command="/app/docker/a2a-entrypoint.sh" \ + --memory="$memory" --timeout="$timeout" --max-instances="$max_instances" \ + --set-env-vars="SELECT_AI_A2A_TEAM=$a2a_team,PUBLIC_URL=https://pending.invalid,SELECT_AI_POOL_MAX_SIZE=$pool_max_size" \ + --update-secrets="$secret_mappings_csv" + +service_url="$(gcloud run services describe "$service" --project="$project_id" --region="$region" --format='value(status.url)')" +gcloud run services update "$service" --project="$project_id" --region="$region" --update-env-vars="PUBLIC_URL=$service_url" + +project_number="$(gcloud projects describe "$project_id" --format='value(projectNumber)')" +gemini_sa="service-$project_number@gcp-sa-discoveryengine.iam.gserviceaccount.com" +gcloud run services add-iam-policy-binding "$service" --project="$project_id" --region="$region" \ + --member="serviceAccount:$gemini_sa" --role="roles/run.invoker" >/dev/null + +active_account="$(gcloud auth list --filter=status:ACTIVE --format='value(account)')" +if [[ -z "$active_account" ]]; then + echo "No active gcloud account. Run: gcloud auth login" >&2 + exit 1 +fi +if gcloud iam service-accounts describe "$active_account" --project="$project_id" >/dev/null 2>&1; then + deployer_member="serviceAccount:$active_account" +else + deployer_member="user:$active_account" +fi +gcloud run services add-iam-policy-binding "$service" --project="$project_id" --region="$region" \ + --member="$deployer_member" --role="roles/run.invoker" >/dev/null + +echo "Cloud Run URL: $service_url" +echo "Fetching the authenticated A2A Agent Card..." +agent_card_file="$(mktemp)" +trap 'rm -f "$agent_card_file"' EXIT +if ! curl --fail --silent --show-error \ + --retry 12 --retry-all-errors --retry-delay 5 --retry-max-time 120 \ + --connect-timeout 10 --max-time 30 \ + -H "Authorization: Bearer $(gcloud auth print-identity-token)" \ + --output "$agent_card_file" \ + "$service_url/.well-known/agent-card.json"; then + echo "Unable to fetch the A2A Agent Card after waiting for Cloud Run to become available." >&2 + exit 1 +fi +python3 -m json.tool < "$agent_card_file" + +cat <=1.0.3", "uvicorn>=0.30", ] +a2a = [ + "select_ai[cli]", +] test = [ "anyio", "pytest", diff --git a/samples/profile_create.py b/samples/profile_create.py index 06aaa4b..da69a93 100644 --- a/samples/profile_create.py +++ b/samples/profile_create.py @@ -22,7 +22,7 @@ select_ai.connect(user=user, password=password, dsn=dsn) provider = select_ai.OCIGenAIProvider( - region="us-chicago-1", oci_apiformat="GENERIC" + region="us-chicago-1", oci_apiformat="GENERIC", model="openai.gpt-4.1" ) profile_attributes = select_ai.ProfileAttributes( credential_name="my_oci_ai_profile_key", diff --git a/src/select_ai/a2a_server.py b/src/select_ai/a2a_server.py deleted file mode 100644 index cd463ae..0000000 --- a/src/select_ai/a2a_server.py +++ /dev/null @@ -1,218 +0,0 @@ -# ----------------------------------------------------------------------------- -# Copyright (c) 2026, Oracle and/or its affiliates. -# -# Licensed under the Universal Permissive License v 1.0 as shown at -# http://oss.oracle.com/licenses/upl. -# ----------------------------------------------------------------------------- - -"""A2A HTTP server support for Oracle Database AI agent teams.""" - -from asyncio import Lock -from contextlib import asynccontextmanager -from typing import Optional - -import select_ai -from select_ai.agent import AsyncTeam -from select_ai.version import __version__ - - -def _a2a_imports(): - """Load optional A2A dependencies only when the server is requested.""" - try: - from a2a.helpers import new_task_from_user_message, new_text_part - from a2a.server.agent_execution import AgentExecutor - from a2a.server.request_handlers import DefaultRequestHandler - from a2a.server.routes import ( - create_agent_card_routes, - create_jsonrpc_routes, - ) - from a2a.server.tasks import InMemoryTaskStore, TaskUpdater - from a2a.types import ( - AgentCapabilities, - AgentCard, - AgentInterface, - AgentSkill, - ) - from starlette.applications import Starlette - except ImportError as exc: - raise RuntimeError( - "A2A server support requires the optional 'a2a' extra. " - "Install it with: pip install 'select_ai[a2a]'" - ) from exc - - return { - "AgentCapabilities": AgentCapabilities, - "AgentCard": AgentCard, - "AgentExecutor": AgentExecutor, - "AgentInterface": AgentInterface, - "AgentSkill": AgentSkill, - "DefaultRequestHandler": DefaultRequestHandler, - "InMemoryTaskStore": InMemoryTaskStore, - "Starlette": Starlette, - "TaskUpdater": TaskUpdater, - "create_agent_card_routes": create_agent_card_routes, - "create_jsonrpc_routes": create_jsonrpc_routes, - "new_task_from_user_message": new_task_from_user_message, - "new_text_part": new_text_part, - } - - -def ensure_a2a_dependencies() -> None: - """Raise a helpful error when the optional A2A dependencies are absent.""" - _a2a_imports() - - -def create_app( # noqa: PLR0913 - team_name: str, - public_url: str, - user: str, - password: str, - dsn: str, - wallet_location: Optional[str] = None, - wallet_password: Optional[str] = None, - description: Optional[str] = None, - pool_max_size: int = 10, -): - """Build an A2A JSON-RPC application for one database AI agent team.""" - if pool_max_size < 1: - raise ValueError("pool_max_size must be at least 1") - - imports = _a2a_imports() - agent_card = _build_agent_card(imports, team_name, public_url, description) - executor = _build_executor(imports, team_name) - handler = imports["DefaultRequestHandler"]( - agent_executor=executor, - task_store=imports["InMemoryTaskStore"](), - agent_card=agent_card, - ) - - @asynccontextmanager - async def lifespan(app): - connect_args = { - "user": user, - "password": password, - "dsn": dsn, - "min_size": 1, - "max_size": pool_max_size, - } - if wallet_location: - connect_args["wallet_location"] = wallet_location - connect_args["config_dir"] = wallet_location - if wallet_password: - connect_args["wallet_password"] = wallet_password - select_ai.create_pool_async(**connect_args) - try: - yield - finally: - await select_ai.async_disconnect() - - routes = imports["create_agent_card_routes"](agent_card) - routes.extend( - imports["create_jsonrpc_routes"]( - handler, - rpc_url="/a2a/jsonrpc/", - enable_v0_3_compat=True, - ) - ) - return imports["Starlette"](routes=routes, lifespan=lifespan) - - -def _build_agent_card(imports, team_name, public_url, description): - description = description or f"Oracle Database AI agent team {team_name}." - endpoint = f"{public_url.rstrip('/')}/a2a/jsonrpc/" - return imports["AgentCard"]( - name=team_name, - description=description, - version=__version__, - default_input_modes=["text/plain"], - default_output_modes=["text/plain"], - capabilities=imports["AgentCapabilities"](streaming=True), - supported_interfaces=[ - imports["AgentInterface"]( - protocol_binding="JSONRPC", - protocol_version="1.0", - url=endpoint, - ), - imports["AgentInterface"]( - protocol_binding="JSONRPC", - protocol_version="0.3", - url=endpoint, - ), - ], - skills=[ - imports["AgentSkill"]( - id=team_name.lower(), - name=team_name, - description=description, - tags=["oracle", "database", "select-ai"], - examples=[], - input_modes=["text/plain"], - output_modes=["text/plain"], - ) - ], - ) - - -def _build_executor(imports, team_name): - agent_executor = imports["AgentExecutor"] - task_updater = imports["TaskUpdater"] - new_task_from_user_message = imports["new_task_from_user_message"] - new_text_part = imports["new_text_part"] - conversation_ids = {} - conversation_lock = Lock() - - async def get_database_conversation_id(task): - """Create one Oracle conversation for each A2A context.""" - a2a_context_id = task.context_id or task.id - async with conversation_lock: - conversation_id = conversation_ids.get(a2a_context_id) - if conversation_id: - return conversation_id - - conversation = select_ai.AsyncConversation( - attributes=select_ai.ConversationAttributes( - title=f"A2A {team_name}", - description=f"A2A context {a2a_context_id}", - ) - ) - conversation_id = await conversation.create() - conversation_ids[a2a_context_id] = conversation_id - return conversation_id - - class DatabaseTeamExecutor(agent_executor): - async def execute(self, context, event_queue): - if context.current_task: - task = context.current_task - else: - task = new_task_from_user_message(context.message) - await event_queue.enqueue_event(task) - - updater = task_updater( - event_queue=event_queue, - task_id=task.id, - context_id=task.context_id, - ) - await updater.start_work() - conversation_id = await get_database_conversation_id(task) - result = await AsyncTeam(team_name=team_name).run( - prompt=context.get_user_input(), - params={"conversation_id": conversation_id}, - ) - await updater.add_artifact( - parts=[new_text_part(result or "")], - name="database-agent-result", - last_chunk=True, - ) - await updater.complete() - - async def cancel(self, context, event_queue): - if context.current_task is None: - return - updater = task_updater( - event_queue=event_queue, - task_id=context.current_task.id, - context_id=context.current_task.context_id, - ) - await updater.cancel() - - return DatabaseTeamExecutor() diff --git a/src/select_ai/agent/a2a/__init__.py b/src/select_ai/agent/a2a/__init__.py new file mode 100644 index 0000000..f4786b6 --- /dev/null +++ b/src/select_ai/agent/a2a/__init__.py @@ -0,0 +1,8 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# https://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +"""A2A support for Select AI Agent Teams.""" diff --git a/src/select_ai/agent/a2a/context_store.py b/src/select_ai/agent/a2a/context_store.py new file mode 100644 index 0000000..6dacc37 --- /dev/null +++ b/src/select_ai/agent/a2a/context_store.py @@ -0,0 +1,138 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# https://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +"""Oracle Database storage for A2A-to-Oracle conversation mappings.""" + +from asyncio import Lock +from typing import Optional + +import oracledb +from a2a.server.context import ServerCallContext +from a2a.server.owner_resolver import OwnerResolver, resolve_user_scope + +import select_ai +from select_ai.db import async_get_connection + +_CREATE_TABLE = """ + BEGIN + EXECUTE IMMEDIATE ' + CREATE TABLE SELECT_AI_A2A_CONTEXTS ( + owner VARCHAR2(512) NOT NULL, + context_id VARCHAR2(255) NOT NULL, + conversation_id VARCHAR2(255) NOT NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL, + CONSTRAINT select_ai_a2a_contexts_pk PRIMARY KEY (owner, context_id) + )'; + EXECUTE IMMEDIATE ' + COMMENT ON TABLE SELECT_AI_A2A_CONTEXTS + IS ''Managed by select_ai.a2a.context_store'''; + EXCEPTION + WHEN OTHERS THEN + IF SQLCODE != -955 THEN + RAISE; + END IF; + END; +""" + + +class OracleContextStore: + """Persist one Oracle conversation for each A2A context.""" + + def __init__( + self, + owner_resolver: OwnerResolver = resolve_user_scope, + ) -> None: + self.owner_resolver = owner_resolver + self.initialized = False + self.initialize_lock = Lock() + + async def initialize(self) -> None: + """Create the context mapping table if it does not already exist.""" + if self.initialized: + return + async with self.initialize_lock: + if self.initialized: + return + await self._execute(_CREATE_TABLE) + self.initialized = True + + async def get_or_create( + self, + context_id: str, + context: ServerCallContext, + team_name: str, + ) -> str: + """Return the Oracle conversation for an A2A context, creating it once.""" + await self.initialize() + owner = self._owner(context) + conversation_id = await self._get(owner, context_id) + if conversation_id: + return conversation_id + + conversation = select_ai.AsyncConversation( + attributes=select_ai.ConversationAttributes( + title=f"A2A {team_name}", + description=f"A2A context {context_id}", + ) + ) + conversation_id = await conversation.create() + try: + await self._execute( + """ + INSERT INTO SELECT_AI_A2A_CONTEXTS ( + owner, context_id, conversation_id, created_at + ) VALUES ( + :owner, :context_id, :conversation_id, SYSTIMESTAMP + ) + """, + owner=owner, + context_id=context_id, + conversation_id=conversation_id, + ) + except oracledb.DatabaseError as error: + if error.args[0].code != 1: + raise + existing_conversation_id = await self._get(owner, context_id) + if existing_conversation_id: + return existing_conversation_id + raise + return conversation_id + + async def _get(self, owner: str, context_id: str) -> Optional[str]: + row = await self._fetchone( + """ + SELECT conversation_id + FROM SELECT_AI_A2A_CONTEXTS + WHERE owner = :owner AND context_id = :context_id + """, + owner=owner, + context_id=context_id, + ) + return row[0] if row else None + + def _owner(self, context: ServerCallContext) -> str: + return self.owner_resolver(context) or "anonymous" + + @staticmethod + async def _execute(statement: str, **parameters) -> None: + async with async_get_connection() as connection: + cursor = connection.cursor() + try: + await cursor.execute(statement, **parameters) + await connection.commit() + finally: + cursor.close() + + @staticmethod + async def _fetchone(statement: str, **parameters): + async with async_get_connection() as connection: + cursor = connection.cursor() + try: + await cursor.execute(statement, **parameters) + return await cursor.fetchone() + finally: + cursor.close() diff --git a/src/select_ai/agent/a2a/server.py b/src/select_ai/agent/a2a/server.py new file mode 100644 index 0000000..686200c --- /dev/null +++ b/src/select_ai/agent/a2a/server.py @@ -0,0 +1,190 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# https://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +"""A2A HTTP server for Oracle Database AI Agent Teams.""" + +from contextlib import asynccontextmanager +from typing import Optional + +from a2a.compat.v0_3.conversions import to_compat_agent_card +from a2a.helpers import new_task_from_user_message, new_text_part +from a2a.server.agent_execution import AgentExecutor +from a2a.server.request_handlers import DefaultRequestHandler +from a2a.server.routes import create_jsonrpc_routes +from a2a.server.tasks import TaskUpdater +from a2a.types import AgentCapabilities, AgentCard, AgentInterface, AgentSkill +from starlette.applications import Starlette +from starlette.responses import JSONResponse +from starlette.routing import Route + +import select_ai +from select_ai.agent import AsyncTeam +from select_ai.agent.a2a.context_store import OracleContextStore +from select_ai.agent.a2a.task_store import OracleTaskStore +from select_ai.version import __version__ + + +class DatabaseTeamExecutor(AgentExecutor): + """Execute A2A requests with one Oracle conversation per A2A context.""" + + def __init__(self, team_name: str, context_store: OracleContextStore): + self.team_name = team_name + self.context_store = context_store + + async def execute(self, context, event_queue): + if context.current_task: + task = context.current_task + else: + task = new_task_from_user_message(context.message) + await event_queue.enqueue_event(task) + + updater = TaskUpdater( + event_queue=event_queue, + task_id=task.id, + context_id=task.context_id, + ) + await updater.start_work() + conversation_id = await self.context_store.get_or_create( + context_id=task.context_id or task.id, + context=context.call_context, + team_name=self.team_name, + ) + result = await AsyncTeam(team_name=self.team_name).run( + prompt=context.get_user_input(), + params={"conversation_id": conversation_id}, + ) + await updater.add_artifact( + parts=[new_text_part(result or "")], + name="database-agent-result", + last_chunk=True, + ) + await updater.complete() + + async def cancel(self, context, event_queue): + if context.current_task is None: + return + updater = TaskUpdater( + event_queue=event_queue, + task_id=context.current_task.id, + context_id=context.current_task.context_id, + ) + await updater.cancel() + + +def create_app( # noqa: PLR0913 + team_name: str, + public_url: str, + user: str, + password: str, + dsn: str, + wallet_location: Optional[str] = None, + wallet_password: Optional[str] = None, + description: Optional[str] = None, + pool_max_size: int = 10, +) -> Starlette: + """Build an A2A JSON-RPC application for one database AI Agent Team.""" + if pool_max_size < 1: + raise ValueError("pool_max_size must be at least 1") + + agent_card = _build_agent_card(team_name, public_url, description) + compat_agent_card = _build_v03_agent_card(agent_card) + task_store = OracleTaskStore() + context_store = OracleContextStore() + handler = DefaultRequestHandler( + agent_executor=DatabaseTeamExecutor(team_name, context_store), + task_store=task_store, + agent_card=agent_card, + ) + + @asynccontextmanager + async def lifespan(app): + connect_args = { + "user": user, + "password": password, + "dsn": dsn, + "min_size": 1, + "max_size": pool_max_size, + } + if wallet_location: + connect_args["wallet_location"] = wallet_location + connect_args["config_dir"] = wallet_location + if wallet_password: + connect_args["wallet_password"] = wallet_password + select_ai.create_pool_async(**connect_args) + try: + await task_store.initialize() + await context_store.initialize() + yield + finally: + await select_ai.async_disconnect() + + async def get_agent_card(request): + """Serve the documented A2A v0.3 card required by Gemini Enterprise.""" + return JSONResponse(compat_agent_card) + + routes = [ + Route( + "/.well-known/agent-card.json", + get_agent_card, + methods=["GET"], + ) + ] + routes.extend( + create_jsonrpc_routes( + handler, + rpc_url="/a2a/jsonrpc/", + enable_v0_3_compat=True, + ) + ) + return Starlette(routes=routes, lifespan=lifespan) + + +def _build_agent_card( + team_name: str, + public_url: str, + description: Optional[str], +) -> AgentCard: + description = description or f"Oracle Database AI agent team {team_name}." + endpoint = f"{public_url.rstrip('/')}/a2a/jsonrpc/" + return AgentCard( + name=team_name, + description=description, + version=__version__, + default_input_modes=["text/plain"], + default_output_modes=["text/plain"], + capabilities=AgentCapabilities(streaming=True), + supported_interfaces=[ + AgentInterface( + protocol_binding="JSONRPC", + protocol_version="1.0", + url=endpoint, + ), + AgentInterface( + protocol_binding="JSONRPC", + protocol_version="0.3", + url=endpoint, + ), + ], + skills=[ + AgentSkill( + id=team_name.lower(), + name=team_name, + description=description, + tags=["oracle", "database", "select-ai"], + examples=[], + input_modes=["text/plain"], + output_modes=["text/plain"], + ) + ], + ) + + +def _build_v03_agent_card(agent_card: AgentCard) -> dict: + """Return the standalone A2A v0.3 discovery representation.""" + return to_compat_agent_card(agent_card).model_dump( + by_alias=True, exclude_none=True + ) diff --git a/src/select_ai/agent/a2a/task_store.py b/src/select_ai/agent/a2a/task_store.py new file mode 100644 index 0000000..3962e8e --- /dev/null +++ b/src/select_ai/agent/a2a/task_store.py @@ -0,0 +1,243 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# https://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +"""Oracle Database implementation of the A2A TaskStore interface.""" + +from __future__ import annotations + +from asyncio import Lock +from typing import Optional + +from a2a.server.context import ServerCallContext +from a2a.server.owner_resolver import OwnerResolver, resolve_user_scope +from a2a.server.tasks.task_store import TaskStore +from a2a.types import a2a_pb2 +from a2a.types.a2a_pb2 import Task +from a2a.utils.constants import DEFAULT_LIST_TASKS_PAGE_SIZE +from a2a.utils.errors import InvalidParamsError +from a2a.utils.task import decode_page_token, encode_page_token +from google.protobuf.json_format import MessageToJson, Parse, ParseDict + +from select_ai.db import async_get_connection + +_CREATE_TABLE = """ + BEGIN + EXECUTE IMMEDIATE ' + CREATE TABLE SELECT_AI_A2A_TASKS ( + owner VARCHAR2(512) NOT NULL, + task_id VARCHAR2(255) NOT NULL, + context_id VARCHAR2(255), + task_json CLOB NOT NULL CHECK (task_json IS JSON), + updated_at TIMESTAMP WITH TIME ZONE NOT NULL, + CONSTRAINT select_ai_a2a_tasks_pk PRIMARY KEY (owner, task_id) + )'; + EXECUTE IMMEDIATE ' + COMMENT ON TABLE SELECT_AI_A2A_TASKS + IS ''Managed by select_ai.a2a.task_store'''; + EXCEPTION + WHEN OTHERS THEN + IF SQLCODE != -955 THEN + RAISE; + END IF; + END; +""" + + +class OracleTaskStore(TaskStore): + """Persist A2A tasks in Oracle Database using Select AI's connection pool.""" + + def __init__( + self, + owner_resolver: OwnerResolver = resolve_user_scope, + ) -> None: + self.owner_resolver = owner_resolver + self.initialized = False + self.initialize_lock = Lock() + + async def initialize(self) -> None: + """Create the task table if it does not already exist.""" + if self.initialized: + return + async with self.initialize_lock: + if self.initialized: + return + await self._execute(_CREATE_TABLE) + self.initialized = True + + async def save(self, task: Task, context: ServerCallContext) -> None: + """Insert or update a task for its resolved owner.""" + await self.initialize() + await self._execute( + """ + MERGE INTO SELECT_AI_A2A_TASKS target + USING ( + SELECT :owner AS owner, :task_id AS task_id FROM dual + ) source + ON (target.owner = source.owner AND target.task_id = source.task_id) + WHEN MATCHED THEN UPDATE SET + context_id = :context_id, + task_json = :task_json, + updated_at = SYSTIMESTAMP + WHEN NOT MATCHED THEN INSERT ( + owner, task_id, context_id, task_json, updated_at + ) VALUES ( + :owner, :task_id, :context_id, :task_json, SYSTIMESTAMP + ) + """, + owner=self._owner(context), + task_id=task.id, + context_id=task.context_id, + task_json=MessageToJson(task), + ) + + async def get( + self, + task_id: str, + context: ServerCallContext, + ) -> Optional[Task]: + """Return a task by ID for its resolved owner.""" + await self.initialize() + row = await self._fetchone( + """ + SELECT task_json + FROM SELECT_AI_A2A_TASKS + WHERE owner = :owner AND task_id = :task_id + """, + owner=self._owner(context), + task_id=task_id, + ) + if row is None: + return None + return await self._task_from_json(row[0]) + + async def list( + self, + params: a2a_pb2.ListTasksRequest, + context: ServerCallContext, + ) -> a2a_pb2.ListTasksResponse: + """Return filtered, paginated tasks for the resolved owner.""" + await self.initialize() + rows = await self._fetchall( + """ + SELECT task_json + FROM SELECT_AI_A2A_TASKS + WHERE owner = :owner + ORDER BY updated_at DESC, task_id DESC + """, + owner=self._owner(context), + ) + tasks = [await self._task_from_json(row[0]) for row in rows] + tasks = self._filter_tasks(tasks, params) + total_size = len(tasks) + start_index = self._page_start_index(tasks, params.page_token) + page_size = params.page_size or DEFAULT_LIST_TASKS_PAGE_SIZE + end_index = start_index + page_size + page = tasks[start_index:end_index] + next_page_token = ( + encode_page_token(tasks[end_index].id) + if end_index < total_size + else None + ) + return a2a_pb2.ListTasksResponse( + tasks=page, + total_size=total_size, + page_size=page_size, + next_page_token=next_page_token, + ) + + async def delete(self, task_id: str, context: ServerCallContext) -> None: + """Delete a task by ID for its resolved owner.""" + await self.initialize() + await self._execute( + """ + DELETE FROM SELECT_AI_A2A_TASKS + WHERE owner = :owner AND task_id = :task_id + """, + owner=self._owner(context), + task_id=task_id, + ) + + @staticmethod + def _filter_tasks( + tasks: list[Task], + params: a2a_pb2.ListTasksRequest, + ) -> list[Task]: + if params.context_id: + tasks = [ + task for task in tasks if task.context_id == params.context_id + ] + if params.status: + tasks = [ + task + for task in tasks + if task.HasField("status") + and task.status.state == params.status + ] + if params.HasField("status_timestamp_after"): + timestamp_after = params.status_timestamp_after.ToJsonString() + tasks = [ + task + for task in tasks + if task.HasField("status") + and task.status.HasField("timestamp") + and task.status.timestamp.ToJsonString() >= timestamp_after + ] + return tasks + + def _owner(self, context: ServerCallContext) -> str: + return self.owner_resolver(context) or "anonymous" + + @staticmethod + def _page_start_index(tasks: list[Task], page_token: str) -> int: + if not page_token: + return 0 + task_id = decode_page_token(page_token) + for index, task in enumerate(tasks): + if task.id == task_id: + return index + raise InvalidParamsError(f"Invalid page token: {page_token}") + + @staticmethod + async def _task_from_json(value) -> Task: + if hasattr(value, "read"): + value = await value.read() + task = Task() + if isinstance(value, dict): + ParseDict(value, task) + else: + Parse(value, task) + return task + + @staticmethod + async def _execute(statement: str, **parameters) -> None: + async with async_get_connection() as connection: + cursor = connection.cursor() + try: + await cursor.execute(statement, **parameters) + await connection.commit() + finally: + cursor.close() + + @staticmethod + async def _fetchone(statement: str, **parameters): + async with async_get_connection() as connection: + cursor = connection.cursor() + try: + await cursor.execute(statement, **parameters) + return await cursor.fetchone() + finally: + cursor.close() + + @staticmethod + async def _fetchall(statement: str, **parameters): + async with async_get_connection() as connection: + cursor = connection.cursor() + try: + await cursor.execute(statement, **parameters) + return await cursor.fetchall() + finally: + cursor.close() diff --git a/src/select_ai/cli/a2a.py b/src/select_ai/cli/a2a.py index a681446..9e3bb70 100644 --- a/src/select_ai/cli/a2a.py +++ b/src/select_ai/cli/a2a.py @@ -13,6 +13,14 @@ from select_ai.cli.common import connection_options from select_ai.version import __version__ +try: + import uvicorn + + from select_ai.agent.a2a.server import create_app +except ImportError: + create_app = None + uvicorn = None + @click.group() def a2a(): @@ -50,21 +58,11 @@ def serve( wallet_password, ): """Start an A2A HTTP server for one database AI agent team.""" - try: - from select_ai.a2a_server import ( - create_app, - ensure_a2a_dependencies, - ) - - ensure_a2a_dependencies() - import uvicorn - except RuntimeError as exc: - raise click.ClickException(str(exc)) from exc - except ImportError as exc: + if create_app is None or uvicorn is None: raise click.ClickException( - "A2A server support requires the optional 'a2a' extra. " - "Install it with: pip install 'select_ai[a2a]'" - ) from exc + "A2A server support requires the optional 'cli' extra. " + "Install it with: pip install 'select_ai[cli]'" + ) if password is None: password = getpass.getpass("Database password: ") diff --git a/tests/a2a/test_agent_card.py b/tests/a2a/test_agent_card.py new file mode 100644 index 0000000..8b543c4 --- /dev/null +++ b/tests/a2a/test_agent_card.py @@ -0,0 +1,55 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# https://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +import asyncio +import json + +import pytest + +pytest.importorskip("a2a") + +from select_ai.agent.a2a.server import ( + _build_agent_card, + _build_v03_agent_card, + create_app, +) + + +def test_v03_discovery_card_is_gemini_enterprise_compatible(): + card = _build_agent_card( + team_name="ORACLE_AI_DATABASE_AGENT", + public_url="https://agent.example.com", + description=None, + ) + + payload = _build_v03_agent_card(card) + + assert payload["protocolVersion"] == "0.3" + assert payload["url"] == "https://agent.example.com/a2a/jsonrpc/" + assert "supportedInterfaces" not in payload + + +def test_discovery_route_serves_only_the_v03_agent_card(): + app = create_app( + team_name="ORACLE_AI_DATABASE_AGENT", + public_url="https://agent.example.com", + user="user", + password="password", + dsn="database", + ) + route = next( + route + for route in app.routes + if route.path == "/.well-known/agent-card.json" + ) + + response = asyncio.run(route.endpoint(None)) + payload = json.loads(response.body) + + assert payload["protocolVersion"] == "0.3" + assert payload["url"] == "https://agent.example.com/a2a/jsonrpc/" + assert "supportedInterfaces" not in payload From 426cf5a23a9c105bdca07d54858bc41c80f2dfbc Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Wed, 26 Aug 2026 13:49:40 -0700 Subject: [PATCH 02/31] Upgraded version to 1.5.0 --- src/select_ai/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/select_ai/version.py b/src/select_ai/version.py index 2691fab..e44a1fe 100644 --- a/src/select_ai/version.py +++ b/src/select_ai/version.py @@ -5,4 +5,4 @@ # http://oss.oracle.com/licenses/upl. # ----------------------------------------------------------------------------- -__version__ = "1.4.1" +__version__ = "1.5.0" From af6b9d9521ae0c23285e4e1d2ea368ab19e45c57 Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Wed, 26 Aug 2026 15:22:00 -0700 Subject: [PATCH 03/31] Install dependencies during test --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 4b35757..2dd3c6b 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -50,7 +50,7 @@ jobs: run: | python -m pip install --upgrade pip setuptools pip install pytest anyio - pip install -e . + pip install -e ".[cli]" - name: Wait for ADB Free Container run: | From 2f439ea8294690a667fd777a2bf1f87da349a152 Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Wed, 26 Aug 2026 21:53:56 -0700 Subject: [PATCH 04/31] Added samples for A2A client --- .dockerignore | 1 + .gcloudignore | 1 + .gitignore | 1 + pyproject.toml | 2 +- samples/README.md | 29 ++++++++++++++++ samples/a2a/blocking_task.py | 48 +++++++++++++++++++++++++++ samples/a2a/task_poll.py | 64 ++++++++++++++++++++++++++++++++++++ 7 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 samples/a2a/blocking_task.py create mode 100644 samples/a2a/task_poll.py diff --git a/.dockerignore b/.dockerignore index a4f01ef..439d722 100644 --- a/.dockerignore +++ b/.dockerignore @@ -21,3 +21,4 @@ wallets *.sso *.p12 *.zip +build/ diff --git a/.gcloudignore b/.gcloudignore index f2fd78d..f1312b1 100644 --- a/.gcloudignore +++ b/.gcloudignore @@ -28,3 +28,4 @@ samples/ *.png *.jpg *.jpeg +build/ diff --git a/.gitignore b/.gitignore index 1bf21f0..3c02fc7 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ sample_connect.py async_pipeline_test.py parquet.py local_sample +build/ diff --git a/pyproject.toml b/pyproject.toml index 0607211..1c39728 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ dependencies = [ cli = [ "click", "a2a-sdk[http-server]>=1.0.3", - "uvicorn>=0.30", + "uvicorn[standard]>=0.30", ] a2a = [ "select_ai[cli]", diff --git a/samples/README.md b/samples/README.md index 73b1a6b..858e66f 100644 --- a/samples/README.md +++ b/samples/README.md @@ -23,6 +23,35 @@ Some of the new samples use this optional environment variable: - `SELECT_AI_PROFILE_NAME` — existing profile for the conversation and supervised-team samples. +## A2A non-blocking task polling + +Start a Select AI A2A server before running these samples: + +```bash +select-ai a2a serve --team ORACLE_AI_DATABASE_AGENT --port 8000 +``` + +After starting a local A2A server, run the fixed sales-analysis prompt as a +non-blocking task and poll it until completion: + +```bash +python samples/a2a/task_poll.py +``` + +The sample sends the A2A v0.3 `message/send` request with +`configuration.blocking: false`, prints the returned task ID, and polls +`tasks/get`. Edit `ENDPOINT` or `PROMPT` at the top of the script if needed. + +To compare it with the default blocking behavior, run: + +```bash +python samples/a2a/blocking_task.py +``` + +This sample intentionally omits `configuration.blocking`. The server waits +for the database work to finish and returns the completed Task in the initial +`message/send` response; no polling is needed. + `SELECT_AI_DB_CONNECT_STRING` can be in any one of the following formats diff --git a/samples/a2a/blocking_task.py b/samples/a2a/blocking_task.py new file mode 100644 index 0000000..32112c2 --- /dev/null +++ b/samples/a2a/blocking_task.py @@ -0,0 +1,48 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# https://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +"""Send a blocking A2A request and receive its completed task.""" + +import json +import uuid +from urllib.request import Request, urlopen + +ENDPOINT = "http://127.0.0.1:8000/a2a/jsonrpc/" +PROMPT = "What were last month's sales by product category?" + + +# There is intentionally no "configuration": {"blocking": false} here. +# Omitting it is blocking by default, so this call waits for the database work +# to finish before the server returns the Task. +request = Request( + ENDPOINT, + data=json.dumps( + { + "jsonrpc": "2.0", + "id": str(uuid.uuid4()), + "method": "message/send", + "params": { + "message": { + "messageId": str(uuid.uuid4()), + "role": "user", + "parts": [{"kind": "text", "text": PROMPT}], + } + }, + } + ).encode(), + headers={"Content-Type": "application/json"}, + method="POST", +) + +with urlopen(request) as response: # noqa: S310 + body = json.load(response) +if "error" in body: + raise RuntimeError(body["error"]) + +task = body["result"] +print(f"Task {task['id']}: {task['status']['state']}") +print(json.dumps(task, indent=2)) diff --git a/samples/a2a/task_poll.py b/samples/a2a/task_poll.py new file mode 100644 index 0000000..8ee4cfa --- /dev/null +++ b/samples/a2a/task_poll.py @@ -0,0 +1,64 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# https://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +"""Start a non-blocking A2A task, then poll until it completes.""" + +import json +import time +import uuid +from urllib.request import Request, urlopen + +ENDPOINT = "http://127.0.0.1:8000/a2a/jsonrpc/" +PROMPT = "What were last month's sales by product category?" +TERMINAL_STATES = {"completed", "failed", "canceled", "rejected"} + + +def call(method, params): + """Make one A2A v0.3 JSON-RPC call.""" + request = Request( + ENDPOINT, + data=json.dumps( + { + "jsonrpc": "2.0", + "id": str(uuid.uuid4()), + "method": method, + "params": params, + } + ).encode(), + headers={"Content-Type": "application/json"}, + method="POST", + ) + with urlopen(request) as response: # noqa: S310 + body = json.load(response) + if "error" in body: + raise RuntimeError(body["error"]) + return body["result"] + + +# blocking=False returns immediately with a Task. Database work continues on +# the server while this client polls tasks/get. +task = call( + "message/send", + { + "message": { + "messageId": str(uuid.uuid4()), + "role": "user", + "parts": [{"kind": "text", "text": PROMPT}], + }, + "configuration": {"blocking": False}, + }, +) + +task_id = task["id"] +print(f"Task {task_id}: {task['status']['state']}") + +while task["status"]["state"] not in TERMINAL_STATES: + time.sleep(1) + task = call("tasks/get", {"id": task_id}) + print(f"Task {task_id}: {task['status']['state']}") + +print(json.dumps(task, indent=2)) From 7bf37661c1c6ce2fd7df8794a57a0f8478429366 Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Thu, 27 Aug 2026 09:22:00 -0700 Subject: [PATCH 05/31] Remove provider_endpoint while serializing ProfileAttributes. Database doesnt like it --- samples/profile_create_aws.py | 72 ++++++++++++++++++++++++++++++ samples/profile_create_azure.py | 79 +++++++++++++++++++++++++++++++++ samples/profile_create_gcp.py | 70 +++++++++++++++++++++++++++++ src/select_ai/async_profile.py | 4 +- src/select_ai/base_profile.py | 6 ++- src/select_ai/profile.py | 4 +- src/select_ai/provider.py | 56 ++++++++++++++++++++--- 7 files changed, 279 insertions(+), 12 deletions(-) create mode 100644 samples/profile_create_aws.py create mode 100644 samples/profile_create_azure.py create mode 100644 samples/profile_create_gcp.py diff --git a/samples/profile_create_aws.py b/samples/profile_create_aws.py new file mode 100644 index 0000000..40f6ab5 --- /dev/null +++ b/samples/profile_create_aws.py @@ -0,0 +1,72 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +"""Create an AWS Bedrock Select AI profile using only select_ai APIs. + +Before running, source test.env. The script grants the required database HTTP +access, creates/replaces AWS_CRED, and creates/replaces the profile. +""" + +import os +from pprint import pformat + +import select_ai + +AWS_HOST = "bedrock-runtime.us-east-1.amazonaws.com" +CREDENTIAL_NAME = "AWS_CRED" +PROFILE_NAME = "aws_bedrock_meta_prf" + +admin_user = os.environ["SELECT_AI_ADMIN_USER"] +admin_password = os.environ["SELECT_AI_ADMIN_PASSWORD"] +app_user = os.environ["SELECT_AI_USER"] +app_password = os.environ["SELECT_AI_PASSWORD"] +dsn = os.environ["SELECT_AI_DB_CONNECT_STRING"] + +# Equivalent to DBMS_NETWORK_ACL_ADMIN.APPEND_HOST_ACE(..., 'http'). +select_ai.connect(user=admin_user, password=admin_password, dsn=dsn) +try: + select_ai.grant_network_access( + users=app_user, + host=AWS_HOST, + privileges="http", + ) +finally: + select_ai.disconnect() + +select_ai.connect(user=app_user, password=app_password, dsn=dsn) +try: + select_ai.create_credential( + { + "credential_name": CREDENTIAL_NAME, + "username": os.environ["AWS_ACCESS_KEY_ID"], + "password": os.environ["AWS_SECRET_ACCESS_KEY"], + }, + replace=True, + ) + + profile = select_ai.Profile( + profile_name=PROFILE_NAME, + attributes=select_ai.ProfileAttributes( + credential_name=CREDENTIAL_NAME, + provider=select_ai.AWSProvider( + region="us-east-1", + model="meta.llama3-70b-instruct-v1:0", + embedding_model="amazon.titan-embed-text-v1", + ), + object_list=[{"owner": app_user, "name": "CUSTOMERS"}], + conversation=True, + temperature=1, + max_tokens=1500, + ), + replace=True, + ) + + print("Created profile:", profile.profile_name) + print(pformat(profile.get_attributes().dict(exclude_null=False))) + print("Chat response:", profile.chat("Reply with: AWS chat succeeded.")) +finally: + select_ai.disconnect() diff --git a/samples/profile_create_azure.py b/samples/profile_create_azure.py new file mode 100644 index 0000000..75a7ce8 --- /dev/null +++ b/samples/profile_create_azure.py @@ -0,0 +1,79 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +"""Create an Azure OpenAI Select AI profile using only select_ai APIs. + +Before running, source test.env. The script grants the required database HTTP +access, creates/replaces AZUREAI_CRED, and creates/replaces the profile. +""" + +import os +from pprint import pformat + +import select_ai + +AZURE_HOST = "adbst-ai-resource-japan-east.openai.azure.com" +AZURE_RESOURCE = "ADBST-AI-RESOURCE-JAPAN-EAST" +AZURE_DEPLOYMENT = "ADBST-AI-RESOURCE-JAPAN-EAST-DEPLOYMENT" +AZURE_EMBEDDING_DEPLOYMENT = ( + "ADBST-AI-RESOURCE-JAPAN-EAST-text-embedding-3-large" +) +CREDENTIAL_NAME = "AZUREAI_CRED" +PROFILE_NAME = "azureai_prf" + +admin_user = os.environ["SELECT_AI_ADMIN_USER"] +admin_password = os.environ["SELECT_AI_ADMIN_PASSWORD"] +app_user = os.environ["SELECT_AI_USER"] +app_password = os.environ["SELECT_AI_PASSWORD"] +dsn = os.environ["SELECT_AI_DB_CONNECT_STRING"] + +# Equivalent to DBMS_NETWORK_ACL_ADMIN.APPEND_HOST_ACE(..., 'http'). +select_ai.connect(user=admin_user, password=admin_password, dsn=dsn) +try: + select_ai.grant_network_access( + users=app_user, + host=AZURE_HOST, + privileges="http", + ) +finally: + select_ai.disconnect() + +select_ai.connect(user=app_user, password=app_password, dsn=dsn) +try: + select_ai.create_credential( + { + "credential_name": CREDENTIAL_NAME, + "username": "azure", + "password": os.environ["AZURE_API_KEY"], + }, + replace=True, + ) + + profile = select_ai.Profile( + profile_name=PROFILE_NAME, + attributes=select_ai.ProfileAttributes( + credential_name=CREDENTIAL_NAME, + provider=select_ai.AzureProvider( + azure_resource_name=AZURE_RESOURCE, + azure_deployment_name=AZURE_DEPLOYMENT, + azure_embedding_deployment_name=AZURE_EMBEDDING_DEPLOYMENT, + ), + object_list=[{"owner": app_user, "name": "CUSTOMERS"}], + conversation=True, + temperature=1, + max_tokens=1500, + seed=20, + ), + replace=True, + ) + p = select_ai.Profile.fetch(profile_name=PROFILE_NAME) + print(p) + print("Created profile:", profile.profile_name) + print(pformat(profile.get_attributes().dict(exclude_null=False))) + print("Chat response:", profile.chat("Reply with: Azure chat succeeded.")) +finally: + select_ai.disconnect() diff --git a/samples/profile_create_gcp.py b/samples/profile_create_gcp.py new file mode 100644 index 0000000..5db5f95 --- /dev/null +++ b/samples/profile_create_gcp.py @@ -0,0 +1,70 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +"""Create a Google Gemini Select AI profile using only select_ai APIs. + +Before running, source test.env. The script grants the required database HTTP +access, creates/replaces GOOGLE_CRED, and creates/replaces the profile. +""" + +import os +from pprint import pformat + +import select_ai + +GCP_HOST = "generativelanguage.googleapis.com" +CREDENTIAL_NAME = "GOOGLE_CRED" +PROFILE_NAME = "google_gemini_3_6_flash" + +admin_user = os.environ["SELECT_AI_ADMIN_USER"] +admin_password = os.environ["SELECT_AI_ADMIN_PASSWORD"] +app_user = os.environ["SELECT_AI_USER"] +app_password = os.environ["SELECT_AI_PASSWORD"] +dsn = os.environ["SELECT_AI_DB_CONNECT_STRING"] + +# Equivalent to DBMS_NETWORK_ACL_ADMIN.APPEND_HOST_ACE(..., 'http'). +select_ai.connect(user=admin_user, password=admin_password, dsn=dsn) +try: + select_ai.grant_network_access( + users=app_user, + host=GCP_HOST, + privileges="http", + ) +finally: + select_ai.disconnect() + +select_ai.connect(user=app_user, password=app_password, dsn=dsn) +try: + select_ai.create_credential( + { + "credential_name": CREDENTIAL_NAME, + "username": "GOOGLE", + "password": os.environ["GOOGLE_API_KEY"], + }, + replace=True, + ) + + profile = select_ai.Profile( + profile_name=PROFILE_NAME, + attributes=select_ai.ProfileAttributes( + credential_name=CREDENTIAL_NAME, + provider=select_ai.GoogleProvider( + embedding_model="gemini-embedding-001", + model="gemini-3.6-flash", + ), + temperature=1, + max_tokens=1500, + seed=20, + ), + replace=True, + ) + + print("Created profile:", profile.profile_name) + print(pformat(profile.get_attributes().dict(exclude_null=False))) + print("Chat response:", profile.chat("Reply with: GCP chat succeeded.")) +finally: + select_ai.disconnect() diff --git a/src/select_ai/async_profile.py b/src/select_ai/async_profile.py index 43ce4cb..4efaaca 100644 --- a/src/select_ai/async_profile.py +++ b/src/select_ai/async_profile.py @@ -191,8 +191,8 @@ async def set_attribute( """ self.attributes.set_attribute(attribute_name, attribute_value) if isinstance(attribute_value, Provider): - for k, v in attribute_value.dict().items(): - await self._set_attribute(k, v) + for k, v in attribute_value.profile_dict().items(): + await self._set_attribute(Provider.key_alias(k), v) else: await self._set_attribute(attribute_name, attribute_value) diff --git a/src/select_ai/base_profile.py b/src/select_ai/base_profile.py index 02103dd..2a62142 100644 --- a/src/select_ai/base_profile.py +++ b/src/select_ai/base_profile.py @@ -50,6 +50,8 @@ class ProfileAttributes(SelectAIDataClass): most relevant tables or all tables to the LLM. Supported values are - 'automated' and 'all' :param select_ai.Provider provider: AI Provider + :param int seed: Signed 64-bit integer used to make model output more + reproducible when the provider supports it. :param str stop_tokens: The generated text will be terminated at the beginning of the earliest stop sequence. Sequence will be incorporated into the text. The attribute value must be a valid array of string values @@ -75,7 +77,7 @@ class ProfileAttributes(SelectAIDataClass): object_list: Optional[List[Mapping]] = None object_list_mode: Optional[str] = None provider: Optional[Provider] = None - seed: Optional[str] = None + seed: Optional[int] = None stop_tokens: Optional[str] = None streaming: Optional[str] = None temperature: Optional[float] = None @@ -92,7 +94,7 @@ def json(self, exclude_null=True): attributes = {} for k, v in self.dict(exclude_null=exclude_null).items(): if isinstance(v, Provider): - for provider_k, provider_v in v.dict( + for provider_k, provider_v in v.profile_dict( exclude_null=exclude_null ).items(): attributes[Provider.key_alias(provider_k)] = provider_v diff --git a/src/select_ai/profile.py b/src/select_ai/profile.py index 69a7c5b..695fbc4 100644 --- a/src/select_ai/profile.py +++ b/src/select_ai/profile.py @@ -165,8 +165,8 @@ def set_attribute( """ self.attributes.set_attribute(attribute_name, attribute_value) if isinstance(attribute_value, Provider): - for k, v in attribute_value.dict().items(): - self._set_attribute(k, v) + for k, v in attribute_value.profile_dict().items(): + self._set_attribute(Provider.key_alias(k), v) else: self._set_attribute(attribute_name, attribute_value) diff --git a/src/select_ai/provider.py b/src/select_ai/provider.py index dd00cf6..83e547a 100644 --- a/src/select_ai/provider.py +++ b/src/select_ai/provider.py @@ -86,6 +86,23 @@ def keys(cls): "aws_apiformat", } + def profile_dict(self, exclude_null=True): + """Return provider attributes suitable for a DBMS_CLOUD_AI profile. + + The result contains only values held by this provider instance. In + particular, native provider endpoints remain available for network + access configuration but are omitted from database profile payloads. + OpenAI and endpoint-only custom providers retain provider_endpoint. + """ + attributes = self.dict(exclude_null=exclude_null) + if not self.should_include_provider_endpoint(): + attributes.pop("provider_endpoint", None) + return attributes + + def should_include_provider_endpoint(self) -> bool: + """Whether to include provider_endpoint in a DBMS_CLOUD_AI profile.""" + return True + @dataclass class AzureProvider(Provider): @@ -106,7 +123,13 @@ class AzureProvider(Provider): def __post_init__(self): super().__post_init__() - self.provider_endpoint = f"{self.azure_resource_name}.openai.azure.com" + if self.provider_endpoint is None: + self.provider_endpoint = ( + f"{self.azure_resource_name}.openai.azure.com" + ) + + def should_include_provider_endpoint(self) -> bool: + return False @dataclass @@ -140,6 +163,9 @@ class OCIGenAIProvider(Provider): oci_endpoint_id: Optional[str] = None oci_runtimetype: Optional[str] = None + def should_include_provider_endpoint(self) -> bool: + return False + @dataclass class CohereProvider(Provider): @@ -148,7 +174,10 @@ class CohereProvider(Provider): """ provider_name: str = COHERE - provider_endpoint = "api.cohere.ai" + provider_endpoint: Optional[str] = "api.cohere.ai" + + def should_include_provider_endpoint(self) -> bool: + return False @dataclass @@ -158,7 +187,10 @@ class GoogleProvider(Provider): """ provider_name: str = GOOGLE - provider_endpoint = "generativelanguage.googleapis.com" + provider_endpoint: Optional[str] = "generativelanguage.googleapis.com" + + def should_include_provider_endpoint(self) -> bool: + return False @dataclass @@ -168,7 +200,10 @@ class HuggingFaceProvider(Provider): """ provider_name: str = HUGGINGFACE - provider_endpoint = "api-inference.huggingface.co" + provider_endpoint: Optional[str] = "api-inference.huggingface.co" + + def should_include_provider_endpoint(self) -> bool: + return False @dataclass @@ -182,7 +217,13 @@ class AWSProvider(Provider): def __post_init__(self): super().__post_init__() - self.provider_endpoint = f"bedrock-runtime.{self.region}.amazonaws.com" + if self.provider_endpoint is None: + self.provider_endpoint = ( + f"bedrock-runtime.{self.region}.amazonaws.com" + ) + + def should_include_provider_endpoint(self) -> bool: + return False @dataclass @@ -192,4 +233,7 @@ class AnthropicProvider(Provider): """ provider_name: str = ANTHROPIC - provider_endpoint = "api.anthropic.com" + provider_endpoint: Optional[str] = "api.anthropic.com" + + def should_include_provider_endpoint(self) -> bool: + return False From 7a30f57a3f18739432233dc8028c054fdc315b05 Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Thu, 27 Aug 2026 10:05:35 -0700 Subject: [PATCH 06/31] Added enable/disable methods in profile --- src/select_ai/async_profile.py | 24 +++++++++++++++++++++++ src/select_ai/profile.py | 24 +++++++++++++++++++++++ tests/profiles/test_1200_profile.py | 22 +++++++++++++++++++++ tests/profiles/test_1300_profile_async.py | 22 +++++++++++++++++++++ 4 files changed, 92 insertions(+) diff --git a/src/select_ai/async_profile.py b/src/select_ai/async_profile.py index 4efaaca..7b3d778 100644 --- a/src/select_ai/async_profile.py +++ b/src/select_ai/async_profile.py @@ -275,6 +275,30 @@ async def delete(self, force=False) -> None: """ await self._delete(profile_name=self.profile_name, force=force) + async def enable(self) -> None: + """Asynchronously enable this AI profile in the database. + + :return: None + :raises: oracledb.DatabaseError + """ + async with async_cursor() as cr: + await cr.callproc( + "DBMS_CLOUD_AI.ENABLE_PROFILE", + keyword_parameters={"profile_name": self.profile_name}, + ) + + async def disable(self) -> None: + """Asynchronously disable this AI profile in the database. + + :return: None + :raises: oracledb.DatabaseError + """ + async with async_cursor() as cr: + await cr.callproc( + "DBMS_CLOUD_AI.DISABLE_PROFILE", + keyword_parameters={"profile_name": self.profile_name}, + ) + @classmethod async def delete_profile(cls, profile_name: str, force: bool = False): """Asynchronously deletes an AI profile from the database diff --git a/src/select_ai/profile.py b/src/select_ai/profile.py index 695fbc4..de2cb37 100644 --- a/src/select_ai/profile.py +++ b/src/select_ai/profile.py @@ -247,6 +247,30 @@ def delete(self, force=False) -> None: """ self._delete(profile_name=self.profile_name, force=force) + def enable(self) -> None: + """Enable this AI profile in the database. + + :return: None + :raises: oracledb.DatabaseError + """ + with cursor() as cr: + cr.callproc( + "DBMS_CLOUD_AI.ENABLE_PROFILE", + keyword_parameters={"profile_name": self.profile_name}, + ) + + def disable(self) -> None: + """Disable this AI profile in the database. + + :return: None + :raises: oracledb.DatabaseError + """ + with cursor() as cr: + cr.callproc( + "DBMS_CLOUD_AI.DISABLE_PROFILE", + keyword_parameters={"profile_name": self.profile_name}, + ) + @classmethod def delete_profile(cls, profile_name: str, force: bool = False): """Class method to delete an AI profile from the database diff --git a/tests/profiles/test_1200_profile.py b/tests/profiles/test_1200_profile.py index 16d5626..cfaab13 100644 --- a/tests/profiles/test_1200_profile.py +++ b/tests/profiles/test_1200_profile.py @@ -382,3 +382,25 @@ def test_1218(python_gen_ai_profile): text="Thank you", source_language="en", target_language="de" ) assert response == "Danke" + + +def test_1219_profile_status(python_gen_ai_profile, cursor): + """Disable and re-enable a profile.""" + try: + python_gen_ai_profile.disable() + cursor.execute( + "SELECT status FROM USER_CLOUD_AI_PROFILES " + "WHERE profile_name = :profile_name", + profile_name=python_gen_ai_profile.profile_name, + ) + assert cursor.fetchone()[0] == "DISABLED" + finally: + # Keep the shared fixture usable if the status assertion fails. + python_gen_ai_profile.enable() + + cursor.execute( + "SELECT status FROM USER_CLOUD_AI_PROFILES " + "WHERE profile_name = :profile_name", + profile_name=python_gen_ai_profile.profile_name, + ) + assert cursor.fetchone()[0] == "ENABLED" diff --git a/tests/profiles/test_1300_profile_async.py b/tests/profiles/test_1300_profile_async.py index 2a34d14..0de11ee 100644 --- a/tests/profiles/test_1300_profile_async.py +++ b/tests/profiles/test_1300_profile_async.py @@ -475,3 +475,25 @@ async def test_1318(python_gen_ai_profile): text="Thank you", source_language="en", target_language="de" ) assert response == "Danke" + + +async def test_1319_profile_status(python_gen_ai_profile, async_cursor): + """Disable and re-enable an async profile.""" + try: + await python_gen_ai_profile.disable() + await async_cursor.execute( + "SELECT status FROM USER_CLOUD_AI_PROFILES " + "WHERE profile_name = :profile_name", + profile_name=python_gen_ai_profile.profile_name, + ) + assert (await async_cursor.fetchone())[0] == "DISABLED" + finally: + # Keep the shared fixture usable if the status assertion fails. + await python_gen_ai_profile.enable() + + await async_cursor.execute( + "SELECT status FROM USER_CLOUD_AI_PROFILES " + "WHERE profile_name = :profile_name", + profile_name=python_gen_ai_profile.profile_name, + ) + assert (await async_cursor.fetchone())[0] == "ENABLED" From 376d80ba284f93b0a1a51695188facfcc8e6a4fc Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Thu, 27 Aug 2026 10:19:23 -0700 Subject: [PATCH 07/31] source_language and target_language are optional params to translate() and can be passed during profile creation --- doc/source/user_guide/profile_attributes.rst | 5 +++++ src/select_ai/async_profile.py | 14 ++++++++++---- src/select_ai/base_profile.py | 8 ++++++++ src/select_ai/profile.py | 14 ++++++++++---- 4 files changed, 33 insertions(+), 8 deletions(-) diff --git a/doc/source/user_guide/profile_attributes.rst b/doc/source/user_guide/profile_attributes.rst index 4adf67a..c725aba 100644 --- a/doc/source/user_guide/profile_attributes.rst +++ b/doc/source/user_guide/profile_attributes.rst @@ -74,6 +74,11 @@ Attribute groups - Tunes model generation behavior. * - ``conversation`` - Enables conversation history for context-aware chat workflows. + * - ``source_language``, ``target_language`` + - Set default languages for ``Profile.translate()`` and + ``AsyncProfile.translate()``. If no source language is configured or + supplied per call, the provider detects it. A target language must be + supplied either per call or in the profile. * - ``vector_index_name``, ``enable_sources``, ``enable_source_offsets``, ``enable_custom_source_uri`` - Configures retrieval-augmented generation and source reporting for diff --git a/src/select_ai/async_profile.py b/src/select_ai/async_profile.py index 7b3d778..91572fe 100644 --- a/src/select_ai/async_profile.py +++ b/src/select_ai/async_profile.py @@ -814,14 +814,20 @@ async def run_pipeline( return responses async def translate( - self, text: str, source_language: str, target_language: str + self, + text: str, + source_language: Optional[str] = None, + target_language: Optional[str] = None, ) -> Union[str, None]: """ - Translate a text using a source language and a target language + Translate text using the supplied languages or the profile defaults. :param str text: Text to translate - :param str source_language: Source language - :param str target_language: Target language + :param str source_language: Source language. When omitted, the profile + value is used; if the profile does not define one, the provider + detects the source language. + :param str target_language: Target language. When omitted, the profile + value is used. :return: str """ parameters = { diff --git a/src/select_ai/base_profile.py b/src/select_ai/base_profile.py index 2a62142..41f9b90 100644 --- a/src/select_ai/base_profile.py +++ b/src/select_ai/base_profile.py @@ -52,6 +52,9 @@ class ProfileAttributes(SelectAIDataClass): :param select_ai.Provider provider: AI Provider :param int seed: Signed 64-bit integer used to make model output more reproducible when the provider supports it. + :param str source_language: Default language of text passed to the + translate operation. If omitted, the translation provider can detect the + source language. :param str stop_tokens: The generated text will be terminated at the beginning of the earliest stop sequence. Sequence will be incorporated into the text. The attribute value must be a valid array of string values @@ -59,6 +62,9 @@ class ProfileAttributes(SelectAIDataClass): :param float temperature: Temperature is a non-negative float number used to tune the degree of randomness. Lower temperatures mean less random generations. + :param str target_language: Default language into which text is translated. + This is required by the database when no target language is supplied to + the translate operation. :param str vector_index_name: Name of the vector index """ @@ -78,9 +84,11 @@ class ProfileAttributes(SelectAIDataClass): object_list_mode: Optional[str] = None provider: Optional[Provider] = None seed: Optional[int] = None + source_language: Optional[str] = None stop_tokens: Optional[str] = None streaming: Optional[str] = None temperature: Optional[float] = None + target_language: Optional[str] = None vector_index_name: Optional[str] = None def __post_init__(self): diff --git a/src/select_ai/profile.py b/src/select_ai/profile.py index de2cb37..23f2b72 100644 --- a/src/select_ai/profile.py +++ b/src/select_ai/profile.py @@ -737,14 +737,20 @@ def generate_synthetic_data( ) def translate( - self, text: str, source_language: str, target_language: str + self, + text: str, + source_language: Optional[str] = None, + target_language: Optional[str] = None, ) -> Union[str, None]: """ - Translate a text using a source language and a target language + Translate text using the supplied languages or the profile defaults. :param str text: Text to translate - :param str source_language: Source language - :param str target_language: Target language + :param str source_language: Source language. When omitted, the profile + value is used; if the profile does not define one, the provider + detects the source language. + :param str target_language: Target language. When omitted, the profile + value is used. :return: str """ parameters = { From 82c6b2d73f500262a75bf8df89334b43516334f7 Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Thu, 27 Aug 2026 10:40:03 -0700 Subject: [PATCH 08/31] Removed defaults in SyntheticDataParams. Use DB side defaults --- doc/source/user_guide/synthetic_data.rst | 4 +++- src/select_ai/synthetic_data.py | 13 ++++++------ tests/gsd/test_2000_synthetic_data.py | 25 ++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/doc/source/user_guide/synthetic_data.rst b/doc/source/user_guide/synthetic_data.rst index c607360..726b13f 100644 --- a/doc/source/user_guide/synthetic_data.rst +++ b/doc/source/user_guide/synthetic_data.rst @@ -72,7 +72,9 @@ Use ``SyntheticDataParams`` to control how generation is performed: ``sample_rows`` controls how many existing rows are used as examples for the model. ``table_statistics`` and ``comments`` include additional table metadata. ``priority`` controls resource priority for generation work; supported values -are ``HIGH``, ``MEDIUM``, and ``LOW``. +are ``HIGH``, ``MEDIUM``, and ``LOW``. All parameters are optional. Parameters +that are not supplied are omitted from the request, allowing the database to +apply its defaults. Sync and async APIs =================== diff --git a/src/select_ai/synthetic_data.py b/src/select_ai/synthetic_data.py index a047af5..f5e2d85 100644 --- a/src/select_ai/synthetic_data.py +++ b/src/select_ai/synthetic_data.py @@ -20,22 +20,23 @@ class SyntheticDataParams(SelectAIDataClass): to guide the LLM in data generation :param bool table_statistics: Enable or disable the use of table - statistics information. Default value is False + statistics information. When omitted, the database default is used. :param str priority: Assign a priority value that defines the number of parallel requests sent to the LLM for generating synthetic data. Tasks with a higher priority will consume more database resources and - complete faster. Possible values are: HIGH, MEDIUM, LOW + complete faster. Possible values are: HIGH, MEDIUM, LOW. When omitted, + the database default is used. :param bool comments: Enable or disable sending comments to the LLM to - guide data generation. Default value is False + guide data generation. When omitted, the database default is used. """ sample_rows: Optional[int] = None - table_statistics: Optional[bool] = False - priority: Optional[str] = "HIGH" - comments: Optional[bool] = False + table_statistics: Optional[bool] = None + priority: Optional[str] = None + comments: Optional[bool] = None @dataclass diff --git a/tests/gsd/test_2000_synthetic_data.py b/tests/gsd/test_2000_synthetic_data.py index 4662dbd..13c4b59 100644 --- a/tests/gsd/test_2000_synthetic_data.py +++ b/tests/gsd/test_2000_synthetic_data.py @@ -201,3 +201,28 @@ def test_2009_params_json_string_is_coerced(): assert isinstance(attributes.params, SyntheticDataParams) assert attributes.params.sample_rows == 1 assert attributes.params.table_statistics is True + + +def test_2010_params_omit_unspecified_values(): + """Only explicitly supplied parameters are serialized.""" + params = SyntheticDataParams(sample_rows=1) + + assert params.dict() == {"sample_rows": 1} + + +def test_2011_empty_params_serialize_as_empty_json_object(): + """An empty params object is serialized as an empty JSON object.""" + attributes = SyntheticDataAttributes( + object_name="people", params=SyntheticDataParams() + ) + + assert attributes.prepare()["params"] == "{}" + + +def test_2012_generate_with_empty_params(synthetic_profile): + """The database accepts an empty JSON object for params.""" + attributes = _build_attributes(params=SyntheticDataParams()) + + result = synthetic_profile.generate_synthetic_data(attributes) + + assert result is None From 772137631b14f09ee8f9460bc37fb19eb931b2d0 Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Thu, 27 Aug 2026 12:57:44 -0700 Subject: [PATCH 09/31] Added History APIs to query USER_AI_AGENT_*_HISTORY views --- doc/source/user_guide/agent.rst | 38 +++ doc/source/user_guide/async_agent.rst | 14 + samples/agent/async/agent_history_list.py | 46 ++++ samples/agent/history_list.py | 32 +++ src/select_ai/agent/__init__.py | 11 + src/select_ai/agent/history.py | 295 ++++++++++++++++++++++ src/select_ai/agent/sql.py | 34 +++ tests/agents/test_3300_teams.py | 83 +++++- tests/agents/test_3700_async_teams.py | 96 ++++++- 9 files changed, 643 insertions(+), 6 deletions(-) create mode 100644 samples/agent/async/agent_history_list.py create mode 100644 samples/agent/history_list.py create mode 100644 src/select_ai/agent/history.py diff --git a/doc/source/user_guide/agent.rst b/doc/source/user_guide/agent.rst index aad3c00..d86e366 100644 --- a/doc/source/user_guide/agent.rst +++ b/doc/source/user_guide/agent.rst @@ -446,6 +446,44 @@ operations. .. latex:clearpage:: +************* +Agent history +************* + +``TeamHistory``, ``TaskHistory``, and ``ToolHistory`` provide typed, +read-only access to the current user's Select AI Agent history views. They +query only ``USER_AI_AGENT_TEAM_HISTORY``, ``USER_AI_AGENT_TASK_HISTORY``, +and ``USER_AI_AGENT_TOOL_HISTORY`` respectively. Results are yielded newest +first. Tool ``input`` and ``output`` values are decoded to Python objects when +they contain valid JSON; other CLOB payloads are returned as strings. + +.. code-block:: python + + from select_ai.agent import TaskHistory, TeamHistory, ToolHistory + + for run in TeamHistory.list(team_name="MOVIE_AGENT_TEAM", limit=10): + print(run.team_exec_id, run.state) + + for run in TaskHistory.list(team_exec_id=""): + print(run.task_name, run.result) + + for call in ToolHistory.list(tool_name="MOVIE_SQL_TOOL", limit=20): + print(call.input, call.output) + +The sample retrieves a team's latest execution and uses its ``team_exec_id`` +to retrieve the associated task and tool history. + +.. autoclass:: select_ai.agent.TeamHistory + :members: + +.. autoclass:: select_ai.agent.TaskHistory + :members: + +.. autoclass:: select_ai.agent.ToolHistory + :members: + +.. latex:clearpage:: + ***************** AI agent examples ***************** diff --git a/doc/source/user_guide/async_agent.rst b/doc/source/user_guide/async_agent.rst index df17e2d..5f48f42 100644 --- a/doc/source/user_guide/async_agent.rst +++ b/doc/source/user_guide/async_agent.rst @@ -5,6 +5,20 @@ use ``asyncio`` and ``select_ai.async_connect()`` or ``select_ai.create_pool_async()``. +The history API follows the same pattern. ``AsyncTeamHistory``, +``AsyncTaskHistory``, and ``AsyncToolHistory`` query only the current user's +history views and yield typed events newest first. + +.. code-block:: python + + from select_ai.agent import AsyncToolHistory + + async for call in AsyncToolHistory.list(limit=10): + print(call.tool_name, call.output) + +The async sample retrieves a team's latest execution and uses its +``team_exec_id`` to retrieve the associated task and tool history. + The async agent object model mirrors the synchronous agent object model: .. list-table:: Sync and async agent APIs diff --git a/samples/agent/async/agent_history_list.py b/samples/agent/async/agent_history_list.py new file mode 100644 index 0000000..b9f84f7 --- /dev/null +++ b/samples/agent/async/agent_history_list.py @@ -0,0 +1,46 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +"""Asynchronously debug the latest execution of one agent team.""" + +import asyncio +import os +from pprint import pprint + +import select_ai +from select_ai.agent import ( + AsyncTaskHistory, + AsyncTeamHistory, + AsyncToolHistory, +) + +user = os.getenv("SELECT_AI_USER") +password = os.getenv("SELECT_AI_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +team_name = "ORACLE_AI_DATABASE_AGENT" + + +async def main(): + await select_ai.async_connect(user=user, password=password, dsn=dsn) + + # Replace team_name with team_exec_id when the application has recorded it. + async for team_run in AsyncTeamHistory.list(team_name=team_name, limit=1): + pprint(team_run) + + # team_exec_id scopes the remaining history to the same execution. + async for task_run in AsyncTaskHistory.list( + team_exec_id=team_run.team_exec_id + ): + pprint(task_run) + + async for tool_run in AsyncToolHistory.list( + team_exec_id=team_run.team_exec_id + ): + pprint(tool_run) + + +asyncio.run(main()) diff --git a/samples/agent/history_list.py b/samples/agent/history_list.py new file mode 100644 index 0000000..94dc555 --- /dev/null +++ b/samples/agent/history_list.py @@ -0,0 +1,32 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +"""Debug the latest execution of one agent team.""" + +import os +from pprint import pprint + +import select_ai +from select_ai.agent import TaskHistory, TeamHistory, ToolHistory + +user = os.getenv("SELECT_AI_USER") +password = os.getenv("SELECT_AI_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +team_name = "ORACLE_AI_DATABASE_AGENT" + +select_ai.connect(user=user, password=password, dsn=dsn) + +# Replace team_name with team_exec_id when the application has recorded it. +for team_run in TeamHistory.list(team_name=team_name, limit=1): + pprint(team_run) + + # team_exec_id scopes the remaining history to the same execution. + for task_run in TaskHistory.list(team_exec_id=team_run.team_exec_id): + pprint(task_run) + + for tool_run in ToolHistory.list(team_exec_id=team_run.team_exec_id): + pprint(tool_run) diff --git a/src/select_ai/agent/__init__.py b/src/select_ai/agent/__init__.py index 6f29cc8..9cb4ae2 100644 --- a/src/select_ai/agent/__init__.py +++ b/src/select_ai/agent/__init__.py @@ -8,6 +8,17 @@ from .core import Agent, AgentAttributes, AsyncAgent from .definition import async_get_definition, get_definition +from .history import ( + AsyncTaskHistory, + AsyncTeamHistory, + AsyncToolHistory, + TaskHistory, + TaskHistoryEvent, + TeamHistory, + TeamHistoryEvent, + ToolHistory, + ToolHistoryEvent, +) from .task import AsyncTask, Task, TaskAttributes from .team import AsyncTeam, Team, TeamAttributes from .tool import ( diff --git a/src/select_ai/agent/history.py b/src/select_ai/agent/history.py new file mode 100644 index 0000000..2cda87b --- /dev/null +++ b/src/select_ai/agent/history.py @@ -0,0 +1,295 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +"""Typed access to the current user's Select AI Agent history views.""" + +import json +from dataclasses import dataclass +from datetime import datetime +from typing import ( + Any, + AsyncGenerator, + Iterator, + Optional, + Sequence, + Type, + TypeVar, +) + +import oracledb + +from select_ai._abc import SelectAIDataClass +from select_ai.agent.sql import ( + LIST_USER_AI_AGENT_TASK_HISTORY, + LIST_USER_AI_AGENT_TEAM_HISTORY, + LIST_USER_AI_AGENT_TOOL_HISTORY, +) +from select_ai.db import async_cursor, cursor + + +@dataclass +class TeamHistoryEvent(SelectAIDataClass): + """One row from ``USER_AI_AGENT_TEAM_HISTORY``.""" + + team_exec_id: str + team_name: str + state: str + start_date: Optional[datetime] = None + end_date: Optional[datetime] = None + conversation_id: Optional[str] = None + params: Optional[str] = None + + +@dataclass +class TaskHistoryEvent(SelectAIDataClass): + """One row from ``USER_AI_AGENT_TASK_HISTORY``.""" + + team_exec_id: str + team_name: str + task_order: Optional[int] + agent_name: str + task_name: Optional[str] + conversation_params: Optional[str] + input: Optional[str] + result: Optional[str] + state: str + start_date: Optional[datetime] + end_date: Optional[datetime] + + +@dataclass +class ToolHistoryEvent(SelectAIDataClass): + """One row from ``USER_AI_AGENT_TOOL_HISTORY``.""" + + invocation_id: int + team_exec_id: str + task_order: Optional[int] + tool_name: Optional[str] + agent_name: Optional[str] + task_name: Optional[str] + start_date: Optional[datetime] + end_date: Optional[datetime] + input: Optional[Any] + output: Optional[Any] + tool_output: Optional[str] + + def __post_init__(self): + super().__post_init__() + self.input = _load_json(self.input) + self.output = _load_json(self.output) + + +HistoryEvent = TypeVar("HistoryEvent", bound=SelectAIDataClass) + + +def _load_json(value: Optional[str]) -> Any: + if value is None: + return None + try: + return json.loads(value) + except (TypeError, json.JSONDecodeError): + return value + + +def _validate_limit(limit: Optional[int]) -> None: + if limit is not None and (not isinstance(limit, int) or limit < 1): + raise ValueError("'limit' must be a positive integer or None") + + +def _read_lobs(row: Sequence[object]) -> tuple: + return tuple( + value.read() if isinstance(value, oracledb.LOB) else value + for value in row + ) + + +async def _async_read_lobs(row: Sequence[object]) -> tuple: + values = [] + for value in row: + if isinstance(value, oracledb.AsyncLOB): + value = await value.read() + values.append(value) + return tuple(values) + + +def _events( + query: str, + event_type: Type[HistoryEvent], + parameters: dict, + limit: Optional[int], +) -> Iterator[HistoryEvent]: + _validate_limit(limit) + with cursor() as cr: + cr.execute(query, parameters) + count = 0 + for row in cr: + yield event_type(*_read_lobs(row)) + count += 1 + if limit is not None and count >= limit: + break + + +async def _async_events( + query: str, + event_type: Type[HistoryEvent], + parameters: dict, + limit: Optional[int], +) -> AsyncGenerator[HistoryEvent, None]: + _validate_limit(limit) + async with async_cursor() as cr: + await cr.execute(query, parameters) + count = 0 + async for row in cr: + yield event_type(*await _async_read_lobs(row)) + count += 1 + if limit is not None and count >= limit: + break + + +class TeamHistory: + """Read runs from ``USER_AI_AGENT_TEAM_HISTORY``.""" + + @classmethod + def list( + cls, + team_name: Optional[str] = None, + team_exec_id: Optional[str] = None, + limit: Optional[int] = None, + ) -> Iterator[TeamHistoryEvent]: + """Yield team runs ordered from newest to oldest.""" + yield from _events( + LIST_USER_AI_AGENT_TEAM_HISTORY, + TeamHistoryEvent, + {"team_name": team_name, "team_exec_id": team_exec_id}, + limit, + ) + + +class TaskHistory: + """Read task runs from ``USER_AI_AGENT_TASK_HISTORY``.""" + + @classmethod + def list( + cls, + team_name: Optional[str] = None, + task_name: Optional[str] = None, + agent_name: Optional[str] = None, + team_exec_id: Optional[str] = None, + limit: Optional[int] = None, + ) -> Iterator[TaskHistoryEvent]: + """Yield task runs ordered from newest to oldest.""" + yield from _events( + LIST_USER_AI_AGENT_TASK_HISTORY, + TaskHistoryEvent, + { + "team_name": team_name, + "task_name": task_name, + "agent_name": agent_name, + "team_exec_id": team_exec_id, + }, + limit, + ) + + +class ToolHistory: + """Read tool calls from ``USER_AI_AGENT_TOOL_HISTORY``.""" + + @classmethod + def list( + cls, + tool_name: Optional[str] = None, + task_name: Optional[str] = None, + agent_name: Optional[str] = None, + team_exec_id: Optional[str] = None, + limit: Optional[int] = None, + ) -> Iterator[ToolHistoryEvent]: + """Yield tool calls ordered from newest to oldest.""" + yield from _events( + LIST_USER_AI_AGENT_TOOL_HISTORY, + ToolHistoryEvent, + { + "tool_name": tool_name, + "task_name": task_name, + "agent_name": agent_name, + "team_exec_id": team_exec_id, + }, + limit, + ) + + +class AsyncTeamHistory: + """Asynchronously read runs from ``USER_AI_AGENT_TEAM_HISTORY``.""" + + @classmethod + async def list( + cls, + team_name: Optional[str] = None, + team_exec_id: Optional[str] = None, + limit: Optional[int] = None, + ) -> AsyncGenerator[TeamHistoryEvent, None]: + """Yield team runs ordered from newest to oldest.""" + async for event in _async_events( + LIST_USER_AI_AGENT_TEAM_HISTORY, + TeamHistoryEvent, + {"team_name": team_name, "team_exec_id": team_exec_id}, + limit, + ): + yield event + + +class AsyncTaskHistory: + """Asynchronously read task runs from ``USER_AI_AGENT_TASK_HISTORY``.""" + + @classmethod + async def list( + cls, + team_name: Optional[str] = None, + task_name: Optional[str] = None, + agent_name: Optional[str] = None, + team_exec_id: Optional[str] = None, + limit: Optional[int] = None, + ) -> AsyncGenerator[TaskHistoryEvent, None]: + """Yield task runs ordered from newest to oldest.""" + async for event in _async_events( + LIST_USER_AI_AGENT_TASK_HISTORY, + TaskHistoryEvent, + { + "team_name": team_name, + "task_name": task_name, + "agent_name": agent_name, + "team_exec_id": team_exec_id, + }, + limit, + ): + yield event + + +class AsyncToolHistory: + """Asynchronously read tool calls from ``USER_AI_AGENT_TOOL_HISTORY``.""" + + @classmethod + async def list( + cls, + tool_name: Optional[str] = None, + task_name: Optional[str] = None, + agent_name: Optional[str] = None, + team_exec_id: Optional[str] = None, + limit: Optional[int] = None, + ) -> AsyncGenerator[ToolHistoryEvent, None]: + """Yield tool calls ordered from newest to oldest.""" + async for event in _async_events( + LIST_USER_AI_AGENT_TOOL_HISTORY, + ToolHistoryEvent, + { + "tool_name": tool_name, + "task_name": task_name, + "agent_name": agent_name, + "team_exec_id": team_exec_id, + }, + limit, + ): + yield event diff --git a/src/select_ai/agent/sql.py b/src/select_ai/agent/sql.py index b56cf8c..126f7c4 100644 --- a/src/select_ai/agent/sql.py +++ b/src/select_ai/agent/sql.py @@ -80,3 +80,37 @@ FROM USER_AI_AGENT_TEAMS t WHERE REGEXP_LIKE(t.AGENT_TEAM_NAME, :team_name_pattern, 'i') """ + + +LIST_USER_AI_AGENT_TEAM_HISTORY = """ +SELECT team_exec_id, team_name, state, start_date, end_date, conversation_id, + params +FROM user_ai_agent_team_history +WHERE (:team_name IS NULL OR team_name = :team_name) + AND (:team_exec_id IS NULL OR team_exec_id = :team_exec_id) +ORDER BY start_date DESC NULLS LAST +""" + + +LIST_USER_AI_AGENT_TASK_HISTORY = """ +SELECT team_exec_id, team_name, task_order, agent_name, task_name, + conversation_params, input, result, state, start_date, end_date +FROM user_ai_agent_task_history +WHERE (:team_name IS NULL OR team_name = :team_name) + AND (:task_name IS NULL OR task_name = :task_name) + AND (:agent_name IS NULL OR agent_name = :agent_name) + AND (:team_exec_id IS NULL OR team_exec_id = :team_exec_id) +ORDER BY start_date DESC NULLS LAST +""" + + +LIST_USER_AI_AGENT_TOOL_HISTORY = """ +SELECT invocation_id, team_exec_id, task_order, tool_name, agent_name, + task_name, start_date, end_date, input, output, tool_output +FROM user_ai_agent_tool_history +WHERE (:tool_name IS NULL OR tool_name = :tool_name) + AND (:task_name IS NULL OR task_name = :task_name) + AND (:agent_name IS NULL OR agent_name = :agent_name) + AND (:team_exec_id IS NULL OR team_exec_id = :team_exec_id) +ORDER BY start_date DESC NULLS LAST +""" diff --git a/tests/agents/test_3300_teams.py b/tests/agents/test_3300_teams.py index 435efb8..58d694a 100644 --- a/tests/agents/test_3300_teams.py +++ b/tests/agents/test_3300_teams.py @@ -18,8 +18,12 @@ AgentAttributes, Task, TaskAttributes, + TaskHistory, Team, TeamAttributes, + TeamHistory, + Tool, + ToolHistory, ) PYSAI_3300_AGENT_NAME = f"PYSAI_3300_AGENT_{uuid.uuid4().hex.upper()}" @@ -29,6 +33,8 @@ PYSAI_3300_TASK_DESCRIPTION = "PYSAI_3100_SQL_TASK_DESCRIPTION" PYSAI_3300_TEAM_NAME = f"PYSAI_3300_TEAM_{uuid.uuid4().hex.upper()}" PYSAI_3300_TEAM_DESCRIPTION = "PYSAI_3300_TEAM_DESCRIPTION" +PYSAI_3300_FUNCTION_NAME = f"PYSAI_3300_FUNCTION_{uuid.uuid4().hex.upper()}" +PYSAI_3300_TOOL_NAME = f"PYSAI_3300_TOOL_{uuid.uuid4().hex.upper()}" @pytest.fixture(scope="module") @@ -43,10 +49,36 @@ def python_gen_ai_profile(profile_attributes): @pytest.fixture(scope="module") -def task_attributes(): +def history_tool(): + with select_ai.cursor() as cr: + cr.execute( + f""" + CREATE OR REPLACE FUNCTION {PYSAI_3300_FUNCTION_NAME} + RETURN VARCHAR2 + IS + BEGIN + RETURN '{"message":"history test complete"}'; + END; + """ + ) + + tool = Tool.create_pl_sql_tool( + tool_name=PYSAI_3300_TOOL_NAME, + function=PYSAI_3300_FUNCTION_NAME, + description="Returns JSON with the history test result", + ) + yield tool + tool.delete(force=True) + with select_ai.cursor() as cr: + cr.execute(f"DROP FUNCTION {PYSAI_3300_FUNCTION_NAME}") + + +@pytest.fixture(scope="module") +def task_attributes(history_tool): return TaskAttributes( - instruction="Help the user with their request about movies. " - "User question: {query}. ", + instruction="You must call the available tool exactly once, then " + "answer the user's question using its result. User question: {query}.", + tools=[history_tool.tool_name], enable_human_tool=False, ) @@ -142,3 +174,48 @@ def test_3303(team): assert len(response) > 0 finally: conversation.delete(force=True) + + +def test_3304_team_and_task_history(team): + """Run a team and retrieve its generated team and task history rows.""" + conversation = select_ai.Conversation( + attributes=select_ai.ConversationAttributes( + title="Agent history test", + description="Conversation for agent history test", + ) + ) + conversation.create() + try: + response = team.run( + prompt="Reply with one sentence about the movie Titanic.", + params={"conversation_id": conversation.conversation_id}, + ) + assert isinstance(response, str) + assert response + + team_runs = list(TeamHistory.list(team_name=team.team_name, limit=1)) + assert len(team_runs) == 1 + assert team_runs[0].team_name == team.team_name + assert team_runs[0].team_exec_id + assert team_runs[0].conversation_id == conversation.conversation_id + + task_runs = list( + TaskHistory.list(team_exec_id=team_runs[0].team_exec_id, limit=1) + ) + assert len(task_runs) == 1 + assert task_runs[0].team_name == team.team_name + assert task_runs[0].task_name == PYSAI_3300_TASK_NAME + + tool_runs = list( + ToolHistory.list( + tool_name=PYSAI_3300_TOOL_NAME, + team_exec_id=team_runs[0].team_exec_id, + limit=1, + ) + ) + assert len(tool_runs) == 1 + assert tool_runs[0].tool_name == PYSAI_3300_TOOL_NAME + assert tool_runs[0].invocation_id + assert tool_runs[0].output == {"message": "history test complete"} + finally: + conversation.delete(force=True) diff --git a/tests/agents/test_3700_async_teams.py b/tests/agents/test_3700_async_teams.py index a7fc909..75547c2 100644 --- a/tests/agents/test_3700_async_teams.py +++ b/tests/agents/test_3700_async_teams.py @@ -17,7 +17,11 @@ AgentAttributes, AsyncAgent, AsyncTask, + AsyncTaskHistory, AsyncTeam, + AsyncTeamHistory, + AsyncTool, + AsyncToolHistory, TaskAttributes, TeamAttributes, ) @@ -29,6 +33,8 @@ PYSAI_3700_TASK_DESCRIPTION = "PYSAI_3100_SQL_TASK_DESCRIPTION" PYSAI_3700_TEAM_NAME = f"PYSAI_3700_TEAM_{uuid.uuid4().hex.upper()}" PYSAI_3700_TEAM_DESCRIPTION = "PYSAI_3700_TEAM_DESCRIPTION" +PYSAI_3700_FUNCTION_NAME = f"PYSAI_3700_FUNCTION_{uuid.uuid4().hex.upper()}" +PYSAI_3700_TOOL_NAME = f"PYSAI_3700_TOOL_{uuid.uuid4().hex.upper()}" @pytest.fixture(scope="module") @@ -43,10 +49,36 @@ async def python_gen_ai_profile(profile_attributes): @pytest.fixture(scope="module") -def task_attributes(): +async def history_tool(): + async with select_ai.async_cursor() as cr: + await cr.execute( + f""" + CREATE OR REPLACE FUNCTION {PYSAI_3700_FUNCTION_NAME} + RETURN VARCHAR2 + IS + BEGIN + RETURN '{"message":"async history test complete"}'; + END; + """ + ) + + tool = await AsyncTool.create_pl_sql_tool( + tool_name=PYSAI_3700_TOOL_NAME, + function=PYSAI_3700_FUNCTION_NAME, + description="Returns JSON with the async history test result", + ) + yield tool + await tool.delete(force=True) + async with select_ai.async_cursor() as cr: + await cr.execute(f"DROP FUNCTION {PYSAI_3700_FUNCTION_NAME}") + + +@pytest.fixture(scope="module") +async def task_attributes(history_tool): return TaskAttributes( - instruction="Help the user with their request about movies. " - "User question: {query}. ", + instruction="You must call the available tool exactly once, then " + "answer the user's question using its result. User question: {query}.", + tools=[history_tool.tool_name], enable_human_tool=False, ) @@ -142,3 +174,61 @@ async def test_3303(team): assert len(response) > 0 finally: await conversation.delete(force=True) + + +async def test_3304_async_team_and_task_history(team): + """Run a team and retrieve its generated history rows asynchronously.""" + conversation = select_ai.AsyncConversation( + attributes=select_ai.ConversationAttributes( + title="Async agent history test", + description="Conversation for async agent history test", + ) + ) + await conversation.create() + try: + response = await team.run( + prompt="Reply with one sentence about the movie Titanic.", + params={"conversation_id": conversation.conversation_id}, + ) + assert isinstance(response, str) + assert response + + team_runs = [ + run + async for run in AsyncTeamHistory.list( + team_name=team.team_name, + limit=1, + ) + ] + assert len(team_runs) == 1 + assert team_runs[0].team_name == team.team_name + assert team_runs[0].team_exec_id + assert team_runs[0].conversation_id == conversation.conversation_id + + task_runs = [ + run + async for run in AsyncTaskHistory.list( + team_exec_id=team_runs[0].team_exec_id, + limit=1, + ) + ] + assert len(task_runs) == 1 + assert task_runs[0].team_name == team.team_name + assert task_runs[0].task_name == PYSAI_3700_TASK_NAME + + tool_runs = [ + run + async for run in AsyncToolHistory.list( + tool_name=PYSAI_3700_TOOL_NAME, + team_exec_id=team_runs[0].team_exec_id, + limit=1, + ) + ] + assert len(tool_runs) == 1 + assert tool_runs[0].tool_name == PYSAI_3700_TOOL_NAME + assert tool_runs[0].invocation_id + assert tool_runs[0].output == { + "message": "async history test complete" + } + finally: + await conversation.delete(force=True) From b523e42725e668af407476b28ee3df0524d10d8f Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Thu, 27 Aug 2026 13:40:33 -0700 Subject: [PATCH 10/31] Updated Readme and copyright header --- README.md | 110 +++++++++++------- .../image/select_ai_a2a_server_demo.gif | Bin 0 -> 29222 bytes docker/Dockerfile | 7 ++ docker/a2a-entrypoint.sh | 9 +- gcloud/cloudbuild.yaml | 7 ++ gcloud/deploy.sh | 7 ++ 6 files changed, 94 insertions(+), 46 deletions(-) create mode 100644 doc/source/image/select_ai_a2a_server_demo.gif diff --git a/README.md b/README.md index 5cbd863..7bd2a08 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,22 @@ Select AI for Python enables you to ask questions of your database data using na Select AI for Python enables you to leverage the broader Python ecosystem in combination with generative AI and database functionality - bridging the gap between the DBMS_CLOUD_AI PL/SQL package and Python's rich ecosystem. It provides intuitive objects and methods for AI model interaction. +## Table of Contents + +- [Installation](#installation) +- [Documentation](#documentation) +- [Getting Started](#getting-started) + - [Async Example](#async-example) +- [Command Line Interface](#command-line-interface) + - [Chat](#chat) + - [A2A Server](#a2a-server) + - [Cloud Run](#cloud-run) +- [Samples](#samples) +- [Help](#help) +- [Contributing](#contributing) +- [Security](#security) +- [License](#license) + ## Installation @@ -26,9 +42,48 @@ The CLI extra includes A2A server support. See [Select AI for Python documentation][documentation] -## Samples +## Getting Started + +```python +import select_ai + +user = "" +password = "" +dsn = "" + +select_ai.connect(user=user, password=password, dsn=dsn) +profile = select_ai.Profile(profile_name="oci_ai_profile") +# run_sql returns a pandas dataframe +df = profile.run_sql(prompt="How many promotions?") +print(df.columns) +print(df) +``` -Examples can be found in the [/samples][samples] directory +### Async Example + +```python + +import asyncio + +import select_ai + +user = "" +password = "" +dsn = "" + +# This example shows how to asynchronously run sql +async def main(): + await select_ai.async_connect(user=user, password=password, dsn=dsn) + async_profile = await select_ai.AsyncProfile( + profile_name="async_oci_ai_profile", + ) + # run_sql returns a pandas df + df = await async_profile.run_sql("How many promotions?") + print(df) + +asyncio.run(main()) + +``` ## Command Line Interface @@ -36,11 +91,12 @@ The optional `select-ai` command provides interactive chat, SQL, profile management, and A2A server tools for Select AI: ### Chat + ```bash select-ai chat --profile OCI_AI_PROFILE ``` -![Select AI CLI demo](doc/source/image/select_ai_cli_demo.gif) +![Select AI CLI demo](doc/source/image/select_ai_cli_demo.gif) ### A2A Server @@ -50,6 +106,8 @@ Expose one Oracle Database AI agent team as an A2A JSON-RPC HTTP server: select-ai a2a serve --team SALES_ANALYST --port 8000 ``` +![Select AI A2A server demo](doc/source/image/select_ai_a2a_server_demo.gif) + The command obtains database connection settings from its options or the `SELECT_AI_*` environment variables. Its Agent Card is available at `/.well-known/agent-card.json`, and its JSON-RPC endpoint is @@ -73,53 +131,15 @@ select-ai a2a agent-card \ --public-url https://YOUR-SERVICE.run.app ``` -## Cloud Run +#### Cloud Run Deploy the A2A server to Cloud Run using the instructions in [gcloud/README.md](https://github.com/oracle/python-select-ai/blob/main/gcloud/README.md). -## Basic Example - -```python -import select_ai - -user = "" -password = "" -dsn = "" - -select_ai.connect(user=user, password=password, dsn=dsn) -profile = select_ai.Profile(profile_name="oci_ai_profile") -# run_sql returns a pandas dataframe -df = profile.run_sql(prompt="How many promotions?") -print(df.columns) -print(df) -``` - -### Async Example - -```python - -import asyncio - -import select_ai - -user = "" -password = "" -dsn = "" - -# This example shows how to asynchronously run sql -async def main(): - await select_ai.async_connect(user=user, password=password, dsn=dsn) - async_profile = await select_ai.AsyncProfile( - profile_name="async_oci_ai_profile", - ) - # run_sql returns a pandas df - df = await async_profile.run_sql("How many promotions?") - print(df) +## Samples -asyncio.run(main()) +For in-depth examples, see the [/samples][samples] directory. -``` ## Help Questions can be asked in [GitHub Discussions][ghdiscussions]. @@ -136,7 +156,7 @@ Please consult the [security guide][security] for our responsible security vulne ## License -Copyright (c) 2025 Oracle and/or its affiliates. +Copyright (c) 2025, 2026 Oracle and/or its affiliates. Released under the Universal Permissive License v1.0 as shown at . diff --git a/doc/source/image/select_ai_a2a_server_demo.gif b/doc/source/image/select_ai_a2a_server_demo.gif new file mode 100644 index 0000000000000000000000000000000000000000..9b0ab9fbfde9ebb681df8171ad7e53a3773a36dd GIT binary patch literal 29222 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

{(=Pt$2A5O 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 literal 0 HcmV?d00001 diff --git a/docker/Dockerfile b/docker/Dockerfile index ae074c3..acfc00f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,3 +1,10 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + FROM oraclelinux:10-slim ENV PATH=/opt/venv/bin:$PATH diff --git a/docker/a2a-entrypoint.sh b/docker/a2a-entrypoint.sh index b4326d6..258a9b1 100644 --- a/docker/a2a-entrypoint.sh +++ b/docker/a2a-entrypoint.sh @@ -1,4 +1,11 @@ -#!/usr/bin/env sh +#!/usr/bin/env bash + +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- # Cloud Run-only A2A launcher. It expands the optional wallet archive mounted # by deploy.sh, then starts the generic select-ai CLI in A2A server mode. diff --git a/gcloud/cloudbuild.yaml b/gcloud/cloudbuild.yaml index c21d6fc..9977f13 100644 --- a/gcloud/cloudbuild.yaml +++ b/gcloud/cloudbuild.yaml @@ -1,3 +1,10 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + # Build one reusable A2A server image. Database team selection is Cloud Run # configuration, not an image-build input. steps: diff --git a/gcloud/deploy.sh b/gcloud/deploy.sh index c5449e7..0eeab66 100755 --- a/gcloud/deploy.sh +++ b/gcloud/deploy.sh @@ -1,5 +1,12 @@ #!/usr/bin/env bash +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + # Deploy one Select AI A2A server to private Cloud Run. On its first run it # creates the ADB secrets used by this service. Later runs reuse both those # secrets and the service's currently deployed image. From b8d47b1ad86a0d8d94b252cf6b090b24edb9dae3 Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Thu, 27 Aug 2026 16:17:08 -0700 Subject: [PATCH 11/31] Fix tests --- tests/agents/test_3300_teams.py | 2 +- tests/agents/test_3700_async_teams.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/agents/test_3300_teams.py b/tests/agents/test_3300_teams.py index 58d694a..6f9ff31 100644 --- a/tests/agents/test_3300_teams.py +++ b/tests/agents/test_3300_teams.py @@ -57,7 +57,7 @@ def history_tool(): RETURN VARCHAR2 IS BEGIN - RETURN '{"message":"history test complete"}'; + RETURN '{{"message":"history test complete"}}'; END; """ ) diff --git a/tests/agents/test_3700_async_teams.py b/tests/agents/test_3700_async_teams.py index 75547c2..e78f670 100644 --- a/tests/agents/test_3700_async_teams.py +++ b/tests/agents/test_3700_async_teams.py @@ -57,7 +57,7 @@ async def history_tool(): RETURN VARCHAR2 IS BEGIN - RETURN '{"message":"async history test complete"}'; + RETURN '{{"message":"async history test complete"}}'; END; """ ) From 8ff857853ddef4ce3981c68d3bc55e2d095d83de Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Thu, 27 Aug 2026 16:59:58 -0700 Subject: [PATCH 12/31] Updated tests to fix assertions --- src/select_ai/agent/a2a/server.py | 33 +++++++++++++++++++++++++-- tests/agents/test_3300_teams.py | 5 +++- tests/agents/test_3700_async_teams.py | 3 ++- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/select_ai/agent/a2a/server.py b/src/select_ai/agent/a2a/server.py index 686200c..6b28a26 100644 --- a/src/select_ai/agent/a2a/server.py +++ b/src/select_ai/agent/a2a/server.py @@ -7,11 +7,16 @@ """A2A HTTP server for Oracle Database AI Agent Teams.""" +import json from contextlib import asynccontextmanager from typing import Optional from a2a.compat.v0_3.conversions import to_compat_agent_card -from a2a.helpers import new_task_from_user_message, new_text_part +from a2a.helpers import ( + new_data_part, + new_task_from_user_message, + new_text_part, +) from a2a.server.agent_execution import AgentExecutor from a2a.server.request_handlers import DefaultRequestHandler from a2a.server.routes import create_jsonrpc_routes @@ -27,6 +32,25 @@ from select_ai.agent.a2a.task_store import OracleTaskStore from select_ai.version import __version__ +_A2UI_MIME_TYPE = "application/a2ui+json" + + +def _a2ui_payload(result: str | None) -> dict | None: + """Return an A2UI response envelope, if ``RUN_TEAM`` returned one.""" + if not result: + return None + try: + payload = json.loads(result) + except json.JSONDecodeError: + return None + if not isinstance(payload, dict): + return None + if payload.get("metadata", {}).get("mimeType") != _A2UI_MIME_TYPE: + return None + if not isinstance(payload.get("data"), list): + return None + return payload + class DatabaseTeamExecutor(AgentExecutor): """Execute A2A requests with one Oracle conversation per A2A context.""" @@ -57,8 +81,13 @@ async def execute(self, context, event_queue): prompt=context.get_user_input(), params={"conversation_id": conversation_id}, ) + a2ui_payload = _a2ui_payload(result) await updater.add_artifact( - parts=[new_text_part(result or "")], + parts=( + [new_data_part(a2ui_payload)] + if a2ui_payload is not None + else [new_text_part(result or "")] + ), name="database-agent-result", last_chunk=True, ) diff --git a/tests/agents/test_3300_teams.py b/tests/agents/test_3300_teams.py index 6f9ff31..bcc42d8 100644 --- a/tests/agents/test_3300_teams.py +++ b/tests/agents/test_3300_teams.py @@ -216,6 +216,9 @@ def test_3304_team_and_task_history(team): assert len(tool_runs) == 1 assert tool_runs[0].tool_name == PYSAI_3300_TOOL_NAME assert tool_runs[0].invocation_id - assert tool_runs[0].output == {"message": "history test complete"} + assert tool_runs[0].output == { + "status": "success", + "result": '\'{"message":"history test complete"}\'', + } finally: conversation.delete(force=True) diff --git a/tests/agents/test_3700_async_teams.py b/tests/agents/test_3700_async_teams.py index e78f670..ceef86a 100644 --- a/tests/agents/test_3700_async_teams.py +++ b/tests/agents/test_3700_async_teams.py @@ -228,7 +228,8 @@ async def test_3304_async_team_and_task_history(team): assert tool_runs[0].tool_name == PYSAI_3700_TOOL_NAME assert tool_runs[0].invocation_id assert tool_runs[0].output == { - "message": "async history test complete" + "status": "success", + "result": '\'{"message":"async history test complete"}\'', } finally: await conversation.delete(force=True) From 08afb3058e2de2dad6e19f0e425a4b879563f473 Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Fri, 28 Aug 2026 10:56:24 -0700 Subject: [PATCH 13/31] Support A2UI JSON --- src/select_ai/agent/a2a/server.py | 34 ++++++++++------ tests/a2a/test_a2ui_parts.py | 64 +++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 11 deletions(-) create mode 100644 tests/a2a/test_a2ui_parts.py diff --git a/src/select_ai/agent/a2a/server.py b/src/select_ai/agent/a2a/server.py index 6b28a26..1c60014 100644 --- a/src/select_ai/agent/a2a/server.py +++ b/src/select_ai/agent/a2a/server.py @@ -22,6 +22,7 @@ from a2a.server.routes import create_jsonrpc_routes from a2a.server.tasks import TaskUpdater from a2a.types import AgentCapabilities, AgentCard, AgentInterface, AgentSkill +from google.protobuf.json_format import ParseDict from starlette.applications import Starlette from starlette.responses import JSONResponse from starlette.routing import Route @@ -32,11 +33,9 @@ from select_ai.agent.a2a.task_store import OracleTaskStore from select_ai.version import __version__ -_A2UI_MIME_TYPE = "application/a2ui+json" - -def _a2ui_payload(result: str | None) -> dict | None: - """Return an A2UI response envelope, if ``RUN_TEAM`` returned one.""" +def _message_parts(result: str | None): + """Convert a serialized A2A message into its constituent parts.""" if not result: return None try: @@ -45,11 +44,24 @@ def _a2ui_payload(result: str | None) -> dict | None: return None if not isinstance(payload, dict): return None - if payload.get("metadata", {}).get("mimeType") != _A2UI_MIME_TYPE: - return None - if not isinstance(payload.get("data"), list): + message_parts = payload.get("parts") + if payload.get("kind") != "message" or not isinstance(message_parts, list): return None - return payload + parts = [] + for part in message_parts: + if not isinstance(part, dict): + return None + if part.get("kind") == "text" and isinstance(part.get("text"), str): + parts.append(new_text_part(part["text"])) + elif part.get("kind") == "data" and "data" in part: + output_part = new_data_part(part["data"]) + if isinstance(part.get("metadata"), dict): + ParseDict(part["metadata"], output_part.metadata) + parts.append(output_part) + else: + # Avoid silently discarding an unsupported part type. + return None + return parts or None class DatabaseTeamExecutor(AgentExecutor): @@ -81,11 +93,11 @@ async def execute(self, context, event_queue): prompt=context.get_user_input(), params={"conversation_id": conversation_id}, ) - a2ui_payload = _a2ui_payload(result) + message_parts = _message_parts(result) await updater.add_artifact( parts=( - [new_data_part(a2ui_payload)] - if a2ui_payload is not None + message_parts + if message_parts is not None else [new_text_part(result or "")] ), name="database-agent-result", diff --git a/tests/a2a/test_a2ui_parts.py b/tests/a2a/test_a2ui_parts.py new file mode 100644 index 0000000..82a1e40 --- /dev/null +++ b/tests/a2a/test_a2ui_parts.py @@ -0,0 +1,64 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# https://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +import json + +import pytest +from google.protobuf.json_format import MessageToDict + +pytest.importorskip("a2a") + +from select_ai.agent.a2a.server import _message_parts + + +def test_a2a_message_parts_are_forwarded_with_metadata(): + result = json.dumps( + { + "kind": "message", + "parts": [ + {"kind": "text", "text": "A2UI visualization ready."}, + { + "kind": "data", + "data": { + "version": "v0.9", + "createSurface": {"surfaceId": "smoke-test"}, + }, + "metadata": {"mimeType": "application/json+a2ui"}, + }, + ], + } + ) + + parts = _message_parts(result) + + assert [ + MessageToDict(part, preserving_proto_field_name=True) for part in parts + ] == [ + {"text": "A2UI visualization ready."}, + { + "data": { + "version": "v0.9", + "createSurface": {"surfaceId": "smoke-test"}, + }, + "metadata": {"mimeType": "application/json+a2ui"}, + }, + ] + + +def test_a2a_text_message_is_forwarded(): + result = json.dumps( + { + "kind": "message", + "parts": [{"kind": "text", "text": "ordinary response"}], + } + ) + + parts = _message_parts(result) + + assert [ + MessageToDict(part, preserving_proto_field_name=True) for part in parts + ] == [{"text": "ordinary response"}] From 99b308078f8489ff3520cd7be873f76c6d42d13b Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Mon, 31 Aug 2026 15:06:43 -0700 Subject: [PATCH 14/31] Added support for Select AI A2A Gateway --- .pre-commit-config.yaml | 1 + gcloud/README.md | 172 +-------- gcloud/gateway/README.md | 145 ++++++++ gcloud/gateway/cloudbuild.yaml | 220 ++++++++++++ gcloud/gateway/deploy.sh | 236 ++++++++++++ gcloud/gateway/gke/consul.yaml | 50 +++ gcloud/gateway/gke/namespace.yaml | 4 + gcloud/gateway/gke/worker-mtls.yaml | 82 +++++ gcloud/gateway/gke/worker.yaml | 52 +++ gcloud/standalone/README.md | 159 +++++++++ gcloud/{ => standalone}/cloudbuild.yaml | 4 +- gcloud/{ => standalone}/deploy.sh | 6 +- pyproject.toml | 3 + src/select_ai/agent/a2a/__init__.py | 30 +- src/select_ai/agent/a2a/a2ui.py | 33 ++ src/select_ai/agent/a2a/forms.py | 117 ++++++ src/select_ai/agent/a2a/gateway.py | 231 ++++++++++++ src/select_ai/agent/a2a/models.py | 71 ++++ src/select_ai/agent/a2a/results.py | 58 +++ src/select_ai/agent/a2a/server.py | 48 +-- src/select_ai/agent/a2a/worker.py | 436 +++++++++++++++++++++++ src/select_ai/agent/a2a/worker_client.py | 174 +++++++++ src/select_ai/cli/a2a.py | 151 ++++++++ tests/a2a/test_agent_card.py | 55 +++ tests/a2a/test_worker_runtime.py | 382 ++++++++++++++++++++ 25 files changed, 2720 insertions(+), 200 deletions(-) create mode 100644 gcloud/gateway/README.md create mode 100644 gcloud/gateway/cloudbuild.yaml create mode 100755 gcloud/gateway/deploy.sh create mode 100644 gcloud/gateway/gke/consul.yaml create mode 100644 gcloud/gateway/gke/namespace.yaml create mode 100644 gcloud/gateway/gke/worker-mtls.yaml create mode 100644 gcloud/gateway/gke/worker.yaml create mode 100644 gcloud/standalone/README.md rename gcloud/{ => standalone}/cloudbuild.yaml (82%) rename gcloud/{ => standalone}/deploy.sh (98%) create mode 100644 src/select_ai/agent/a2a/a2ui.py create mode 100644 src/select_ai/agent/a2a/forms.py create mode 100644 src/select_ai/agent/a2a/gateway.py create mode 100644 src/select_ai/agent/a2a/models.py create mode 100644 src/select_ai/agent/a2a/results.py create mode 100644 src/select_ai/agent/a2a/worker.py create mode 100644 src/select_ai/agent/a2a/worker_client.py create mode 100644 tests/a2a/test_worker_runtime.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3f3609a..a0fea1a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,6 +5,7 @@ repos: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml + args: [--allow-multiple-documents] - id: check-added-large-files args: ['--maxkb=2000'] - repo: https://github.com/psf/black-pre-commit-mirror diff --git a/gcloud/README.md b/gcloud/README.md index e075515..cf52356 100644 --- a/gcloud/README.md +++ b/gcloud/README.md @@ -1,159 +1,19 @@ -# Deploy the Select AI A2A server to Google Cloud + # Google Cloud deployment modes -`gcloud/deploy.sh` builds or selects a Select AI container image, creates or -updates a private Cloud Run service, and configures its database secrets. Run -it on a machine with the [Google Cloud CLI](https://cloud.google.com/sdk/docs/install) -installed and authenticated to the target project. +Select AI supports two distinct Google Cloud deployment modes. Choose based on +whether the database and Select AI team are known at deployment time or must +be selected dynamically by each user. -## IAM permissions - -The scripts use the active `gcloud` identity. They do not elevate its access. - -### Deployer (the active gcloud identity) - -| Operation | Required permissions | -| --- | --- | -| Inspect and create the Artifact Registry repository | `artifactregistry.repositories.get`, `artifactregistry.repositories.create` | -| Inspect and create the default runtime service account | `iam.serviceAccounts.get`, `iam.serviceAccounts.create` | -| Deploy or update Cloud Run | `run.services.create`, `run.services.update`, `run.services.get`, `run.operations.get`; `iam.serviceAccounts.actAs` on the runtime service account; `artifactregistry.repositories.downloadArtifacts` on the image repository | -| With `--build`, upload local source, submit, and wait for a build | `storage.buckets.get`, `storage.objects.create` on the configured source-staging bucket; `cloudbuild.builds.create`, `cloudbuild.builds.get`, `serviceusage.services.use` | -| Inspect, create, and add versions to database or wallet secrets | `secretmanager.secrets.get`, `secretmanager.secrets.create`, `secretmanager.versions.add` | -| Grant the runtime account access to those secrets | `secretmanager.secrets.getIamPolicy`, `secretmanager.secrets.setIamPolicy` | -| Grant Gemini Enterprise and the active gcloud identity access to the service | `run.services.getIamPolicy`, `run.services.setIamPolicy` | -| Obtain the project number | `resourcemanager.projects.get` | - -### Runtime service account - -| Operation | Required permissions | -| --- | --- | -| Read database and wallet secrets while serving requests | `secretmanager.versions.access` | - -### Other service identities - -| Principal | Operation | Required permissions | +| | Standalone | Dynamic gateway | | --- | --- | --- | -| Cloud Build execution service account | With `--build`, push the built image | `artifactregistry.repositories.uploadArtifacts` | -| Gemini Enterprise service agent | Invoke the private Cloud Run service | `run.routes.invoke` | -| Active gcloud identity | Fetch the Agent Card after deployment | `run.routes.invoke` | - -The source-staging bucket is Cloud Build's default unless a custom bucket is -configured. Cloud Build also needs access to its build-log destination; the -default same-project build account has that access. If your organization uses -a custom build service account, source bucket, or log bucket, its administrator -must grant the equivalent Cloud Storage permissions on those resources. - -Google Cloud references: [Service Usage access control](https://cloud.google.com/service-usage/docs/access-control), [Cloud Run deployment permissions](https://cloud.google.com/run/docs/reference/iam/roles), [Secret Manager access control](https://cloud.google.com/secret-manager/docs/access-control), [Artifact Registry roles](https://cloud.google.com/artifact-registry/docs/access-control), and [Cloud Build roles](https://cloud.google.com/build/docs/iam-roles-permissions). - -The rows that set IAM policy are administrative mutations. They are present -because `deploy.sh` creates and rotates secrets and configures private-service -invocation. If your customer deployment identity must not change IAM, provision -the secrets and the `secretmanager.versions.access`/`run.routes.invoke` -permissions beforehand, then -remove those policy-setting commands from the deployment workflow. - -## Prerequisite: enable project APIs once - -An administrator must enable these APIs once for the project: - -```bash -gcloud services enable \ - run.googleapis.com \ - cloudbuild.googleapis.com \ - artifactregistry.googleapis.com \ - secretmanager.googleapis.com \ - discoveryengine.googleapis.com \ - --project PROJECT_ID -``` - -## Deploy (and update) the A2A server - -```bash -gcloud/deploy.sh --build -``` - -On the first deployment, the script prompts for the ADB user, password, and -connect descriptor. It stores them in Secret Manager under names based on the -Cloud Run service, and grants only the runtime service account access. The -container receives the values as `SELECT_AI_USER`, `SELECT_AI_PASSWORD`, and -`SELECT_AI_DB_CONNECT_STRING`; they are never placed in the image or source -tree. - -### Optional: Autonomous Database mTLS wallet - -The Select AI SDK already supports `wallet_location` and `wallet_password`. -For Cloud Run, pass the path to the downloaded Autonomous Database wallet ZIP -on the first deployment (or when replacing it): - -```bash -gcloud/deploy.sh --wallet-archive /path/to/Wallet_database.zip -``` - -The script prompts for the wallet password, stores the ZIP and password as -service-specific Secret Manager secrets, and grants access only to the runtime -service account. Cloud Run mounts the ZIP read-only; its A2A launcher expands it -into ephemeral `/tmp` storage before starting the SDK, verifies it contains -`ewallet.pem`, and sets `SELECT_AI_WALLET_LOCATION` to that file's directory. -Do not commit the wallet ZIP or put its contents in the image. - -Later deploys reuse the wallet. To replace it, pass `--wallet-archive` again. - -The first deployment needs `--build` (or an explicit `--image-uri`). Later -deployments reuse the image already deployed to the service, so changing Cloud -Run configuration or secrets does not create another image. The command -deploys private Cloud Run, sets the final public URL in the Agent Card, grants your active -gcloud identity and Gemini Enterprise Discovery Engine service agent the -`run.routes.invoke` permission for this Cloud Run service. - -The default Cloud Run service is `oracle-a2a-agent`. Its default Agent Team, -installed in Oracle Database, is `ORACLE_AI_DATABASE_AGENT`. Override either -with explicit options: - -```bash -gcloud/deploy.sh --service sales-analyst-a2a --a2a-team SALES_ANALYST -``` - -Use a distinct `--service` value for each A2A team. Each service gets distinct Secret -Manager secret names by default, so credentials remain attached to that A2A -server. - -`--max-instances` controls the number of Cloud Run containers. Each container -can use up to 10 Oracle connections by default; change that limit with -`--pool-max-size`, for example `gcloud/deploy.sh --pool-max-size 20`. - -### Update the Select AI SDK or this repository - -Update the checkout (or modify its dependency version), then explicitly build -and deploy the new image: - -```bash -git pull -gcloud/deploy.sh --build -``` - -`--build` creates a freshly tagged image from the current source; without it, -the existing image is reused. Existing database secrets are reused without -prompting. To rotate the ADB credentials, explicitly request it: - -```bash -gcloud/deploy.sh --rotate-db-credentials -``` - -### What `cloudbuild.yaml` does - -`gcloud/deploy.sh --build` uses `gcloud/cloudbuild.yaml` to tell Cloud Build to build -`docker/Dockerfile` and push it to Artifact Registry. It is build configuration, -not a command you run. The build context is the repository root, so the image -can install the Select AI source from `pyproject.toml` and `src/`. - -### Cloud Build upload contents - -Before the build starts, `gcloud builds submit` archives and uploads the -repository root. The root `.gcloudignore` excludes local virtual environments, -generated documentation, test data, caches, credentials, and Git metadata. -Keep `src/`, `pyproject.toml`, `docker/`, and `gcloud/` in the upload; they are -required to build the image. If the upload is unexpectedly large, check local -directories against `.gcloudignore` before running `--build` again. - -After a successful deployment, the script prints the A2A Agent Card JSON. -Paste that JSON into Gemini Enterprise to register the private service. The -required Gemini Enterprise invocation permission has already been added. +| Database and team | Fixed at deployment time | Chosen at runtime for each user session | +| Public A2A service | One service for one configured team | One gateway that presents an A2UI connection form | +| Users | All requests use the deployed database identity | Any permitted user can connect to a reachable Oracle database and Select AI team | +| Architecture | One Cloud Run service | Cloud Run gateway, plus Consul and worker replicas in GKE | +| Session isolation | Shared service database pool | One child process and async pool per active user session | +| Main benefit | Simple, predictable deployment | Dynamic, multi-database and multi-team access from one A2A endpoint | +| Operational cost | Low | Higher: GKE workers, Consul, routing, TTL, and session capacity | + +Use [standalone](standalone/README.md) when a service should expose one known +database team. Use [gateway](gateway/README.md) when users must dynamically +choose their database connection and team. diff --git a/gcloud/gateway/README.md b/gcloud/gateway/README.md new file mode 100644 index 0000000..a992159 --- /dev/null +++ b/gcloud/gateway/README.md @@ -0,0 +1,145 @@ +# Dynamic gateway deployment + +Dynamic gateway mode exposes one public A2A endpoint where each user selects +an Oracle database connection and Select AI team through the A2UI connection +form. + +```text +A2A client + | +Cloud Run gateway + | + +-- private VPC --> Consul in GKE + | + +-- private VPC --> worker replicas in GKE + | + session child process + | + Oracle Database +``` + +The gateway is the only public A2A application. Consul and workers are a GKE +clustered service: Consul selects a worker for each new dynamic session, and +the chosen worker retains that session's process and Oracle conversation. + +## Deploy the complete stack + +Run this from the repository root: + +```bash +gcloud/gateway/deploy.sh --project PROJECT_ID +``` + +The script creates the Artifact Registry repository and GKE Autopilot cluster +when they do not already exist. Cloud Build then: + +1. builds the existing `docker/Dockerfile` image once; +2. deploys the GKE namespace and internal Consul service; +3. deploys the requested number of GKE worker replicas using + `select-ai a2a worker`; +4. deploys the same image to Cloud Run using `select-ai a2a gateway`; +5. sets the final Cloud Run URL in `AGENT_URL` for the Agent Card. + +Common options: + +```bash +gcloud/gateway/deploy.sh \ + --project PROJECT_ID \ + --region us-central1 \ + --cluster select-ai-a2a-gateway \ + --worker-replicas 3 \ + --network default \ + --subnet default +``` + +The Cloud Run gateway uses direct VPC egress to reach the internal Consul load +balancer and GKE worker pod addresses. The default one-instance gateway limit +is intentional: gateway A2A task and context/session state is currently in +memory. Workers, rather than the gateway, provide the clustered capacity for +dynamic sessions. + +`cloudbuild.yaml` is the complete build and deployment workflow. It supplies +the generated image and Consul endpoint values to the Cloud Run gateway at +deployment time. + +## Optional worker mTLS test mode + +Local testing does not use mTLS. The default GCloud deployment also keeps the +current private-VPC HTTP worker transport. + +For a short-lived GCloud mTLS test, add a private DNS suffix: + +```bash +gcloud/gateway/deploy.sh \ + --project PROJECT_ID \ + --enable-worker-mtls \ + --worker-domain workers.select-ai.internal \ + --mtls-cert-validity-days 365 +``` + +This mode is intentionally self-contained and is not a production PKI design. +The deployment script generates an ephemeral CA and leaf certificates valid for +365 days by default, then removes the CA private key from its restricted +temporary directory. It never prints or stores that key. The leaf material is +first stored in Google Secret Manager. Cloud Build then creates the Kubernetes +Secrets used by the workers. + +Set `--mtls-cert-validity-days DAYS` to choose the lifetime for both leaf +certificates: the gateway client certificate and the worker server certificate. +The CA is issued for one additional day. + +The first mTLS deployment creates these certificates. Later mTLS deployments +reuse them, including when changing `--worker-replicas`. To deliberately +replace the CA and both leaf certificates, add `--rotate-worker-mtls`. Rotation +recreates the worker StatefulSet and ends active worker sessions. + +Workers run as a StatefulSet. Each worker registers its own private DNS name +with Consul, so the gateway still reaches the exact process that owns the +session. Cloud Build creates the corresponding private Cloud DNS A records. + +The certificate and DNS records are refreshed by rerunning the deployment. +Because this is a test mode, rerun it after a worker pod is recreated outside a +deployment; its pod IP can change. Do not use this mode for a long-lived +production stack—use a managed workload-certificate and DNS reconciliation +solution there. + +### Where the worker certificate files come from + +The certificate paths passed to `select-ai a2a worker` are files **inside each +GKE worker container**. They are mounted from Kubernetes Secrets, not files in +this repository or on the machine that runs `deploy.sh`. + +```text +deploy.sh + -> creates the test certificates and stores them in Secret Manager + -> Cloud Build reads the worker certificate, worker key, and CA certificate + -> Cloud Build creates Kubernetes Secrets in select-ai-gateway + -> GKE mounts those Secrets read-only in every worker container +``` + +The worker pod receives these mounts: + +| Container file | Kubernetes Secret | Secret key | Used for | +| --- | --- | --- | --- | +| `/var/run/select-ai-mtls/tls.crt` | `select-ai-worker-server-tls` | `tls.crt` | worker HTTPS server certificate | +| `/var/run/select-ai-mtls/tls.key` | `select-ai-worker-server-tls` | `tls.key` | worker HTTPS private key | +| `/var/run/select-ai-mtls/gateway-ca.crt` | `select-ai-gateway-client-ca` | `ca.crt` | validates the gateway client certificate | + +GKE does not read Google Secret Manager at worker request time. Cloud Build +copies the required material into Kubernetes Secrets during deployment, and the +worker certificate files are mounted from those Kubernetes Secrets as read-only +files. For example, to inspect the paths in a running worker (without printing +their contents): + +```bash +kubectl -n select-ai-gateway exec select-ai-worker-0 -- \ + ls -l /var/run/select-ai-mtls/ +``` + +The Cloud Run gateway uses a separate direct mount from Google Secret Manager +for its CA, client certificate, and client key. + +The identity that submits Cloud Build needs permission to use GKE, Cloud Run, +Cloud DNS, and Secret Manager. Cloud Build also needs the corresponding GKE, +Cloud Run, Cloud DNS, and Secret Manager permissions because it deploys the +workers, refreshes the private DNS records, and mounts the gateway secrets. diff --git a/gcloud/gateway/cloudbuild.yaml b/gcloud/gateway/cloudbuild.yaml new file mode 100644 index 0000000..69958b7 --- /dev/null +++ b/gcloud/gateway/cloudbuild.yaml @@ -0,0 +1,220 @@ +# Build one Select AI image, deploy Consul and workers to GKE, then deploy the +# public dynamic gateway to Cloud Run. +steps: + - name: gcr.io/cloud-builders/docker + args: + - build + - --file + - docker/Dockerfile + - --tag + - ${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/select-ai:${_IMAGE_TAG} + - . + + # GKE and Cloud Run consume the image during this build, so publish it before + # deploying either workload. The top-level images: field publishes only after + # every build step succeeds, which is too late for the worker rollout. + - name: gcr.io/cloud-builders/docker + args: + - push + - ${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/select-ai:${_IMAGE_TAG} + + - name: gcr.io/cloud-builders/gke-deploy + args: + - run + - --filename=gcloud/gateway/gke/namespace.yaml + - --location=${_REGION} + - --cluster=${_CLUSTER} + + # gke-deploy writes generated manifests to output/. Cloud Build shares the + # source workspace across steps, so remove only that builder output before + # invoking it again. + - name: gcr.io/cloud-builders/gcloud + entrypoint: bash + args: + - -ceu + - rm -rf output + + - name: gcr.io/cloud-builders/kubectl + entrypoint: bash + env: + - CLOUDSDK_COMPUTE_REGION=${_REGION} + - CLOUDSDK_CONTAINER_CLUSTER=${_CLUSTER} + args: + - -ceu + - | + if [[ "${_ENABLE_WORKER_MTLS}" == "true" ]]; then + kubectl delete --namespace select-ai-gateway deployment/select-ai-worker --ignore-not-found + if [[ "${_ROTATE_WORKER_MTLS}" == "true" ]]; then + kubectl delete --namespace select-ai-gateway statefulset/select-ai-worker --ignore-not-found --wait=true + fi + else + kubectl delete --namespace select-ai-gateway statefulset/select-ai-worker --ignore-not-found + fi + + - name: gcr.io/cloud-builders/gcloud + entrypoint: bash + args: + - -ceu + - | + if [[ "${_ENABLE_WORKER_MTLS}" != "true" ]]; then + exit 0 + fi + gcloud secrets versions access latest --secret=select-ai-worker-mtls-cert > /workspace/worker.crt + gcloud secrets versions access latest --secret=select-ai-worker-mtls-key > /workspace/worker.key + gcloud secrets versions access latest --secret=select-ai-gateway-mtls-ca > /workspace/gateway-ca.crt + + - name: gcr.io/cloud-builders/kubectl + entrypoint: bash + env: + - CLOUDSDK_COMPUTE_REGION=${_REGION} + - CLOUDSDK_CONTAINER_CLUSTER=${_CLUSTER} + args: + - -ceu + - | + if [[ "${_ENABLE_WORKER_MTLS}" != "true" ]]; then + exit 0 + fi + kubectl -n select-ai-gateway create secret tls select-ai-worker-server-tls \ + --cert=/workspace/worker.crt --key=/workspace/worker.key \ + --dry-run=client -o yaml | kubectl apply -f - + kubectl -n select-ai-gateway create secret generic select-ai-gateway-client-ca \ + --from-file=ca.crt=/workspace/gateway-ca.crt \ + --dry-run=client -o yaml | kubectl apply -f - + + - name: gcr.io/cloud-builders/gke-deploy + args: + - run + - --filename=gcloud/gateway/gke/consul.yaml + - --location=${_REGION} + - --cluster=${_CLUSTER} + + - name: gcr.io/cloud-builders/kubectl + entrypoint: bash + env: + - CLOUDSDK_COMPUTE_REGION=${_REGION} + - CLOUDSDK_CONTAINER_CLUSTER=${_CLUSTER} + args: + - -ceu + - | + kubectl wait --namespace select-ai-gateway \ + --for=jsonpath='{.status.loadBalancer.ingress[0].ip}' \ + service/consul-server --timeout=300s + kubectl get --namespace select-ai-gateway service/consul-server \ + -o jsonpath='{.status.loadBalancer.ingress[0].ip}' \ + > /workspace/consul-ip + + - name: gcr.io/cloud-builders/gcloud + entrypoint: bash + args: + - -ceu + - | + image="${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/select-ai:${_IMAGE_TAG}" + worker_manifest=gcloud/gateway/gke/worker.yaml + if [[ "${_ENABLE_WORKER_MTLS}" == "true" ]]; then + worker_manifest=gcloud/gateway/gke/worker-mtls.yaml + fi + sed -e "s|WORKER_IMAGE|$image|" \ + -e "s|WORKER_REPLICAS|${_WORKER_REPLICAS}|" \ + -e "s|WORKER_DOMAIN|${_WORKER_DOMAIN}|" \ + "$worker_manifest" > /workspace/worker.yaml + + - name: gcr.io/cloud-builders/gcloud + entrypoint: bash + args: + - -ceu + - rm -rf output + + - name: gcr.io/cloud-builders/gke-deploy + args: + - run + - --filename=/workspace/worker.yaml + - --location=${_REGION} + - --cluster=${_CLUSTER} + + - name: gcr.io/cloud-builders/kubectl + entrypoint: bash + env: + - CLOUDSDK_COMPUTE_REGION=${_REGION} + - CLOUDSDK_CONTAINER_CLUSTER=${_CLUSTER} + args: + - -ceu + - | + if [[ "${_ENABLE_WORKER_MTLS}" == "true" ]]; then + kubectl rollout status --namespace select-ai-gateway statefulset/select-ai-worker --timeout=300s + kubectl get pods --namespace select-ai-gateway -l app=select-ai-worker \ + -o jsonpath='{range .items[*]}{.metadata.name}={.status.podIP}{"\n"}{end}' \ + > /workspace/worker-pods + else + kubectl rollout status --namespace select-ai-gateway deployment/select-ai-worker --timeout=300s + fi + + - name: gcr.io/cloud-builders/gcloud + entrypoint: bash + args: + - -ceu + - | + if [[ "${_ENABLE_WORKER_MTLS}" != "true" ]]; then + exit 0 + fi + while IFS== read -r pod_name pod_ip; do + record_name="${pod_name}.${_WORKER_DOMAIN}." + gcloud dns record-sets delete "$record_name" --type=A \ + --zone="${_WORKER_DNS_ZONE}" --quiet || true + gcloud dns record-sets create "$record_name" --type=A --ttl=30 \ + --rrdatas="$pod_ip" --zone="${_WORKER_DNS_ZONE}" + done < /workspace/worker-pods + + - name: gcr.io/cloud-builders/gcloud + entrypoint: bash + args: + - -ceu + - | + consul_ip=$(cat /workspace/consul-ip) + image="${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/select-ai:${_IMAGE_TAG}" + mtls_args=() + mtls_env="" + if [[ "${_ENABLE_WORKER_MTLS}" == "true" ]]; then + mtls_args=( + "--update-secrets=/var/run/select-ai-mtls-ca/ca.pem=select-ai-gateway-mtls-ca:latest,/var/run/select-ai-mtls-cert/tls.crt=select-ai-gateway-mtls-cert:latest,/var/run/select-ai-mtls-key/tls.key=select-ai-gateway-mtls-key:latest" + ) + mtls_env=",WORKER_TLS_CA_FILE=/var/run/select-ai-mtls-ca/ca.pem,WORKER_TLS_CERT_FILE=/var/run/select-ai-mtls-cert/tls.crt,WORKER_TLS_KEY_FILE=/var/run/select-ai-mtls-key/tls.key" + fi + gcloud run deploy "${_GATEWAY_SERVICE}" \ + --image="$image" \ + --region="${_REGION}" \ + --no-allow-unauthenticated \ + --network="${_NETWORK}" \ + --subnet="${_SUBNET}" \ + --vpc-egress=all-traffic \ + --max-instances=1 \ + --min-instances=1 \ + --port=8080 \ + --command=select-ai \ + --args=a2a,gateway \ + --set-env-vars="AGENT_URL=https://pending.invalid,CONSUL_HTTP_URL=http://$consul_ip:8500,WORKER_SERVICE=select-ai-worker,SESSION_TTL_SECONDS=900$mtls_env" \ + "${mtls_args[@]}" + gateway_url=$(gcloud run services describe "${_GATEWAY_SERVICE}" \ + --region="${_REGION}" --format='value(status.url)') + gcloud run services update "${_GATEWAY_SERVICE}" \ + --region="${_REGION}" \ + --update-env-vars="AGENT_URL=$gateway_url" + +images: + - ${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/select-ai:${_IMAGE_TAG} + +substitutions: + _REGION: us-central1 + _CLUSTER: select-ai-a2a-gateway + _REPOSITORY: select-ai + _IMAGE_TAG: dev + _GATEWAY_SERVICE: select-ai-a2a-gateway + _NETWORK: default + _SUBNET: default + _WORKER_REPLICAS: "2" + _ENABLE_WORKER_MTLS: "false" + _ROTATE_WORKER_MTLS: "false" + _WORKER_DOMAIN: "" + _WORKER_DNS_ZONE: select-ai-workers + +options: + logging: CLOUD_LOGGING_ONLY diff --git a/gcloud/gateway/deploy.sh b/gcloud/gateway/deploy.sh new file mode 100755 index 0000000..1923940 --- /dev/null +++ b/gcloud/gateway/deploy.sh @@ -0,0 +1,236 @@ +#!/usr/bin/env bash + +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# https://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +# Build and deploy the dynamic Select AI gateway stack: Cloud Run gateway plus +# Consul and worker replicas in GKE. + +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: gcloud/gateway/deploy.sh [options] + +Options: + --project PROJECT Google Cloud project (defaults to gcloud config) + --region REGION Region for GKE, Cloud Run, and Artifact Registry (default: us-central1) + --cluster NAME GKE Autopilot cluster name (default: select-ai-a2a-gateway) + --repository NAME Artifact Registry Docker repository (default: select-ai) + --gateway-service NAME Cloud Run gateway service name (default: select-ai-a2a-gateway) + --network NAME VPC network for Cloud Run direct VPC egress (default: default) + --subnet NAME VPC subnet for Cloud Run direct VPC egress (default: default) + --worker-replicas COUNT GKE worker replica count (default: 2) + --enable-worker-mtls Use ephemeral mTLS certificates for gateway-to-worker calls + --rotate-worker-mtls Replace the existing worker mTLS CA and certificates + --worker-domain DOMAIN Private DNS suffix for workers; required with --enable-worker-mtls + --worker-dns-zone NAME Private Cloud DNS zone name (default: select-ai-workers) + --mtls-cert-validity-days DAYS + Gateway and worker certificate lifetime (default: 365) + --image-tag TAG Image tag (default: git SHA plus UTC timestamp) + -h, --help Show this help +EOF +} + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +project_id="" +region="us-central1" +cluster="select-ai-a2a-gateway" +repository="select-ai" +gateway_service="select-ai-a2a-gateway" +network="default" +subnet="default" +worker_replicas="2" +enable_worker_mtls="false" +rotate_worker_mtls="false" +worker_domain="" +worker_dns_zone="select-ai-workers" +mtls_cert_validity_days="365" +image_tag="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --project) project_id="${2:?--project requires a value}"; shift 2 ;; + --region) region="${2:?--region requires a value}"; shift 2 ;; + --cluster) cluster="${2:?--cluster requires a value}"; shift 2 ;; + --repository) repository="${2:?--repository requires a value}"; shift 2 ;; + --gateway-service) gateway_service="${2:?--gateway-service requires a value}"; shift 2 ;; + --network) network="${2:?--network requires a value}"; shift 2 ;; + --subnet) subnet="${2:?--subnet requires a value}"; shift 2 ;; + --worker-replicas) worker_replicas="${2:?--worker-replicas requires a value}"; shift 2 ;; + --enable-worker-mtls) enable_worker_mtls="true"; shift ;; + --rotate-worker-mtls) rotate_worker_mtls="true"; shift ;; + --worker-domain) worker_domain="${2:?--worker-domain requires a value}"; shift 2 ;; + --worker-dns-zone) worker_dns_zone="${2:?--worker-dns-zone requires a value}"; shift 2 ;; + --mtls-cert-validity-days) mtls_cert_validity_days="${2:?--mtls-cert-validity-days requires a value}"; shift 2 ;; + --image-tag) image_tag="${2:?--image-tag requires a value}"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;; + esac +done + +if [[ -z "$project_id" ]]; then + project_id="$(gcloud config get-value project 2>/dev/null || true)" +fi +if [[ -z "$project_id" || "$project_id" == "(unset)" ]]; then + echo "Pass --project or configure one with: gcloud config set project PROJECT_ID" >&2 + exit 2 +fi +if ! [[ "$worker_replicas" =~ ^[1-9][0-9]*$ ]]; then + echo "--worker-replicas must be a positive integer." >&2 + exit 2 +fi +if [[ "$enable_worker_mtls" == "true" && -z "$worker_domain" ]]; then + echo "--worker-domain is required with --enable-worker-mtls." >&2 + exit 2 +fi +if [[ "$rotate_worker_mtls" == "true" && "$enable_worker_mtls" != "true" ]]; then + echo "--rotate-worker-mtls requires --enable-worker-mtls." >&2 + exit 2 +fi +if ! [[ "$mtls_cert_validity_days" =~ ^[1-9][0-9]*$ ]]; then + echo "--mtls-cert-validity-days must be a positive integer." >&2 + exit 2 +fi + +image_tag="${image_tag:-$(git -C "$repo_root" rev-parse --short HEAD)-$(date -u +%Y%m%d%H%M%S)}" + +gcloud services enable \ + artifactregistry.googleapis.com \ + cloudbuild.googleapis.com \ + compute.googleapis.com \ + container.googleapis.com \ + discoveryengine.googleapis.com \ + dns.googleapis.com \ + run.googleapis.com \ + secretmanager.googleapis.com \ + --project="$project_id" + +if ! gcloud artifacts repositories describe "$repository" \ + --location="$region" --project="$project_id" >/dev/null 2>&1; then + gcloud artifacts repositories create "$repository" \ + --repository-format=docker --location="$region" --project="$project_id" +fi + +if ! gcloud container clusters describe "$cluster" \ + --location="$region" --project="$project_id" >/dev/null 2>&1; then + gcloud container clusters create-auto "$cluster" \ + --location="$region" --network="$network" --project="$project_id" +fi + +cleanup_mtls() { + [[ -n "${mtls_dir:-}" ]] && rm -rf "$mtls_dir" +} + +if [[ "$enable_worker_mtls" == "true" ]]; then + create_mtls_material="$rotate_worker_mtls" + for secret_name in \ + select-ai-gateway-mtls-ca \ + select-ai-gateway-mtls-cert \ + select-ai-gateway-mtls-key \ + select-ai-worker-mtls-cert \ + select-ai-worker-mtls-key; do + if ! gcloud secrets versions access latest --secret="$secret_name" \ + --project="$project_id" >/dev/null 2>&1; then + create_mtls_material="true" + fi + done + if [[ "$create_mtls_material" == "true" ]]; then + trap cleanup_mtls EXIT + mtls_dir="$(mktemp -d)" + umask 077 + ca_validity_days="$((mtls_cert_validity_days + 1))" + openssl req -x509 -newkey rsa:2048 -nodes -days "$ca_validity_days" \ + -keyout "$mtls_dir/ca.key" -out "$mtls_dir/ca.crt" \ + -subj "/CN=select-ai ephemeral worker CA" >/dev/null 2>&1 + openssl req -newkey rsa:2048 -nodes \ + -keyout "$mtls_dir/worker.key" -out "$mtls_dir/worker.csr" \ + -subj "/CN=*.$worker_domain" >/dev/null 2>&1 + printf 'subjectAltName=DNS:*.%s\nextendedKeyUsage=serverAuth\n' "$worker_domain" \ + > "$mtls_dir/worker.ext" + openssl x509 -req -days "$mtls_cert_validity_days" -in "$mtls_dir/worker.csr" \ + -CA "$mtls_dir/ca.crt" -CAkey "$mtls_dir/ca.key" -CAcreateserial \ + -out "$mtls_dir/worker.crt" -extfile "$mtls_dir/worker.ext" >/dev/null 2>&1 + openssl req -newkey rsa:2048 -nodes \ + -keyout "$mtls_dir/gateway.key" -out "$mtls_dir/gateway.csr" \ + -subj "/CN=select-ai-gateway" >/dev/null 2>&1 + printf 'extendedKeyUsage=clientAuth\n' > "$mtls_dir/gateway.ext" + openssl x509 -req -days "$mtls_cert_validity_days" -in "$mtls_dir/gateway.csr" \ + -CA "$mtls_dir/ca.crt" -CAkey "$mtls_dir/ca.key" -CAcreateserial \ + -out "$mtls_dir/gateway.crt" -extfile "$mtls_dir/gateway.ext" >/dev/null 2>&1 + for secret_name in \ + select-ai-gateway-mtls-ca \ + select-ai-gateway-mtls-cert \ + select-ai-gateway-mtls-key \ + select-ai-worker-mtls-cert \ + select-ai-worker-mtls-key; do + gcloud secrets describe "$secret_name" --project="$project_id" >/dev/null 2>&1 || \ + gcloud secrets create "$secret_name" --replication-policy=automatic --project="$project_id" + done + gcloud secrets versions add select-ai-gateway-mtls-ca --data-file="$mtls_dir/ca.crt" --project="$project_id" + gcloud secrets versions add select-ai-gateway-mtls-cert --data-file="$mtls_dir/gateway.crt" --project="$project_id" + gcloud secrets versions add select-ai-gateway-mtls-key --data-file="$mtls_dir/gateway.key" --project="$project_id" + gcloud secrets versions add select-ai-worker-mtls-cert --data-file="$mtls_dir/worker.crt" --project="$project_id" + gcloud secrets versions add select-ai-worker-mtls-key --data-file="$mtls_dir/worker.key" --project="$project_id" + if [[ "$rotate_worker_mtls" == "true" ]]; then + echo "Rotating worker mTLS material; worker sessions will be interrupted." + else + echo "Created initial worker mTLS material." + fi + else + echo "Reusing existing worker mTLS material." + fi + if ! gcloud dns managed-zones describe "$worker_dns_zone" --project="$project_id" >/dev/null 2>&1; then + gcloud dns managed-zones create "$worker_dns_zone" --dns-name="${worker_domain}." \ + --visibility=private --networks="https://www.googleapis.com/compute/v1/projects/$project_id/global/networks/$network" \ + --description="Private DNS records for Select AI mTLS workers" \ + --project="$project_id" + fi + runtime_sa="${project_id_number:-$(gcloud projects describe "$project_id" --format='value(projectNumber)')}-compute@developer.gserviceaccount.com" + for secret_name in \ + select-ai-gateway-mtls-ca \ + select-ai-gateway-mtls-cert \ + select-ai-gateway-mtls-key \ + select-ai-worker-mtls-cert \ + select-ai-worker-mtls-key; do + gcloud secrets add-iam-policy-binding "$secret_name" --project="$project_id" \ + --member="serviceAccount:$runtime_sa" --role="roles/secretmanager.secretAccessor" >/dev/null + done +fi + +gcloud builds submit "$repo_root" \ + --project="$project_id" \ + --region="$region" \ + --config="$repo_root/gcloud/gateway/cloudbuild.yaml" \ + --substitutions="_REGION=$region,_CLUSTER=$cluster,_REPOSITORY=$repository,_IMAGE_TAG=$image_tag,_GATEWAY_SERVICE=$gateway_service,_NETWORK=$network,_SUBNET=$subnet,_WORKER_REPLICAS=$worker_replicas,_ENABLE_WORKER_MTLS=$enable_worker_mtls,_ROTATE_WORKER_MTLS=$rotate_worker_mtls,_WORKER_DOMAIN=$worker_domain,_WORKER_DNS_ZONE=$worker_dns_zone" + +gateway_url="$(gcloud run services describe "$gateway_service" \ + --region="$region" --project="$project_id" --format='value(status.url)')" +project_number="$(gcloud projects describe "$project_id" --format='value(projectNumber)')" +gemini_service_agent="service-$project_number@gcp-sa-discoveryengine.iam.gserviceaccount.com" + +gcloud run services add-iam-policy-binding "$gateway_service" \ + --region="$region" --project="$project_id" \ + --member="serviceAccount:$gemini_service_agent" \ + --role="roles/run.invoker" >/dev/null + +active_account="$(gcloud auth list --filter=status:ACTIVE --format='value(account)')" +if [[ -z "$active_account" ]]; then + echo "No active gcloud account. Run: gcloud auth login" >&2 + exit 2 +fi +if gcloud iam service-accounts describe "$active_account" \ + --project="$project_id" >/dev/null 2>&1; then + deployer_member="serviceAccount:$active_account" +else + deployer_member="user:$active_account" +fi +gcloud run services add-iam-policy-binding "$gateway_service" \ + --region="$region" --project="$project_id" \ + --member="$deployer_member" --role="roles/run.invoker" >/dev/null + +printf 'Gateway Agent Card URL:\n%s/.well-known/agent-card.json\n' "$gateway_url" diff --git a/gcloud/gateway/gke/consul.yaml b/gcloud/gateway/gke/consul.yaml new file mode 100644 index 0000000..19569c0 --- /dev/null +++ b/gcloud/gateway/gke/consul.yaml @@ -0,0 +1,50 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: consul-server + namespace: select-ai-gateway +spec: + replicas: 1 + selector: + matchLabels: + app: consul-server + template: + metadata: + labels: + app: consul-server + spec: + containers: + - name: consul + image: hashicorp/consul:1.21.5 + args: + - agent + - -server + - -bootstrap-expect=1 + - -client=0.0.0.0 + ports: + - name: http + containerPort: 8500 + readinessProbe: + httpGet: + path: /v1/status/leader + port: http + livenessProbe: + httpGet: + path: /v1/status/leader + port: http +--- +apiVersion: v1 +kind: Service +metadata: + name: consul-server + namespace: select-ai-gateway + annotations: + networking.gke.io/load-balancer-type: Internal +spec: + type: LoadBalancer + selector: + app: consul-server + ports: + - name: http + port: 8500 + targetPort: http diff --git a/gcloud/gateway/gke/namespace.yaml b/gcloud/gateway/gke/namespace.yaml new file mode 100644 index 0000000..dc1ff53 --- /dev/null +++ b/gcloud/gateway/gke/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: select-ai-gateway diff --git a/gcloud/gateway/gke/worker-mtls.yaml b/gcloud/gateway/gke/worker-mtls.yaml new file mode 100644 index 0000000..9861212 --- /dev/null +++ b/gcloud/gateway/gke/worker-mtls.yaml @@ -0,0 +1,82 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: select-ai-worker + namespace: select-ai-gateway +spec: + serviceName: select-ai-worker + replicas: WORKER_REPLICAS + selector: + matchLabels: + app: select-ai-worker + template: + metadata: + labels: + app: select-ai-worker + spec: + terminationGracePeriodSeconds: 30 + containers: + - name: worker + image: WORKER_IMAGE + command: [select-ai] + args: + - a2a + - worker + - --host + - 0.0.0.0 + - --port + - "8443" + - --tls-cert-file + - /var/run/select-ai-mtls/tls.crt + - --tls-key-file + - /var/run/select-ai-mtls/tls.key + - --tls-ca-file + - /var/run/select-ai-mtls/gateway-ca.crt + ports: + - name: https + containerPort: 8443 + env: + - name: CONSUL_HTTP_URL + value: http://consul-server.select-ai-gateway.svc.cluster.local:8500 + - name: WORKER_ID + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: WORKER_ADDRESS + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: WORKER_PORT + value: "8443" + - name: WORKER_ENDPOINT + value: https://$(POD_NAME).WORKER_DOMAIN:8443 + volumeMounts: + - name: worker-server-tls + mountPath: /var/run/select-ai-mtls/tls.crt + subPath: tls.crt + readOnly: true + - name: worker-server-tls + mountPath: /var/run/select-ai-mtls/tls.key + subPath: tls.key + readOnly: true + - name: gateway-client-ca + mountPath: /var/run/select-ai-mtls/gateway-ca.crt + subPath: ca.crt + readOnly: true + readinessProbe: + tcpSocket: + port: https + livenessProbe: + tcpSocket: + port: https + volumes: + - name: worker-server-tls + secret: + secretName: select-ai-worker-server-tls + - name: gateway-client-ca + secret: + secretName: select-ai-gateway-client-ca diff --git a/gcloud/gateway/gke/worker.yaml b/gcloud/gateway/gke/worker.yaml new file mode 100644 index 0000000..fc24c5e --- /dev/null +++ b/gcloud/gateway/gke/worker.yaml @@ -0,0 +1,52 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: select-ai-worker + namespace: select-ai-gateway +spec: + replicas: WORKER_REPLICAS + selector: + matchLabels: + app: select-ai-worker + template: + metadata: + labels: + app: select-ai-worker + spec: + terminationGracePeriodSeconds: 30 + containers: + - name: worker + image: WORKER_IMAGE + command: + - select-ai + args: + - a2a + - worker + - --host + - 0.0.0.0 + - --port + - "8080" + ports: + - name: http + containerPort: 8080 + env: + - name: CONSUL_HTTP_URL + value: http://consul-server.select-ai-gateway.svc.cluster.local:8500 + - name: WORKER_ID + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: WORKER_ADDRESS + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: WORKER_PORT + value: "8080" + readinessProbe: + httpGet: + path: /health + port: http + livenessProbe: + httpGet: + path: /health + port: http diff --git a/gcloud/standalone/README.md b/gcloud/standalone/README.md new file mode 100644 index 0000000..5c2df66 --- /dev/null +++ b/gcloud/standalone/README.md @@ -0,0 +1,159 @@ +# Standalone Select AI A2A deployment + +`gcloud/standalone/deploy.sh` builds or selects a Select AI container image, creates or +updates a private Cloud Run service, and configures its database secrets. Run +it on a machine with the [Google Cloud CLI](https://cloud.google.com/sdk/docs/install) +installed and authenticated to the target project. + +## IAM permissions + +The scripts use the active `gcloud` identity. They do not elevate its access. + +### Deployer (the active gcloud identity) + +| Operation | Required permissions | +| --- | --- | +| Inspect and create the Artifact Registry repository | `artifactregistry.repositories.get`, `artifactregistry.repositories.create` | +| Inspect and create the default runtime service account | `iam.serviceAccounts.get`, `iam.serviceAccounts.create` | +| Deploy or update Cloud Run | `run.services.create`, `run.services.update`, `run.services.get`, `run.operations.get`; `iam.serviceAccounts.actAs` on the runtime service account; `artifactregistry.repositories.downloadArtifacts` on the image repository | +| With `--build`, upload local source, submit, and wait for a build | `storage.buckets.get`, `storage.objects.create` on the configured source-staging bucket; `cloudbuild.builds.create`, `cloudbuild.builds.get`, `serviceusage.services.use` | +| Inspect, create, and add versions to database or wallet secrets | `secretmanager.secrets.get`, `secretmanager.secrets.create`, `secretmanager.versions.add` | +| Grant the runtime account access to those secrets | `secretmanager.secrets.getIamPolicy`, `secretmanager.secrets.setIamPolicy` | +| Grant Gemini Enterprise and the active gcloud identity access to the service | `run.services.getIamPolicy`, `run.services.setIamPolicy` | +| Obtain the project number | `resourcemanager.projects.get` | + +### Runtime service account + +| Operation | Required permissions | +| --- | --- | +| Read database and wallet secrets while serving requests | `secretmanager.versions.access` | + +### Other service identities + +| Principal | Operation | Required permissions | +| --- | --- | --- | +| Cloud Build execution service account | With `--build`, push the built image | `artifactregistry.repositories.uploadArtifacts` | +| Gemini Enterprise service agent | Invoke the private Cloud Run service | `run.routes.invoke` | +| Active gcloud identity | Fetch the Agent Card after deployment | `run.routes.invoke` | + +The source-staging bucket is Cloud Build's default unless a custom bucket is +configured. Cloud Build also needs access to its build-log destination; the +default same-project build account has that access. If your organization uses +a custom build service account, source bucket, or log bucket, its administrator +must grant the equivalent Cloud Storage permissions on those resources. + +Google Cloud references: [Service Usage access control](https://cloud.google.com/service-usage/docs/access-control), [Cloud Run deployment permissions](https://cloud.google.com/run/docs/reference/iam/roles), [Secret Manager access control](https://cloud.google.com/secret-manager/docs/access-control), [Artifact Registry roles](https://cloud.google.com/artifact-registry/docs/access-control), and [Cloud Build roles](https://cloud.google.com/build/docs/iam-roles-permissions). + +The rows that set IAM policy are administrative mutations. They are present +because `deploy.sh` creates and rotates secrets and configures private-service +invocation. If your customer deployment identity must not change IAM, provision +the secrets and the `secretmanager.versions.access`/`run.routes.invoke` +permissions beforehand, then +remove those policy-setting commands from the deployment workflow. + +## Prerequisite: enable project APIs once + +An administrator must enable these APIs once for the project: + +```bash +gcloud services enable \ + run.googleapis.com \ + cloudbuild.googleapis.com \ + artifactregistry.googleapis.com \ + secretmanager.googleapis.com \ + discoveryengine.googleapis.com \ + --project PROJECT_ID +``` + +## Deploy (and update) the A2A server + +```bash +gcloud/standalone/deploy.sh --build +``` + +On the first deployment, the script prompts for the ADB user, password, and +connect descriptor. It stores them in Secret Manager under names based on the +Cloud Run service, and grants only the runtime service account access. The +container receives the values as `SELECT_AI_USER`, `SELECT_AI_PASSWORD`, and +`SELECT_AI_DB_CONNECT_STRING`; they are never placed in the image or source +tree. + +### Optional: Autonomous Database mTLS wallet + +The Select AI SDK already supports `wallet_location` and `wallet_password`. +For Cloud Run, pass the path to the downloaded Autonomous Database wallet ZIP +on the first deployment (or when replacing it): + +```bash +gcloud/standalone/deploy.sh --wallet-archive /path/to/Wallet_database.zip +``` + +The script prompts for the wallet password, stores the ZIP and password as +service-specific Secret Manager secrets, and grants access only to the runtime +service account. Cloud Run mounts the ZIP read-only; its A2A launcher expands it +into ephemeral `/tmp` storage before starting the SDK, verifies it contains +`ewallet.pem`, and sets `SELECT_AI_WALLET_LOCATION` to that file's directory. +Do not commit the wallet ZIP or put its contents in the image. + +Later deploys reuse the wallet. To replace it, pass `--wallet-archive` again. + +The first deployment needs `--build` (or an explicit `--image-uri`). Later +deployments reuse the image already deployed to the service, so changing Cloud +Run configuration or secrets does not create another image. The command +deploys private Cloud Run, sets the final public URL in the Agent Card, grants your active +gcloud identity and Gemini Enterprise Discovery Engine service agent the +`run.routes.invoke` permission for this Cloud Run service. + +The default Cloud Run service is `oracle-a2a-agent`. Its default Agent Team, +installed in Oracle Database, is `ORACLE_AI_DATABASE_AGENT`. Override either +with explicit options: + +```bash +gcloud/standalone/deploy.sh --service sales-analyst-a2a --a2a-team SALES_ANALYST +``` + +Use a distinct `--service` value for each A2A team. Each service gets distinct Secret +Manager secret names by default, so credentials remain attached to that A2A +server. + +`--max-instances` controls the number of Cloud Run containers. Each container +can use up to 10 Oracle connections by default; change that limit with +`--pool-max-size`, for example `gcloud/standalone/deploy.sh --pool-max-size 20`. + +### Update the Select AI SDK or this repository + +Update the checkout (or modify its dependency version), then explicitly build +and deploy the new image: + +```bash +git pull +gcloud/standalone/deploy.sh --build +``` + +`--build` creates a freshly tagged image from the current source; without it, +the existing image is reused. Existing database secrets are reused without +prompting. To rotate the ADB credentials, explicitly request it: + +```bash +gcloud/standalone/deploy.sh --rotate-db-credentials +``` + +### What `cloudbuild.yaml` does + +`gcloud/standalone/deploy.sh --build` uses `gcloud/standalone/cloudbuild.yaml` to tell Cloud Build to build +`docker/Dockerfile` and push it to Artifact Registry. It is build configuration, +not a command you run. The build context is the repository root, so the image +can install the Select AI source from `pyproject.toml` and `src/`. + +### Cloud Build upload contents + +Before the build starts, `gcloud builds submit` archives and uploads the +repository root. The root `.gcloudignore` excludes local virtual environments, +generated documentation, test data, caches, credentials, and Git metadata. +Keep `src/`, `pyproject.toml`, `docker/`, and `gcloud/` in the upload; they are +required to build the image. If the upload is unexpectedly large, check local +directories against `.gcloudignore` before running `--build` again. + +After a successful deployment, the script prints the A2A Agent Card JSON. +Paste that JSON into Gemini Enterprise to register the private service. The +required Gemini Enterprise invocation permission has already been added. diff --git a/gcloud/cloudbuild.yaml b/gcloud/standalone/cloudbuild.yaml similarity index 82% rename from gcloud/cloudbuild.yaml rename to gcloud/standalone/cloudbuild.yaml index 9977f13..3cedbda 100644 --- a/gcloud/cloudbuild.yaml +++ b/gcloud/standalone/cloudbuild.yaml @@ -5,8 +5,8 @@ # http://oss.oracle.com/licenses/upl. # ----------------------------------------------------------------------------- -# Build one reusable A2A server image. Database team selection is Cloud Run -# configuration, not an image-build input. +# Build one reusable Select AI image for standalone servers, the gateway, and +# GKE worker replicas. Runtime role selection is deployment configuration. steps: - name: gcr.io/cloud-builders/docker args: diff --git a/gcloud/deploy.sh b/gcloud/standalone/deploy.sh similarity index 98% rename from gcloud/deploy.sh rename to gcloud/standalone/deploy.sh index 0eeab66..3ac7ff1 100755 --- a/gcloud/deploy.sh +++ b/gcloud/standalone/deploy.sh @@ -15,7 +15,7 @@ set -euo pipefail usage() { cat <<'EOF' -Usage: gcloud/deploy.sh [options] +Usage: gcloud/standalone/deploy.sh [options] Deploy the Select AI A2A server to private Cloud Run. @@ -45,7 +45,7 @@ Options: EOF } -repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" project_id="" region="us-central1" repository="select-ai" @@ -227,7 +227,7 @@ if [[ "$build_image" == true ]]; then image_uri="$region-docker.pkg.dev/$project_id/$repository/select-ai:$image_tag" echo "Building $image_uri" gcloud builds submit "$repo_root" --project="$project_id" \ - --config="$repo_root/gcloud/cloudbuild.yaml" \ + --config="$repo_root/gcloud/standalone/cloudbuild.yaml" \ --substitutions="_REGION=$region,_REPOSITORY=$repository,_IMAGE_TAG=$image_tag" elif [[ -z "$image_uri" && "$service_exists" == true ]]; then image_uri="$(gcloud run services describe "$service" --project="$project_id" --region="$region" \ diff --git a/pyproject.toml b/pyproject.toml index 1c39728..6ffa295 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,9 @@ cli = [ "click", "a2a-sdk[http-server]>=1.0.3", "uvicorn[standard]>=0.30", + "fastapi>=0.115,<1", + "httpx>=0.27,<1", + "requests>=2.32,<3", ] a2a = [ "select_ai[cli]", diff --git a/src/select_ai/agent/a2a/__init__.py b/src/select_ai/agent/a2a/__init__.py index f4786b6..7424546 100644 --- a/src/select_ai/agent/a2a/__init__.py +++ b/src/select_ai/agent/a2a/__init__.py @@ -5,4 +5,32 @@ # https://oss.oracle.com/licenses/upl. # ----------------------------------------------------------------------------- -"""A2A support for Select AI Agent Teams.""" +"""A2A support for Select AI Agent Teams and temporary sessions.""" + +from .models import GatewaySettings, SessionInfo, SessionRoute + + +def create_gateway_app(settings): + """Build the public A2A/A2UI gateway application.""" + from select_ai.agent.a2a.gateway import create_gateway_app as factory + + return factory(settings) + + +def create_worker_app( + session_ttl_seconds: int = 900, + session_start_timeout_seconds: int = 30, +): + """Build the internal worker application.""" + from select_ai.agent.a2a.worker import create_worker_app as factory + + return factory(session_ttl_seconds, session_start_timeout_seconds) + + +__all__ = [ + "GatewaySettings", + "SessionInfo", + "SessionRoute", + "create_gateway_app", + "create_worker_app", +] diff --git a/src/select_ai/agent/a2a/a2ui.py b/src/select_ai/agent/a2a/a2ui.py new file mode 100644 index 0000000..a25b7f9 --- /dev/null +++ b/src/select_ai/agent/a2a/a2ui.py @@ -0,0 +1,33 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# https://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +"""Shared A2UI protocol declarations for Select AI A2A agents.""" + +from a2a.types import AgentExtension +from google.protobuf.json_format import ParseDict +from google.protobuf.struct_pb2 import Struct + +from select_ai.agent.a2a.forms import _CATALOG + +A2UI_EXTENSION_URI = "https://a2ui.org/a2a-extension/a2ui/v0.9" +A2UI_MIME_TYPE = "application/json+a2ui" + + +def a2ui_extension() -> AgentExtension: + """Return the A2UI v0.9 capability used by Gemini Enterprise.""" + params = ParseDict( + { + "acceptsInlineCatalogs": True, + "supportedCatalogIds": [_CATALOG], + }, + Struct(), + ) + return AgentExtension( + uri=A2UI_EXTENSION_URI, + description="Provides agent driven UI using the A2UI JSON format.", + params=params, + ) diff --git a/src/select_ai/agent/a2a/forms.py b/src/select_ai/agent/a2a/forms.py new file mode 100644 index 0000000..6823464 --- /dev/null +++ b/src/select_ai/agent/a2a/forms.py @@ -0,0 +1,117 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# https://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +"""A2UI connection form emitted by the public gateway.""" + + +_CATALOG = ( + "https://www.gstatic.com/vertexaisearch/a2ui/v0_9/" + "gemini_enterprise_composite_catalog.json" +) + + +def connection_form() -> list[dict]: + """Return the non-persistent database connection form.""" + return [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "db-connect", + "catalogId": _CATALOG, + }, + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "db-connect", + "components": [ + {"id": "root", "component": "Card", "child": "column"}, + { + "id": "column", + "component": "Column", + "children": [ + "title", + "dsn", + "user", + "password", + "team", + "connect", + ], + }, + { + "id": "title", + "component": "Text", + "text": "Connect to Oracle Database", + "variant": "h2", + }, + { + "id": "dsn", + "component": "TextField", + "label": "Database DSN", + "value": {"path": "/dsn"}, + "variant": "shortText", + }, + { + "id": "user", + "component": "TextField", + "label": "Database username", + "value": {"path": "/username"}, + "variant": "shortText", + }, + { + "id": "password", + "component": "TextField", + "label": "Database password", + "value": {"path": "/password"}, + "variant": "obscured", + }, + { + "id": "team", + "component": "TextField", + "label": "Select AI team name", + "value": {"path": "/team_name"}, + "variant": "shortText", + }, + { + "id": "connect_label", + "component": "Text", + "text": "Connect", + }, + { + "id": "connect", + "component": "Button", + "child": "connect_label", + "variant": "primary", + "action": { + "event": { + "name": "submit_database_connection", + "context": { + "dsn": {"path": "/dsn"}, + "username": {"path": "/username"}, + "password": {"path": "/password"}, + "team_name": {"path": "/team_name"}, + }, + } + }, + }, + ], + }, + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "db-connect", + "path": "/", + "value": { + "dsn": "", + "username": "", + "password": "", + "team_name": "", + }, + }, + }, + ] diff --git a/src/select_ai/agent/a2a/gateway.py b/src/select_ai/agent/a2a/gateway.py new file mode 100644 index 0000000..4b0957e --- /dev/null +++ b/src/select_ai/agent/a2a/gateway.py @@ -0,0 +1,231 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# https://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +"""Public A2A/A2UI gateway for temporary Select AI database sessions.""" + +from __future__ import annotations + +import asyncio + +from a2a.compat.v0_3.conversions import to_compat_agent_card +from a2a.helpers import ( + new_data_part, + new_task_from_user_message, + new_text_part, +) +from a2a.server.agent_execution import AgentExecutor +from a2a.server.request_handlers import DefaultRequestHandler +from a2a.server.routes import create_jsonrpc_routes +from a2a.server.tasks import InMemoryTaskStore, TaskUpdater +from a2a.types import ( + AgentCapabilities, + AgentCard, + AgentInterface, + AgentSkill, +) +from google.protobuf.json_format import MessageToDict, ParseDict +from starlette.applications import Starlette +from starlette.responses import JSONResponse +from starlette.routing import Route + +from select_ai.agent.a2a.a2ui import ( + A2UI_EXTENSION_URI, + A2UI_MIME_TYPE, + a2ui_extension, +) +from select_ai.agent.a2a.forms import connection_form +from select_ai.agent.a2a.models import GatewaySettings, SessionInfo +from select_ai.agent.a2a.results import add_team_result +from select_ai.agent.a2a.worker_client import ReconnectRequired, WorkerClient +from select_ai.version import __version__ + + +class GatewayExecutor(AgentExecutor): + """Route each A2A context to one in-memory worker session.""" + + def __init__(self, worker_client: WorkerClient): + self.worker_client = worker_client + self.sessions: dict[str, str] = {} + + async def execute(self, context, event_queue): + task = context.current_task or new_task_from_user_message( + context.message + ) + if context.current_task is None: + await event_queue.enqueue_event(task) + context_id = task.context_id or task.id + updater = TaskUpdater( + event_queue=event_queue, + task_id=task.id, + context_id=task.context_id, + ) + await updater.start_work() + action = self._a2ui_action(context) + if action and action.get("name") == "submit_database_connection": + parts = await self._open_session( + action.get("context", {}), + context_id, + ) + artifact_name = "database-session" + extensions = None + elif context_id not in self.sessions: + parts = [_a2ui_part(message) for message in connection_form()] + artifact_name = "database-connection-form" + extensions = [A2UI_EXTENSION_URI] + else: + result = await self._send_prompt( + context_id, + context.get_user_input(), + ) + await add_team_result(updater, result) + await updater.complete() + return + await updater.add_artifact( + parts=parts, + name=artifact_name, + last_chunk=True, + extensions=extensions, + ) + await updater.complete() + + async def _open_session(self, action_context: dict, context_id: str): + try: + session_info = SessionInfo.from_a2ui_event(action_context) + session_id = await asyncio.to_thread( + self.worker_client.open_session, + context_id, + session_info, + ) + except (ValueError, RuntimeError): + return [ + new_text_part( + "Could not connect. Check the DSN, credentials, and team " + "name." + ) + ] + self.sessions[context_id] = session_id + return [new_text_part("Connected. Ask a database question.")] + + async def _send_prompt(self, context_id: str, prompt: str) -> str | None: + try: + result = await asyncio.to_thread( + self.worker_client.send_prompt, + self.sessions[context_id], + prompt, + ) + except ReconnectRequired: + self.sessions.pop(context_id, None) + return "Your database session ended. Please reconnect." + return result + + async def cancel(self, context, event_queue): + """Close the live worker session when the A2A task is cancelled.""" + task = context.current_task + if task is None: + return + context_id = task.context_id or task.id + session_id = self.sessions.pop(context_id, None) + if session_id: + await asyncio.to_thread( + self.worker_client.close_session, + session_id, + ) + updater = TaskUpdater( + event_queue=event_queue, + task_id=task.id, + context_id=task.context_id, + ) + await updater.cancel() + + @staticmethod + def _a2ui_action(context) -> dict | None: + for part in context.message.parts: + if part.WhichOneof("content") != "data": + continue + data = MessageToDict(part.data) + messages = data if isinstance(data, list) else [data] + for message in messages: + if ( + not isinstance(message, dict) + or message.get("version") != "v0.9" + ): + continue + action = message.get("action") + if isinstance(action, dict): + return action + return None + + +def _a2ui_part(message: dict): + """Encode one A2UI operation in the format used by Gemini Enterprise.""" + part = new_data_part(message) + ParseDict({"mimeType": A2UI_MIME_TYPE}, part.metadata) + return part + + +def create_gateway_app(settings: GatewaySettings) -> Starlette: + """Build a Gemini Enterprise-compatible A2A v0.3 gateway application.""" + description = "Connects a user to a temporary Select AI database session." + endpoint = f"{settings.agent_url}/a2a/jsonrpc/" + card = AgentCard( + name="Select AI Database Gateway", + description=description, + version=__version__, + default_input_modes=["text/plain", A2UI_MIME_TYPE], + default_output_modes=["text/plain", A2UI_MIME_TYPE], + capabilities=AgentCapabilities( + streaming=False, + extensions=[a2ui_extension()], + ), + supported_interfaces=[ + AgentInterface( + protocol_binding="JSONRPC", + protocol_version="1.0", + url=endpoint, + ), + AgentInterface( + protocol_binding="JSONRPC", + protocol_version="0.3", + url=endpoint, + ), + ], + skills=[ + AgentSkill( + id="database_connect", + name="Connect to database", + description=description, + tags=["oracle", "select-ai"], + examples=[], + input_modes=["text/plain", A2UI_MIME_TYPE], + output_modes=["text/plain", A2UI_MIME_TYPE], + ) + ], + ) + handler = DefaultRequestHandler( + agent_executor=GatewayExecutor(WorkerClient(settings)), + task_store=InMemoryTaskStore(), + agent_card=card, + ) + compat_card = to_compat_agent_card(card).model_dump( + by_alias=True, + exclude_none=True, + ) + + async def get_agent_card(_request): + return JSONResponse(compat_card) + + routes = [ + Route("/.well-known/agent-card.json", get_agent_card, methods=["GET"]) + ] + routes.extend( + create_jsonrpc_routes( + handler, + rpc_url="/a2a/jsonrpc/", + enable_v0_3_compat=True, + ) + ) + return Starlette(routes=routes) diff --git a/src/select_ai/agent/a2a/models.py b/src/select_ai/agent/a2a/models.py new file mode 100644 index 0000000..2812f61 --- /dev/null +++ b/src/select_ai/agent/a2a/models.py @@ -0,0 +1,71 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# https://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +"""Configuration and transient request models for the A2A runtime.""" + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class GatewaySettings: + """Configuration for the public gateway process.""" + + agent_url: str + consul_url: str + worker_service: str + session_ttl_seconds: int + worker_tls_ca_file: str | None = None + worker_tls_cert_file: str | None = None + worker_tls_key_file: str | None = None + + def __post_init__(self) -> None: + if self.session_ttl_seconds < 1: + raise ValueError("session_ttl_seconds must be at least 1") + tls_files = ( + self.worker_tls_ca_file, + self.worker_tls_cert_file, + self.worker_tls_key_file, + ) + if any(tls_files) and not all(tls_files): + raise ValueError( + "worker mTLS requires a CA file, client certificate, and " + "client key." + ) + object.__setattr__(self, "agent_url", self.agent_url.rstrip("/")) + object.__setattr__(self, "consul_url", self.consul_url.rstrip("/")) + + @property + def worker_mtls_enabled(self) -> bool: + """Whether gateway-to-worker calls require mutual TLS.""" + return self.worker_tls_ca_file is not None + + +@dataclass(frozen=True) +class SessionInfo: + """Credentials used only while opening one in-memory worker session.""" + + dsn: str + username: str + password: str + team_name: str + + @classmethod + def from_a2ui_event(cls, event: dict) -> "SessionInfo": + required = ("dsn", "username", "password", "team_name") + if not all( + isinstance(event.get(key), str) and event[key] for key in required + ): + raise ValueError("All database connection fields are required.") + return cls(**{key: event[key] for key in required}) + + +@dataclass(frozen=True) +class SessionRoute: + """Non-secret Consul record that routes a session to one worker.""" + + endpoint: str + expires_at: float diff --git a/src/select_ai/agent/a2a/results.py b/src/select_ai/agent/a2a/results.py new file mode 100644 index 0000000..7e5c0a4 --- /dev/null +++ b/src/select_ai/agent/a2a/results.py @@ -0,0 +1,58 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# https://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +"""Translate serialized Select AI team results into A2A response artifacts.""" + +from __future__ import annotations + +import json + +from a2a.helpers import new_data_part, new_text_part +from a2a.server.tasks import TaskUpdater +from google.protobuf.json_format import ParseDict + + +def message_parts(result: str | None): + """Convert a serialized A2A message into its constituent parts.""" + if not result: + return None + try: + payload = json.loads(result) + except json.JSONDecodeError: + return None + if not isinstance(payload, dict): + return None + serialized_parts = payload.get("parts") + if payload.get("kind") != "message" or not isinstance( + serialized_parts, + list, + ): + return None + parts = [] + for part in serialized_parts: + if not isinstance(part, dict): + return None + if part.get("kind") == "text" and isinstance(part.get("text"), str): + parts.append(new_text_part(part["text"])) + elif part.get("kind") == "data" and "data" in part: + output_part = new_data_part(part["data"]) + if isinstance(part.get("metadata"), dict): + ParseDict(part["metadata"], output_part.metadata) + parts.append(output_part) + else: + # Avoid silently discarding an unsupported part type. + return None + return parts or None + + +async def add_team_result(updater: TaskUpdater, result: str | None) -> None: + """Attach a raw team result as the completed database-result artifact.""" + await updater.add_artifact( + parts=message_parts(result) or [new_text_part(result or "")], + name="database-agent-result", + last_chunk=True, + ) diff --git a/src/select_ai/agent/a2a/server.py b/src/select_ai/agent/a2a/server.py index 6b28a26..d2c06fa 100644 --- a/src/select_ai/agent/a2a/server.py +++ b/src/select_ai/agent/a2a/server.py @@ -7,16 +7,11 @@ """A2A HTTP server for Oracle Database AI Agent Teams.""" -import json from contextlib import asynccontextmanager from typing import Optional from a2a.compat.v0_3.conversions import to_compat_agent_card -from a2a.helpers import ( - new_data_part, - new_task_from_user_message, - new_text_part, -) +from a2a.helpers import new_task_from_user_message from a2a.server.agent_execution import AgentExecutor from a2a.server.request_handlers import DefaultRequestHandler from a2a.server.routes import create_jsonrpc_routes @@ -28,29 +23,12 @@ import select_ai from select_ai.agent import AsyncTeam +from select_ai.agent.a2a.a2ui import A2UI_MIME_TYPE, a2ui_extension from select_ai.agent.a2a.context_store import OracleContextStore +from select_ai.agent.a2a.results import add_team_result from select_ai.agent.a2a.task_store import OracleTaskStore from select_ai.version import __version__ -_A2UI_MIME_TYPE = "application/a2ui+json" - - -def _a2ui_payload(result: str | None) -> dict | None: - """Return an A2UI response envelope, if ``RUN_TEAM`` returned one.""" - if not result: - return None - try: - payload = json.loads(result) - except json.JSONDecodeError: - return None - if not isinstance(payload, dict): - return None - if payload.get("metadata", {}).get("mimeType") != _A2UI_MIME_TYPE: - return None - if not isinstance(payload.get("data"), list): - return None - return payload - class DatabaseTeamExecutor(AgentExecutor): """Execute A2A requests with one Oracle conversation per A2A context.""" @@ -81,16 +59,7 @@ async def execute(self, context, event_queue): prompt=context.get_user_input(), params={"conversation_id": conversation_id}, ) - a2ui_payload = _a2ui_payload(result) - await updater.add_artifact( - parts=( - [new_data_part(a2ui_payload)] - if a2ui_payload is not None - else [new_text_part(result or "")] - ), - name="database-agent-result", - last_chunk=True, - ) + await add_team_result(updater, result) await updater.complete() async def cancel(self, context, event_queue): @@ -184,8 +153,11 @@ def _build_agent_card( description=description, version=__version__, default_input_modes=["text/plain"], - default_output_modes=["text/plain"], - capabilities=AgentCapabilities(streaming=True), + default_output_modes=["text/plain", A2UI_MIME_TYPE], + capabilities=AgentCapabilities( + streaming=True, + extensions=[a2ui_extension()], + ), supported_interfaces=[ AgentInterface( protocol_binding="JSONRPC", @@ -206,7 +178,7 @@ def _build_agent_card( tags=["oracle", "database", "select-ai"], examples=[], input_modes=["text/plain"], - output_modes=["text/plain"], + output_modes=["text/plain", A2UI_MIME_TYPE], ) ], ) diff --git a/src/select_ai/agent/a2a/worker.py b/src/select_ai/agent/a2a/worker.py new file mode 100644 index 0000000..d6e7b04 --- /dev/null +++ b/src/select_ai/agent/a2a/worker.py @@ -0,0 +1,436 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# https://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +"""Internal worker that owns temporary Select AI child processes.""" + +from __future__ import annotations + +import asyncio +import contextlib +import logging +import multiprocessing +import os +import socket +import time +from contextlib import asynccontextmanager +from dataclasses import dataclass +from multiprocessing.connection import Connection + +import httpx +from fastapi import FastAPI, HTTPException, status +from fastapi.responses import PlainTextResponse +from pydantic import BaseModel, Field, SecretStr + +LOGGER = logging.getLogger(__name__) + + +class OpenSessionRequest(BaseModel): + """Sensitive request accepted only on the internal worker interface.""" + + session_id: str + dsn: str = Field(min_length=1, max_length=4_000) + username: str = Field(min_length=1, max_length=128) + password: SecretStr = Field(min_length=1, max_length=1_024) + team_name: str = Field(min_length=1, max_length=128) + + +class PromptRequest(BaseModel): + """One user prompt for a previously opened session.""" + + prompt: str = Field(min_length=1, max_length=32_000) + + +@dataclass +class ChildSession: + """In-memory ownership record for one database-session process.""" + + process: multiprocessing.Process + connection: Connection + expires_at: float + lock: asyncio.Lock + + +class SessionWorker: + """Own one isolated Select AI runtime process for each session.""" + + def __init__( + self, + session_ttl_seconds: int, + session_start_timeout_seconds: int, + ) -> None: + self.session_ttl_seconds = session_ttl_seconds + self.session_start_timeout_seconds = session_start_timeout_seconds + self.sessions: dict[str, ChildSession] = {} + self.lock = asyncio.Lock() + + async def open(self, request: OpenSessionRequest) -> None: + """Start a child runtime and wait until its database pool is ready.""" + parent_connection, child_connection = multiprocessing.Pipe() + credentials = { + "user": request.username, + "password": request.password.get_secret_value(), + "dsn": request.dsn, + } + process = multiprocessing.Process( + target=_session_process_main, + args=( + child_connection, + credentials, + request.session_id, + request.team_name, + ), + daemon=True, + ) + process.start() + child_connection.close() + session = ChildSession( + process=process, + connection=parent_connection, + expires_at=time.monotonic() + self.session_ttl_seconds, + lock=asyncio.Lock(), + ) + try: + await self._wait_ready(session, request) + except Exception: + await self._terminate(session) + raise + async with self.lock: + previous = self.sessions.pop(request.session_id, None) + if previous: + await self._terminate(previous) + self.sessions[request.session_id] = session + + async def get(self, session_id: str) -> ChildSession: + """Return a live session, closing it if its expiry has elapsed.""" + async with self.lock: + session = self.sessions.get(session_id) + if session and ( + session.expires_at <= time.monotonic() + or not session.process.is_alive() + ): + self.sessions.pop(session_id, None) + await self._terminate(session) + session = None + if session is None: + raise HTTPException( + status_code=404, + detail="Database session expired; reconnect required.", + ) + return session + + async def send_prompt(self, session_id: str, prompt: str) -> str | None: + """Run one prompt in the process that owns this database session.""" + session = await self.get(session_id) + async with session.lock: + if not session.process.is_alive(): + await self._discard(session_id, session) + raise HTTPException( + status_code=404, + detail="Database session expired; reconnect required.", + ) + try: + await asyncio.to_thread( + session.connection.send, + {"type": "run", "prompt": prompt}, + ) + response = await self._receive(session, timeout_seconds=120) + except (EOFError, OSError, TimeoutError) as error: + await self._discard(session_id, session) + raise HTTPException( + status_code=502, + detail=( + "Database session is unavailable; reconnect required." + ), + ) from error + if response.get("type") == "result": + return response.get("result") + LOGGER.error("Select AI session process reported a command failure.") + raise HTTPException( + status_code=502, + detail="Database session is unavailable; reconnect required.", + ) + + async def close(self, session_id: str) -> None: + """Terminate a session on an explicit gateway DELETE request.""" + async with self.lock: + session = self.sessions.pop(session_id, None) + if session is None: + raise HTTPException( + status_code=404, + detail="Database session expired; reconnect required.", + ) + await self._terminate(session) + + async def close_all(self) -> None: + """Terminate all child sessions during worker shutdown.""" + async with self.lock: + sessions = list(self.sessions.values()) + self.sessions.clear() + for session in sessions: + await self._terminate(session) + + async def _wait_ready( + self, + session: ChildSession, + request: OpenSessionRequest, + ) -> None: + try: + response = await self._receive( + session, + timeout_seconds=self.session_start_timeout_seconds, + ) + except (EOFError, OSError, TimeoutError) as error: + raise HTTPException( + status_code=504, + detail="Database session start timed out.", + ) from error + if response.get("type") == "ready": + return + detail = response.get("detail", "Database login failed.") + for secret in ( + request.password.get_secret_value(), + request.username, + request.dsn, + ): + detail = detail.replace(secret, "[REDACTED]") + LOGGER.error("Select AI session startup failed: %s", detail[-2_000:]) + raise HTTPException(status_code=400, detail="Database login failed.") + + @staticmethod + async def _receive( + session: ChildSession, + timeout_seconds: float, + ) -> dict: + available = await asyncio.to_thread( + session.connection.poll, + timeout_seconds, + ) + if not available: + raise TimeoutError() + return await asyncio.to_thread(session.connection.recv) + + async def _discard(self, session_id: str, session: ChildSession) -> None: + async with self.lock: + if self.sessions.get(session_id) is session: + self.sessions.pop(session_id, None) + await self._terminate(session) + + @staticmethod + async def _terminate(session: ChildSession) -> None: + def stop() -> None: + try: + if session.process.is_alive(): + with contextlib.suppress(OSError): + session.connection.send({"type": "close"}) + session.process.join(timeout=5) + if session.process.is_alive(): + session.process.terminate() + session.process.join(timeout=5) + finally: + session.connection.close() + + await asyncio.to_thread(stop) + + +def _session_process_main( + connection: Connection, + credentials: dict[str, str], + session_id: str, + team_name: str, +) -> None: + """Entrypoint for a child that owns one Select AI database session.""" + try: + asyncio.run( + _run_session_process( + connection, + credentials, + session_id, + team_name, + ) + ) + finally: + connection.close() + + +async def _run_session_process( + connection: Connection, + credentials: dict[str, str], + session_id: str, + team_name: str, +) -> None: + """Open one async connection and execute raw ``AsyncTeam`` prompts.""" + import select_ai + from select_ai.agent import AsyncTeam + + ready = False + try: + await select_ai.async_connect( + user=credentials["user"], + password=credentials["password"], + dsn=credentials["dsn"], + ) + if not await select_ai.async_is_connected(): + raise RuntimeError("Database login failed.") + connection.send({"type": "ready"}) + ready = True + conversation_id = None + while True: + try: + command = await asyncio.to_thread(connection.recv) + except EOFError: + return + if command.get("type") == "close": + return + if command.get("type") != "run": + connection.send( + {"type": "error", "detail": "Invalid command."} + ) + continue + try: + if conversation_id is None: + conversation = select_ai.AsyncConversation( + attributes=select_ai.ConversationAttributes( + title=f"A2A {team_name}", + description=f"Temporary session {session_id}", + ) + ) + conversation_id = await conversation.create() + result = await AsyncTeam(team_name=team_name).run( + prompt=command["prompt"], + params={"conversation_id": conversation_id}, + ) + connection.send({"type": "result", "result": result}) + except Exception: + # Keep session-process failures private from gateway callers. + LOGGER.error("Select AI session command failed") + connection.send({"type": "error"}) + except Exception as error: + LOGGER.error("Select AI session process startup failed") + if not ready: + with contextlib.suppress(OSError): + connection.send({"type": "error", "detail": str(error)}) + finally: + with contextlib.suppress(Exception): + await select_ai.async_disconnect() + + +async def _register_with_consul( + consul_url: str, + worker_id: str, + worker_address: str, + worker_port: int, + worker_endpoint: str | None = None, +) -> None: + payload = { + "Name": "select-ai-worker", + "ID": worker_id, + "Address": worker_address, + "Port": worker_port, + "Check": {"TTL": "30s", "DeregisterCriticalServiceAfter": "1m"}, + } + if worker_endpoint: + payload["Meta"] = {"endpoint": worker_endpoint.rstrip("/")} + deadline = time.monotonic() + 30 + while True: + try: + async with httpx.AsyncClient(timeout=10) as client: + response = await client.put( + f"{consul_url}/v1/agent/service/register", + json=payload, + ) + response.raise_for_status() + return + except httpx.HTTPError: + if time.monotonic() >= deadline: + raise + await asyncio.sleep(1) + + +async def _heartbeat(consul_url: str, worker_id: str) -> None: + check_id = f"service:{worker_id}" + while True: + async with httpx.AsyncClient(timeout=10) as client: + response = await client.put( + f"{consul_url}/v1/agent/check/pass/{check_id}" + ) + response.raise_for_status() + await asyncio.sleep(10) + + +async def _deregister_from_consul(consul_url: str, worker_id: str) -> None: + async with httpx.AsyncClient(timeout=10) as client: + with contextlib.suppress(httpx.HTTPError): + await client.put( + f"{consul_url}/v1/agent/service/deregister/{worker_id}" + ) + + +def create_worker_app( + session_ttl_seconds: int = 900, + session_start_timeout_seconds: int = 30, +) -> FastAPI: + """Build the internal session-worker HTTP application.""" + worker = SessionWorker(session_ttl_seconds, session_start_timeout_seconds) + + consul_url = os.environ.get("CONSUL_HTTP_URL", "http://consul:8500") + consul_url = consul_url.rstrip("/") + worker_id = os.environ.get("WORKER_ID", socket.gethostname()) + worker_address = os.environ.get("WORKER_ADDRESS", socket.gethostname()) + worker_port = int(os.environ.get("WORKER_PORT", "8080")) + worker_endpoint = os.environ.get("WORKER_ENDPOINT") + + @asynccontextmanager + async def lifespan(_app): + await _register_with_consul( + consul_url, + worker_id, + worker_address, + worker_port, + worker_endpoint, + ) + heartbeat = asyncio.create_task(_heartbeat(consul_url, worker_id)) + try: + yield + finally: + heartbeat.cancel() + with contextlib.suppress(asyncio.CancelledError): + await heartbeat + await worker.close_all() + await _deregister_from_consul(consul_url, worker_id) + + app = FastAPI( + title="Select AI Session Worker", + docs_url=None, + redoc_url=None, + lifespan=lifespan, + ) + + @app.get("/health") + async def health() -> dict[str, str]: + return {"status": "ok"} + + @app.post("/sessions", status_code=status.HTTP_201_CREATED) + async def open_session(request: OpenSessionRequest) -> dict[str, str]: + await worker.open(request) + return {"status": "opened"} + + @app.post("/sessions/{session_id}/messages") + async def send_message( + session_id: str, + request: PromptRequest, + ) -> PlainTextResponse: + result = await worker.send_prompt(session_id, request.prompt) + return PlainTextResponse(result or "") + + @app.delete( + "/sessions/{session_id}", + status_code=status.HTTP_204_NO_CONTENT, + ) + async def close_session(session_id: str) -> None: + await worker.close(session_id) + + return app diff --git a/src/select_ai/agent/a2a/worker_client.py b/src/select_ai/agent/a2a/worker_client.py new file mode 100644 index 0000000..b149453 --- /dev/null +++ b/src/select_ai/agent/a2a/worker_client.py @@ -0,0 +1,174 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# https://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +"""Consul-backed routing client for the internal session worker.""" + +from __future__ import annotations + +import base64 +import json +import time +from threading import Lock + +import requests + +from select_ai.agent.a2a import GatewaySettings, SessionInfo, SessionRoute + + +class ReconnectRequired(RuntimeError): + """The worker no longer owns the requested in-memory session.""" + + +class WorkerClient: + """Open, route, and close short-lived Select AI worker sessions.""" + + def __init__(self, settings: GatewaySettings): + self.settings = settings + self._selection_lock = Lock() + self._next_worker = 0 + self._worker_request_kwargs: dict[str, object] = {} + if settings.worker_mtls_enabled: + self._worker_request_kwargs = { + "verify": settings.worker_tls_ca_file, + "cert": ( + settings.worker_tls_cert_file, + settings.worker_tls_key_file, + ), + } + + def open_session(self, session_id: str, session_info: SessionInfo) -> str: + """Open a session and save a non-secret route in Consul.""" + endpoint = self._select_worker() + response = requests.post( + f"{endpoint}/sessions", + json={"session_id": session_id, **session_info.__dict__}, + timeout=45, + **self._worker_request_kwargs, + ) + response.raise_for_status() + route = SessionRoute( + endpoint=endpoint, + expires_at=time.time() + self.settings.session_ttl_seconds, + ) + if not self._save_route(session_id, route): + self._close_worker_session(route, session_id) + raise RuntimeError("Could not create the database session.") + return session_id + + def send_prompt(self, session_id: str, prompt: str) -> str | None: + """Forward a prompt and return the worker's raw team result.""" + route = self._route_for(session_id) + response = requests.post( + f"{route.endpoint}/sessions/{session_id}/messages", + json={"prompt": prompt}, + timeout=130, + **getattr(self, "_worker_request_kwargs", {}), + ) + if response.status_code in (404, 502): + self._close_worker_session(route, session_id) + raise ReconnectRequired( + "Database session ended; reconnect required." + ) + response.raise_for_status() + return response.text or None + + def close_session(self, session_id: str) -> None: + """Close the child process and remove the Consul route.""" + try: + route = self._route_for(session_id) + except ReconnectRequired: + self._delete_route(session_id) + return + self._close_worker_session(route, session_id) + + def _select_worker(self) -> str: + response = requests.get( + f"{self.settings.consul_url}/v1/health/service/" + f"{self.settings.worker_service}", + params={"passing": "true"}, + timeout=10, + ) + response.raise_for_status() + workers = response.json() + if not workers: + raise RuntimeError("No healthy Select AI workers are available.") + with self._selection_lock: + worker = workers[self._next_worker % len(workers)] + self._next_worker += 1 + service = worker["Service"] + endpoint = service.get("Meta", {}).get("endpoint") + if endpoint: + endpoint = endpoint.rstrip("/") + if self.settings.worker_mtls_enabled and not endpoint.startswith( + "https://" + ): + raise RuntimeError( + "A worker registered a non-HTTPS endpoint while mTLS is " + "required." + ) + return endpoint + if self.settings.worker_mtls_enabled: + raise RuntimeError( + "Workers must register an HTTPS endpoint while mTLS is " + "required." + ) + address = service.get("Address") or worker["Node"]["Address"] + return f"http://{address}:{service['Port']}" + + def _route_for(self, session_id: str) -> SessionRoute: + response = requests.get( + f"{self.settings.consul_url}/v1/kv/select-ai/sessions/" + f"{session_id}", + timeout=10, + ) + if response.status_code == 404: + raise ReconnectRequired( + "Database session expired; reconnect required." + ) + response.raise_for_status() + value = response.json()[0]["Value"] + route = SessionRoute(**json.loads(base64.b64decode(value).decode())) + if route.expires_at <= time.time(): + self._close_worker_session(route, session_id) + raise ReconnectRequired( + "Database session expired; reconnect required." + ) + return route + + def _save_route(self, session_id: str, route: SessionRoute) -> bool: + response = requests.put( + f"{self.settings.consul_url}/v1/kv/select-ai/sessions/" + f"{session_id}?cas=0", + data=json.dumps(route.__dict__), + timeout=10, + ) + return response.ok and response.text.strip().lower() == "true" + + def _delete_route(self, session_id: str) -> None: + requests.delete( + f"{self.settings.consul_url}/v1/kv/select-ai/sessions/" + f"{session_id}", + timeout=10, + ) + + def _close_worker_session( + self, + route: SessionRoute, + session_id: str, + ) -> None: + try: + response = requests.delete( + f"{route.endpoint}/sessions/{session_id}", + timeout=10, + **getattr(self, "_worker_request_kwargs", {}), + ) + if response.status_code != 404: + response.raise_for_status() + except requests.RequestException: + pass + finally: + self._delete_route(session_id) diff --git a/src/select_ai/cli/a2a.py b/src/select_ai/cli/a2a.py index 9e3bb70..e43342e 100644 --- a/src/select_ai/cli/a2a.py +++ b/src/select_ai/cli/a2a.py @@ -7,6 +7,7 @@ import getpass import json +import ssl import click @@ -92,6 +93,156 @@ def serve( uvicorn.run(app, host=host, port=port) +@a2a.command("worker") +@click.option("--host", default="0.0.0.0", show_default=True) +@click.option("--port", default=8080, show_default=True, type=int) +@click.option( + "--session-ttl-seconds", + default=900, + show_default=True, + type=click.IntRange(min=1), +) +@click.option( + "--session-start-timeout-seconds", + default=30, + show_default=True, + type=click.IntRange(min=1), +) +@click.option( + "--tls-cert-file", + type=click.Path(exists=True, dir_okay=False, readable=True), + help="Worker TLS server certificate. Requires all --tls-* options.", +) +@click.option( + "--tls-key-file", + type=click.Path(exists=True, dir_okay=False, readable=True), + help="Worker TLS server private key. Requires all --tls-* options.", +) +@click.option( + "--tls-ca-file", + type=click.Path(exists=True, dir_okay=False, readable=True), + help="CA used to validate the gateway client certificate.", +) +def worker( + host, + port, + session_ttl_seconds, + session_start_timeout_seconds, + tls_cert_file, + tls_key_file, + tls_ca_file, +): + """Start the internal, in-memory Select AI session worker.""" + try: + from select_ai.agent.a2a import create_worker_app + except ImportError as error: + raise click.ClickException( + "Worker support requires the optional 'a2a' extra. " + "Install it with: pip install 'select_ai[a2a]'" + ) from error + + app = create_worker_app( + session_ttl_seconds=session_ttl_seconds, + session_start_timeout_seconds=session_start_timeout_seconds, + ) + tls_files = (tls_cert_file, tls_key_file, tls_ca_file) + if any(tls_files) and not all(tls_files): + raise click.ClickException( + "Worker mTLS requires --tls-cert-file, --tls-key-file, and " + "--tls-ca-file." + ) + uvicorn_options = {} + if tls_cert_file: + uvicorn_options = { + "ssl_certfile": tls_cert_file, + "ssl_keyfile": tls_key_file, + "ssl_ca_certs": tls_ca_file, + "ssl_cert_reqs": ssl.CERT_REQUIRED, + } + uvicorn.run(app, host=host, port=port, **uvicorn_options) + + +@a2a.command("gateway") +@click.option("--host", default="0.0.0.0", show_default=True) +@click.option("--port", default=8080, show_default=True, type=int) +@click.option( + "--agent-url", + required=True, + envvar="AGENT_URL", + help="Public base URL advertised in the gateway Agent Card.", +) +@click.option( + "--consul-url", + default="http://consul:8500", + show_default=True, + envvar="CONSUL_HTTP_URL", + help="Consul HTTP API URL.", +) +@click.option( + "--worker-service", + default="select-ai-worker", + show_default=True, + envvar="WORKER_SERVICE", + help="Consul service name for Select AI workers.", +) +@click.option( + "--session-ttl-seconds", + default=900, + show_default=True, + type=click.IntRange(min=1), + envvar="SESSION_TTL_SECONDS", +) +@click.option( + "--worker-tls-ca-file", + envvar="WORKER_TLS_CA_FILE", + type=click.Path(exists=True, dir_okay=False, readable=True), + help="CA bundle used to validate worker certificates.", +) +@click.option( + "--worker-tls-cert-file", + envvar="WORKER_TLS_CERT_FILE", + type=click.Path(exists=True, dir_okay=False, readable=True), + help="Gateway client certificate used for worker mTLS.", +) +@click.option( + "--worker-tls-key-file", + envvar="WORKER_TLS_KEY_FILE", + type=click.Path(exists=True, dir_okay=False, readable=True), + help="Gateway client private key used for worker mTLS.", +) +def gateway( + host, + port, + agent_url, + consul_url, + worker_service, + session_ttl_seconds, + worker_tls_ca_file, + worker_tls_cert_file, + worker_tls_key_file, +): + """Start the public A2A/A2UI database-session gateway.""" + try: + from select_ai.agent.a2a import GatewaySettings, create_gateway_app + except ImportError as error: + raise click.ClickException( + "Gateway support requires the optional 'a2a' extra. " + "Install it with: pip install 'select_ai[a2a]'" + ) from error + + settings = GatewaySettings( + agent_url=agent_url, + consul_url=consul_url, + worker_service=worker_service, + session_ttl_seconds=session_ttl_seconds, + worker_tls_ca_file=worker_tls_ca_file, + worker_tls_cert_file=worker_tls_cert_file, + worker_tls_key_file=worker_tls_key_file, + ) + app = create_gateway_app(settings) + uvicorn.run(app, host=host, port=port) + + @a2a.command("agent-card") @click.option("--team", "team_name", required=True, help="Database AI team.") @click.option( diff --git a/tests/a2a/test_agent_card.py b/tests/a2a/test_agent_card.py index 8b543c4..edd594c 100644 --- a/tests/a2a/test_agent_card.py +++ b/tests/a2a/test_agent_card.py @@ -12,6 +12,8 @@ pytest.importorskip("a2a") +from select_ai.agent.a2a.gateway import create_gateway_app +from select_ai.agent.a2a.models import GatewaySettings from select_ai.agent.a2a.server import ( _build_agent_card, _build_v03_agent_card, @@ -31,6 +33,21 @@ def test_v03_discovery_card_is_gemini_enterprise_compatible(): assert payload["protocolVersion"] == "0.3" assert payload["url"] == "https://agent.example.com/a2a/jsonrpc/" assert "supportedInterfaces" not in payload + assert "application/json+a2ui" in payload["defaultOutputModes"] + assert payload["capabilities"]["extensions"] == [ + { + "description": "Provides agent driven UI using the A2UI JSON format.", + "params": { + "acceptsInlineCatalogs": True, + "supportedCatalogIds": [ + "https://www.gstatic.com/vertexaisearch/a2ui/v0_9/" + "gemini_enterprise_composite_catalog.json" + ], + }, + "required": False, + "uri": "https://a2ui.org/a2a-extension/a2ui/v0.9", + } + ] def test_discovery_route_serves_only_the_v03_agent_card(): @@ -53,3 +70,41 @@ def test_discovery_route_serves_only_the_v03_agent_card(): assert payload["protocolVersion"] == "0.3" assert payload["url"] == "https://agent.example.com/a2a/jsonrpc/" assert "supportedInterfaces" not in payload + + +def test_gateway_card_advertises_a2ui_input_and_output(): + app = create_gateway_app( + GatewaySettings( + agent_url="https://agent.example.com", + consul_url="http://consul:8500", + worker_service="select-ai-worker", + session_ttl_seconds=900, + ) + ) + route = next( + route + for route in app.routes + if route.path == "/.well-known/agent-card.json" + ) + + response = asyncio.run(route.endpoint(None)) + payload = json.loads(response.body) + + assert "application/json+a2ui" in payload["defaultInputModes"] + assert "application/json+a2ui" in payload["defaultOutputModes"] + assert "application/json+a2ui" in payload["skills"][0]["inputModes"] + assert "application/json+a2ui" in payload["skills"][0]["outputModes"] + assert payload["capabilities"]["extensions"] == [ + { + "description": "Provides agent driven UI using the A2UI JSON format.", + "params": { + "acceptsInlineCatalogs": True, + "supportedCatalogIds": [ + "https://www.gstatic.com/vertexaisearch/a2ui/v0_9/" + "gemini_enterprise_composite_catalog.json" + ], + }, + "required": False, + "uri": "https://a2ui.org/a2a-extension/a2ui/v0.9", + } + ] diff --git a/tests/a2a/test_worker_runtime.py b/tests/a2a/test_worker_runtime.py new file mode 100644 index 0000000..ce9f953 --- /dev/null +++ b/tests/a2a/test_worker_runtime.py @@ -0,0 +1,382 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# https://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +"""Unit tests for the private multiprocessing session runtime.""" + +import asyncio + +import pytest + +pytest.importorskip("fastapi") + +import select_ai +from select_ai.agent.a2a import GatewaySettings, worker +from select_ai.agent.a2a.gateway import GatewayExecutor, _a2ui_part +from select_ai.agent.a2a.results import message_parts +from select_ai.agent.a2a.worker_client import WorkerClient + + +class FakeConnection: + """Small synchronous pipe stand-in for worker protocol tests.""" + + def __init__(self, responses=()): + self.responses = list(responses) + self.sent = [] + self.closed = False + + def send(self, value): + self.sent.append(value) + + def poll(self, _timeout): + return bool(self.responses) + + def recv(self): + return self.responses.pop(0) + + def close(self): + self.closed = True + + +class FakeProcess: + """Process stand-in which lets tests assert IPC without forking.""" + + def __init__(self, *, target, args, daemon): + self.target = target + self.args = args + self.daemon = daemon + self.started = False + self.alive = True + + def start(self): + self.started = True + + def is_alive(self): + return self.alive + + def join(self, timeout): + del timeout + + def terminate(self): + self.alive = False + + +def test_worker_uses_pipe_process_and_returns_raw_result(monkeypatch): + parent = FakeConnection( + [{"type": "ready"}, {"type": "result", "result": "hi"}] + ) + child = FakeConnection() + processes = [] + + def process_factory(**kwargs): + process = FakeProcess(**kwargs) + processes.append(process) + return process + + monkeypatch.setattr( + worker.multiprocessing, + "Pipe", + lambda: (parent, child), + ) + monkeypatch.setattr(worker.multiprocessing, "Process", process_factory) + session_worker = worker.SessionWorker(60, 1) + request = worker.OpenSessionRequest( + session_id="session-1", + username="user", + password="password", + dsn="database", + team_name="TEAM", + ) + + asyncio.run(session_worker.open(request)) + result = asyncio.run(session_worker.send_prompt("session-1", "hello")) + + assert result == "hi" + assert child.closed + assert processes[0].target is worker._session_process_main + assert processes[0].daemon is True + assert parent.sent == [{"type": "run", "prompt": "hello"}] + + +def test_expired_session_closes_the_process_and_pipe(monkeypatch): + parent = FakeConnection([{"type": "ready"}]) + child = FakeConnection() + process = None + + def process_factory(**kwargs): + nonlocal process + process = FakeProcess(**kwargs) + return process + + monkeypatch.setattr( + worker.multiprocessing, + "Pipe", + lambda: (parent, child), + ) + monkeypatch.setattr(worker.multiprocessing, "Process", process_factory) + session_worker = worker.SessionWorker(60, 1) + request = worker.OpenSessionRequest( + session_id="session-1", + username="user", + password="password", + dsn="database", + team_name="TEAM", + ) + asyncio.run(session_worker.open(request)) + session_worker.sessions["session-1"].expires_at = 0 + + with pytest.raises(worker.HTTPException, match="reconnect required"): + asyncio.run(session_worker.get("session-1")) + + assert process is not None + assert process.alive is False + assert parent.closed + assert parent.sent == [{"type": "close"}] + + +def test_session_runtime_uses_one_async_connection_and_returns_raw_result( + monkeypatch, +): + connection = FakeConnection( + [{"type": "run", "prompt": "hello"}, {"type": "close"}] + ) + connection_arguments = {} + + class Conversation: + def __init__(self, attributes): + self.attributes = attributes + + async def create(self): + return "conversation-1" + + class Team: + def __init__(self, team_name): + assert team_name == "TEAM" + + async def run(self, prompt, params): + assert prompt == "hello" + assert params == {"conversation_id": "conversation-1"} + return ( + '{"metadata":{"mimeType":"application/json+a2ui"},' + '"data":[]}' + ) + + async def async_connect(**kwargs): + connection_arguments.update(kwargs) + + async def connected(): + return True + + async def disconnect(): + return None + + monkeypatch.setattr(select_ai, "async_connect", async_connect) + monkeypatch.setattr(select_ai, "async_is_connected", connected) + monkeypatch.setattr(select_ai, "async_disconnect", disconnect) + monkeypatch.setattr(select_ai, "AsyncConversation", Conversation) + monkeypatch.setattr(select_ai.agent, "AsyncTeam", Team) + + asyncio.run( + worker._run_session_process( + connection, + { + "user": "user", + "password": "password", + "dsn": "database", + }, + "session-1", + "TEAM", + ) + ) + + assert connection_arguments == { + "user": "user", + "password": "password", + "dsn": "database", + } + assert connection.sent[0] == {"type": "ready"} + assert connection.sent[1] == { + "type": "result", + "result": ( + '{"metadata":{"mimeType":"application/json+a2ui"},' '"data":[]}' + ), + } + + +def test_message_parts_preserves_serialized_text_and_data_parts(): + parts = message_parts( + """{ + "kind": "message", + "parts": [ + {"kind": "text", "text": "hello"}, + { + "kind": "data", + "data": {"value": 1}, + "metadata": {"mimeType": "application/json+a2ui"} + } + ] + }""" + ) + + assert parts is not None + assert parts[0].text == "hello" + assert parts[1].WhichOneof("content") == "data" + + +def test_worker_client_returns_the_raw_team_result(monkeypatch): + class Response: + status_code = 200 + + @staticmethod + def raise_for_status(): + return None + + text = "team result" + + monkeypatch.setattr( + "select_ai.agent.a2a.worker_client.requests.post", + lambda *args, **kwargs: Response(), + ) + client = WorkerClient.__new__(WorkerClient) + monkeypatch.setattr( + client, + "_route_for", + lambda session_id: type("Route", (), {"endpoint": "http://worker"})(), + ) + + assert client.send_prompt("context-1", "hello") == "team result" + + +def test_worker_client_uses_consul_https_endpoint_with_mtls(monkeypatch): + settings = GatewaySettings( + agent_url="https://gateway.example.com", + consul_url="http://consul:8500", + worker_service="select-ai-worker", + session_ttl_seconds=60, + worker_tls_ca_file="/tls/ca.pem", + worker_tls_cert_file="/tls/gateway.pem", + worker_tls_key_file="/tls/gateway-key.pem", + ) + client = WorkerClient(settings) + + class Response: + @staticmethod + def raise_for_status(): + return None + + @staticmethod + def json(): + return [ + { + "Service": { + "Meta": {"endpoint": "https://worker-0.internal"} + }, + "Node": {}, + } + ] + + monkeypatch.setattr( + "select_ai.agent.a2a.worker_client.requests.get", + lambda *args, **kwargs: Response(), + ) + + assert client._select_worker() == "https://worker-0.internal" + assert client._worker_request_kwargs == { + "verify": "/tls/ca.pem", + "cert": ("/tls/gateway.pem", "/tls/gateway-key.pem"), + } + + +def test_mtls_requires_all_three_gateway_files(): + with pytest.raises(ValueError, match="worker mTLS requires"): + GatewaySettings( + agent_url="https://gateway.example.com", + consul_url="http://consul:8500", + worker_service="select-ai-worker", + session_ttl_seconds=60, + worker_tls_ca_file="/tls/ca.pem", + ) + + +def test_worker_registers_its_https_endpoint(monkeypatch): + registered = {} + + class Response: + @staticmethod + def raise_for_status(): + return None + + class Client: + def __init__(self, **kwargs): + del kwargs + + async def __aenter__(self): + return self + + async def __aexit__(self, *args): + return None + + async def put(self, _url, json): + registered.update(json) + return Response() + + monkeypatch.setattr(worker.httpx, "AsyncClient", Client) + asyncio.run( + worker._register_with_consul( + "http://consul:8500", + "worker-0", + "10.0.0.1", + 8080, + "https://worker-0.internal", + ) + ) + + assert registered["Meta"] == {"endpoint": "https://worker-0.internal"} + + +def test_a2ui_operation_uses_a_metadata_marked_data_part(): + from google.protobuf.json_format import MessageToDict + + part = _a2ui_part( + {"version": "v0.9", "createSurface": {"surfaceId": "form"}} + ) + + assert MessageToDict(part.data) == { + "version": "v0.9", + "createSurface": {"surfaceId": "form"}, + } + assert MessageToDict(part.metadata) == { + "mimeType": "application/json+a2ui" + } + + +def test_a2ui_action_reads_an_operation_list_or_single_operation(): + action = GatewayExecutor._a2ui_action( + type( + "Context", + (), + { + "message": type( + "Message", + (), + { + "parts": [ + _a2ui_part( + { + "version": "v0.9", + "action": { + "name": "submit_database_connection" + }, + } + ) + ] + }, + )() + }, + )() + ) + + assert action == {"name": "submit_database_connection"} From 1f42af0e52a9680e03e94c6ee337e67528753937 Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Mon, 31 Aug 2026 20:44:38 -0700 Subject: [PATCH 15/31] Fixes after E2E testing with Gemini Enterprise --- gcloud/gateway/README.md | 116 ++++++++++++++----------- gcloud/gateway/cloudbuild.yaml | 48 +++++----- gcloud/gateway/deploy.sh | 103 ++++++++++++++++------ gcloud/gateway/gke/worker-mtls.yaml | 4 +- gcloud/gateway/gke/worker-service.yaml | 18 ++++ gcloud/gateway/gke/worker.yaml | 2 + src/select_ai/agent/a2a/gateway.py | 3 +- tests/a2a/test_worker_runtime.py | 28 ++++++ 8 files changed, 216 insertions(+), 106 deletions(-) create mode 100644 gcloud/gateway/gke/worker-service.yaml diff --git a/gcloud/gateway/README.md b/gcloud/gateway/README.md index a992159..a01ee79 100644 --- a/gcloud/gateway/README.md +++ b/gcloud/gateway/README.md @@ -1,21 +1,33 @@ # Dynamic gateway deployment -Dynamic gateway mode exposes one public A2A endpoint where each user selects -an Oracle database connection and Select AI team through the A2UI connection -form. +Dynamic gateway mode exposes one public A2A endpoint. Each user dynamically +selects an Oracle database connection and Select AI team through the A2UI +connection form. A session remains available for 15 minutes by default. Set a +different lifetime in seconds with `--session-ttl-seconds`; for example, +`--session-ttl-seconds 1800` keeps sessions for 30 minutes. ```text -A2A client - | -Cloud Run gateway - | - +-- private VPC --> Consul in GKE - | - +-- private VPC --> worker replicas in GKE - | - session child process - | - Oracle Database + ┌──────────────────────┐ + │ A2A / Gemini client │ + └──────────┬───────────┘ + │ public A2A + v + ┌──────────────────────┐ + │ Cloud Run gateway │ + └──────┬───────┬───────┘ + │ │ private VPC: mTLS request to worker hostname + │ │ + │ │ ┌─────────────────────── GKE ───────────────────────┐ + │ └────>│ [Headless Service + managed VPC DNS] │ + │ │ worker hostname → current worker Pod IP │ + │ │ │ │ + │ │ v │ + │ │ [StatefulSet worker-0 / worker-1 / ...] │ + │ │ session child process → Oracle Database │ + │ │ │ + │ │ [Consul] │ + └────────────>│ selects healthy worker; returns worker hostname │ + └───────────────────────────────────────────────────┘ ``` The gateway is the only public A2A application. Consul and workers are a GKE @@ -31,7 +43,9 @@ gcloud/gateway/deploy.sh --project PROJECT_ID ``` The script creates the Artifact Registry repository and GKE Autopilot cluster -when they do not already exist. Cloud Build then: +when they do not already exist. The cluster is created with +GKE additive VPC DNS: GKE owns the worker DNS records and keeps them current +when a worker Pod is recreated. Cloud Build then: 1. builds the existing `docker/Dockerfile` image once; 2. deploys the GKE namespace and internal Consul service; @@ -47,6 +61,7 @@ gcloud/gateway/deploy.sh \ --project PROJECT_ID \ --region us-central1 \ --cluster select-ai-a2a-gateway \ + --gke-dns-domain select-ai-a2a-gateway.internal \ --worker-replicas 3 \ --network default \ --subnet default @@ -67,13 +82,12 @@ deployment time. Local testing does not use mTLS. The default GCloud deployment also keeps the current private-VPC HTTP worker transport. -For a short-lived GCloud mTLS test, add a private DNS suffix: +For a short-lived GCloud mTLS test: ```bash gcloud/gateway/deploy.sh \ --project PROJECT_ID \ --enable-worker-mtls \ - --worker-domain workers.select-ai.internal \ --mtls-cert-validity-days 365 ``` @@ -93,31 +107,45 @@ reuse them, including when changing `--worker-replicas`. To deliberately replace the CA and both leaf certificates, add `--rotate-worker-mtls`. Rotation recreates the worker StatefulSet and ends active worker sessions. -Workers run as a StatefulSet. Each worker registers its own private DNS name -with Consul, so the gateway still reaches the exact process that owns the -session. Cloud Build creates the corresponding private Cloud DNS A records. +Workers run as a StatefulSet. The `select-ai-worker` headless Service gives +each worker a stable name, for example +`select-ai-worker-0.select-ai-worker.select-ai-gateway.svc.select-ai-a2a-gateway.internal`. +Consul registers that name, so the gateway reaches the exact worker that owns a +session. GKE Cloud DNS updates its Pod-IP record automatically after a worker +is recreated. There is no worker load balancer, custom Cloud DNS zone, or +deployment-time Pod-IP snapshot. -The certificate and DNS records are refreshed by rerunning the deployment. -Because this is a test mode, rerun it after a worker pod is recreated outside a -deployment; its pod IP can change. Do not use this mode for a long-lived -production stack—use a managed workload-certificate and DNS reconciliation -solution there. +`--gke-dns-domain` must be unique in the VPC and cannot end in `.local`. It is +immutable after cluster creation. Autopilot supports additive VPC DNS only when +the cluster is created, so an older cluster without it cannot be reused by this +deployment. Use a new `--cluster` name for the first migration, verify it, then +delete the old cluster when you are ready. -### Where the worker certificate files come from +If existing mTLS material was issued for a different GKE DNS domain, the script +replaces it automatically before deploying the replacement cluster. -The certificate paths passed to `select-ai a2a worker` are files **inside each -GKE worker container**. They are mounted from Kubernetes Secrets, not files in -this repository or on the machine that runs `deploy.sh`. +### How a worker DNS name is decided + +GKE gives a StatefulSet Pod a DNS name using this form: + +```text +...svc. +``` + +For this deployment, worker 0 is: ```text -deploy.sh - -> creates the test certificates and stores them in Secret Manager - -> Cloud Build reads the worker certificate, worker key, and CA certificate - -> Cloud Build creates Kubernetes Secrets in select-ai-gateway - -> GKE mounts those Secrets read-only in every worker container +select-ai-worker-0.select-ai-worker.select-ai-gateway.svc.select-ai-a2a-gateway.internal ``` -The worker pod receives these mounts: +`select-ai-worker-0` is the StatefulSet Pod name, `select-ai-worker` is the +headless Service, `select-ai-gateway` is the Kubernetes namespace, and +`select-ai-a2a-gateway.internal` is the `--gke-dns-domain` value. GKE updates +the resulting record when the Pod IP changes. + +### Certificate mounts + +Worker certificate files are mounted from Kubernetes Secrets: | Container file | Kubernetes Secret | Secret key | Used for | | --- | --- | --- | --- | @@ -125,21 +153,7 @@ The worker pod receives these mounts: | `/var/run/select-ai-mtls/tls.key` | `select-ai-worker-server-tls` | `tls.key` | worker HTTPS private key | | `/var/run/select-ai-mtls/gateway-ca.crt` | `select-ai-gateway-client-ca` | `ca.crt` | validates the gateway client certificate | -GKE does not read Google Secret Manager at worker request time. Cloud Build -copies the required material into Kubernetes Secrets during deployment, and the -worker certificate files are mounted from those Kubernetes Secrets as read-only -files. For example, to inspect the paths in a running worker (without printing -their contents): - -```bash -kubectl -n select-ai-gateway exec select-ai-worker-0 -- \ - ls -l /var/run/select-ai-mtls/ -``` - -The Cloud Run gateway uses a separate direct mount from Google Secret Manager -for its CA, client certificate, and client key. +The Cloud Run gateway certificate files are mounted from Google Secret Manager. The identity that submits Cloud Build needs permission to use GKE, Cloud Run, -Cloud DNS, and Secret Manager. Cloud Build also needs the corresponding GKE, -Cloud Run, Cloud DNS, and Secret Manager permissions because it deploys the -workers, refreshes the private DNS records, and mounts the gateway secrets. +and Secret Manager. GKE maintains the managed worker DNS records. diff --git a/gcloud/gateway/cloudbuild.yaml b/gcloud/gateway/cloudbuild.yaml index 69958b7..89bb0af 100644 --- a/gcloud/gateway/cloudbuild.yaml +++ b/gcloud/gateway/cloudbuild.yaml @@ -25,9 +25,23 @@ steps: - --location=${_REGION} - --cluster=${_CLUSTER} - # gke-deploy writes generated manifests to output/. Cloud Build shares the - # source workspace across steps, so remove only that builder output before - # invoking it again. + # gke-deploy writes generated manifests to output/. Remove its output before + # running it for the headless worker Service. + - name: gcr.io/cloud-builders/gcloud + entrypoint: bash + args: + - -ceu + - rm -rf output + + - name: gcr.io/cloud-builders/gke-deploy + args: + - run + - --filename=gcloud/gateway/gke/worker-service.yaml + - --location=${_REGION} + - --cluster=${_CLUSTER} + + # Cloud Build shares the source workspace across steps, so remove only the + # gke-deploy builder output before invoking another deployment tool. - name: gcr.io/cloud-builders/gcloud entrypoint: bash args: @@ -115,7 +129,8 @@ steps: fi sed -e "s|WORKER_IMAGE|$image|" \ -e "s|WORKER_REPLICAS|${_WORKER_REPLICAS}|" \ - -e "s|WORKER_DOMAIN|${_WORKER_DOMAIN}|" \ + -e "s|SESSION_TTL_SECONDS|${_SESSION_TTL_SECONDS}|" \ + -e "s|GKE_DNS_DOMAIN|${_GKE_DNS_DOMAIN}|" \ "$worker_manifest" > /workspace/worker.yaml - name: gcr.io/cloud-builders/gcloud @@ -141,29 +156,10 @@ steps: - | if [[ "${_ENABLE_WORKER_MTLS}" == "true" ]]; then kubectl rollout status --namespace select-ai-gateway statefulset/select-ai-worker --timeout=300s - kubectl get pods --namespace select-ai-gateway -l app=select-ai-worker \ - -o jsonpath='{range .items[*]}{.metadata.name}={.status.podIP}{"\n"}{end}' \ - > /workspace/worker-pods else kubectl rollout status --namespace select-ai-gateway deployment/select-ai-worker --timeout=300s fi - - name: gcr.io/cloud-builders/gcloud - entrypoint: bash - args: - - -ceu - - | - if [[ "${_ENABLE_WORKER_MTLS}" != "true" ]]; then - exit 0 - fi - while IFS== read -r pod_name pod_ip; do - record_name="${pod_name}.${_WORKER_DOMAIN}." - gcloud dns record-sets delete "$record_name" --type=A \ - --zone="${_WORKER_DNS_ZONE}" --quiet || true - gcloud dns record-sets create "$record_name" --type=A --ttl=30 \ - --rrdatas="$pod_ip" --zone="${_WORKER_DNS_ZONE}" - done < /workspace/worker-pods - - name: gcr.io/cloud-builders/gcloud entrypoint: bash args: @@ -191,7 +187,7 @@ steps: --port=8080 \ --command=select-ai \ --args=a2a,gateway \ - --set-env-vars="AGENT_URL=https://pending.invalid,CONSUL_HTTP_URL=http://$consul_ip:8500,WORKER_SERVICE=select-ai-worker,SESSION_TTL_SECONDS=900$mtls_env" \ + --set-env-vars="AGENT_URL=https://pending.invalid,CONSUL_HTTP_URL=http://$consul_ip:8500,WORKER_SERVICE=select-ai-worker,SESSION_TTL_SECONDS=${_SESSION_TTL_SECONDS}$mtls_env" \ "${mtls_args[@]}" gateway_url=$(gcloud run services describe "${_GATEWAY_SERVICE}" \ --region="${_REGION}" --format='value(status.url)') @@ -211,10 +207,10 @@ substitutions: _NETWORK: default _SUBNET: default _WORKER_REPLICAS: "2" + _SESSION_TTL_SECONDS: "900" _ENABLE_WORKER_MTLS: "false" _ROTATE_WORKER_MTLS: "false" - _WORKER_DOMAIN: "" - _WORKER_DNS_ZONE: select-ai-workers + _GKE_DNS_DOMAIN: select-ai-a2a-gateway.internal options: logging: CLOUD_LOGGING_ONLY diff --git a/gcloud/gateway/deploy.sh b/gcloud/gateway/deploy.sh index 1923940..55959c8 100755 --- a/gcloud/gateway/deploy.sh +++ b/gcloud/gateway/deploy.sh @@ -18,17 +18,19 @@ Usage: gcloud/gateway/deploy.sh [options] Options: --project PROJECT Google Cloud project (defaults to gcloud config) - --region REGION Region for GKE, Cloud Run, and Artifact Registry (default: us-central1) + --region REGION Region for GKE, Cloud Run, and Artifact Registry + (default: us-central1) --cluster NAME GKE Autopilot cluster name (default: select-ai-a2a-gateway) + --gke-dns-domain DOMAIN Unique GKE additive VPC DNS domain + (default: select-ai-a2a-gateway.internal) --repository NAME Artifact Registry Docker repository (default: select-ai) --gateway-service NAME Cloud Run gateway service name (default: select-ai-a2a-gateway) --network NAME VPC network for Cloud Run direct VPC egress (default: default) --subnet NAME VPC subnet for Cloud Run direct VPC egress (default: default) --worker-replicas COUNT GKE worker replica count (default: 2) + --session-ttl-seconds N Dynamic session lifetime (default: 900) --enable-worker-mtls Use ephemeral mTLS certificates for gateway-to-worker calls --rotate-worker-mtls Replace the existing worker mTLS CA and certificates - --worker-domain DOMAIN Private DNS suffix for workers; required with --enable-worker-mtls - --worker-dns-zone NAME Private Cloud DNS zone name (default: select-ai-workers) --mtls-cert-validity-days DAYS Gateway and worker certificate lifetime (default: 365) --image-tag TAG Image tag (default: git SHA plus UTC timestamp) @@ -40,15 +42,15 @@ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" project_id="" region="us-central1" cluster="select-ai-a2a-gateway" +gke_dns_domain="select-ai-a2a-gateway.internal" repository="select-ai" gateway_service="select-ai-a2a-gateway" network="default" subnet="default" worker_replicas="2" +session_ttl_seconds="900" enable_worker_mtls="false" rotate_worker_mtls="false" -worker_domain="" -worker_dns_zone="select-ai-workers" mtls_cert_validity_days="365" image_tag="" @@ -57,16 +59,22 @@ while [[ $# -gt 0 ]]; do --project) project_id="${2:?--project requires a value}"; shift 2 ;; --region) region="${2:?--region requires a value}"; shift 2 ;; --cluster) cluster="${2:?--cluster requires a value}"; shift 2 ;; + --gke-dns-domain) gke_dns_domain="${2:?--gke-dns-domain requires a value}"; shift 2 ;; --repository) repository="${2:?--repository requires a value}"; shift 2 ;; --gateway-service) gateway_service="${2:?--gateway-service requires a value}"; shift 2 ;; --network) network="${2:?--network requires a value}"; shift 2 ;; --subnet) subnet="${2:?--subnet requires a value}"; shift 2 ;; --worker-replicas) worker_replicas="${2:?--worker-replicas requires a value}"; shift 2 ;; + --session-ttl-seconds) + session_ttl_seconds="${2:?--session-ttl-seconds requires a value}" + shift 2 + ;; --enable-worker-mtls) enable_worker_mtls="true"; shift ;; --rotate-worker-mtls) rotate_worker_mtls="true"; shift ;; - --worker-domain) worker_domain="${2:?--worker-domain requires a value}"; shift 2 ;; - --worker-dns-zone) worker_dns_zone="${2:?--worker-dns-zone requires a value}"; shift 2 ;; - --mtls-cert-validity-days) mtls_cert_validity_days="${2:?--mtls-cert-validity-days requires a value}"; shift 2 ;; + --mtls-cert-validity-days) + mtls_cert_validity_days="${2:?--mtls-cert-validity-days requires a value}" + shift 2 + ;; --image-tag) image_tag="${2:?--image-tag requires a value}"; shift 2 ;; -h|--help) usage; exit 0 ;; *) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;; @@ -84,8 +92,12 @@ if ! [[ "$worker_replicas" =~ ^[1-9][0-9]*$ ]]; then echo "--worker-replicas must be a positive integer." >&2 exit 2 fi -if [[ "$enable_worker_mtls" == "true" && -z "$worker_domain" ]]; then - echo "--worker-domain is required with --enable-worker-mtls." >&2 +if ! [[ "$session_ttl_seconds" =~ ^[1-9][0-9]*$ ]]; then + echo "--session-ttl-seconds must be a positive integer." >&2 + exit 2 +fi +if [[ -z "$gke_dns_domain" || "$gke_dns_domain" == *.local ]]; then + echo "--gke-dns-domain must be non-empty and must not end in .local." >&2 exit 2 fi if [[ "$rotate_worker_mtls" == "true" && "$enable_worker_mtls" != "true" ]]; then @@ -119,15 +131,29 @@ fi if ! gcloud container clusters describe "$cluster" \ --location="$region" --project="$project_id" >/dev/null 2>&1; then gcloud container clusters create-auto "$cluster" \ - --location="$region" --network="$network" --project="$project_id" + --location="$region" --network="$network" --subnetwork="$subnet" \ + --additive-vpc-scope-dns-domain="$gke_dns_domain" --project="$project_id" +else + configured_dns_domain="$(gcloud container clusters describe "$cluster" \ + --location="$region" --project="$project_id" \ + --format='value(networkConfig.dnsConfig.additiveVpcScopeDnsDomain)')" + if [[ "$configured_dns_domain" != "$gke_dns_domain" ]]; then + echo "Cluster $cluster does not use additive VPC DNS domain $gke_dns_domain." >&2 + echo "Create a replacement cluster or pass its configured --gke-dns-domain." >&2 + exit 2 + fi fi +project_number="$(gcloud projects describe "$project_id" \ + --format='value(projectNumber)')" + cleanup_mtls() { [[ -n "${mtls_dir:-}" ]] && rm -rf "$mtls_dir" } if [[ "$enable_worker_mtls" == "true" ]]; then create_mtls_material="$rotate_worker_mtls" + worker_certificate_dns_name="*.select-ai-worker.select-ai-gateway.svc.$gke_dns_domain" for secret_name in \ select-ai-gateway-mtls-ca \ select-ai-gateway-mtls-cert \ @@ -139,6 +165,16 @@ if [[ "$enable_worker_mtls" == "true" ]]; then create_mtls_material="true" fi done + if [[ "$create_mtls_material" != "true" ]]; then + existing_worker_certificate_sans="$(gcloud secrets versions access latest \ + --secret=select-ai-worker-mtls-cert --project="$project_id" 2>/dev/null | \ + openssl x509 -noout -ext subjectAltName 2>/dev/null || true)" + if [[ "$existing_worker_certificate_sans" != *"DNS:$worker_certificate_dns_name"* ]]; then + create_mtls_material="true" + echo "Replacing mTLS material because the worker certificate does not" + echo "match the GKE DNS domain." + fi + fi if [[ "$create_mtls_material" == "true" ]]; then trap cleanup_mtls EXIT mtls_dir="$(mktemp -d)" @@ -149,8 +185,8 @@ if [[ "$enable_worker_mtls" == "true" ]]; then -subj "/CN=select-ai ephemeral worker CA" >/dev/null 2>&1 openssl req -newkey rsa:2048 -nodes \ -keyout "$mtls_dir/worker.key" -out "$mtls_dir/worker.csr" \ - -subj "/CN=*.$worker_domain" >/dev/null 2>&1 - printf 'subjectAltName=DNS:*.%s\nextendedKeyUsage=serverAuth\n' "$worker_domain" \ + -subj "/CN=select-ai-worker" >/dev/null 2>&1 + printf 'subjectAltName=DNS:%s\nextendedKeyUsage=serverAuth\n' "$worker_certificate_dns_name" \ > "$mtls_dir/worker.ext" openssl x509 -req -days "$mtls_cert_validity_days" -in "$mtls_dir/worker.csr" \ -CA "$mtls_dir/ca.crt" -CAkey "$mtls_dir/ca.key" -CAcreateserial \ @@ -171,11 +207,16 @@ if [[ "$enable_worker_mtls" == "true" ]]; then gcloud secrets describe "$secret_name" --project="$project_id" >/dev/null 2>&1 || \ gcloud secrets create "$secret_name" --replication-policy=automatic --project="$project_id" done - gcloud secrets versions add select-ai-gateway-mtls-ca --data-file="$mtls_dir/ca.crt" --project="$project_id" - gcloud secrets versions add select-ai-gateway-mtls-cert --data-file="$mtls_dir/gateway.crt" --project="$project_id" - gcloud secrets versions add select-ai-gateway-mtls-key --data-file="$mtls_dir/gateway.key" --project="$project_id" - gcloud secrets versions add select-ai-worker-mtls-cert --data-file="$mtls_dir/worker.crt" --project="$project_id" - gcloud secrets versions add select-ai-worker-mtls-key --data-file="$mtls_dir/worker.key" --project="$project_id" + gcloud secrets versions add select-ai-gateway-mtls-ca \ + --data-file="$mtls_dir/ca.crt" --project="$project_id" + gcloud secrets versions add select-ai-gateway-mtls-cert \ + --data-file="$mtls_dir/gateway.crt" --project="$project_id" + gcloud secrets versions add select-ai-gateway-mtls-key \ + --data-file="$mtls_dir/gateway.key" --project="$project_id" + gcloud secrets versions add select-ai-worker-mtls-cert \ + --data-file="$mtls_dir/worker.crt" --project="$project_id" + gcloud secrets versions add select-ai-worker-mtls-key \ + --data-file="$mtls_dir/worker.key" --project="$project_id" if [[ "$rotate_worker_mtls" == "true" ]]; then echo "Rotating worker mTLS material; worker sessions will be interrupted." else @@ -184,13 +225,7 @@ if [[ "$enable_worker_mtls" == "true" ]]; then else echo "Reusing existing worker mTLS material." fi - if ! gcloud dns managed-zones describe "$worker_dns_zone" --project="$project_id" >/dev/null 2>&1; then - gcloud dns managed-zones create "$worker_dns_zone" --dns-name="${worker_domain}." \ - --visibility=private --networks="https://www.googleapis.com/compute/v1/projects/$project_id/global/networks/$network" \ - --description="Private DNS records for Select AI mTLS workers" \ - --project="$project_id" - fi - runtime_sa="${project_id_number:-$(gcloud projects describe "$project_id" --format='value(projectNumber)')}-compute@developer.gserviceaccount.com" + runtime_sa="${project_number}-compute@developer.gserviceaccount.com" for secret_name in \ select-ai-gateway-mtls-ca \ select-ai-gateway-mtls-cert \ @@ -202,15 +237,29 @@ if [[ "$enable_worker_mtls" == "true" ]]; then done fi +build_substitutions=( + "_REGION=$region" + "_CLUSTER=$cluster" + "_REPOSITORY=$repository" + "_IMAGE_TAG=$image_tag" + "_GATEWAY_SERVICE=$gateway_service" + "_NETWORK=$network" + "_SUBNET=$subnet" + "_WORKER_REPLICAS=$worker_replicas" + "_SESSION_TTL_SECONDS=$session_ttl_seconds" + "_ENABLE_WORKER_MTLS=$enable_worker_mtls" + "_ROTATE_WORKER_MTLS=$rotate_worker_mtls" + "_GKE_DNS_DOMAIN=$gke_dns_domain" +) + gcloud builds submit "$repo_root" \ --project="$project_id" \ --region="$region" \ --config="$repo_root/gcloud/gateway/cloudbuild.yaml" \ - --substitutions="_REGION=$region,_CLUSTER=$cluster,_REPOSITORY=$repository,_IMAGE_TAG=$image_tag,_GATEWAY_SERVICE=$gateway_service,_NETWORK=$network,_SUBNET=$subnet,_WORKER_REPLICAS=$worker_replicas,_ENABLE_WORKER_MTLS=$enable_worker_mtls,_ROTATE_WORKER_MTLS=$rotate_worker_mtls,_WORKER_DOMAIN=$worker_domain,_WORKER_DNS_ZONE=$worker_dns_zone" + --substitutions="$(IFS=,; printf '%s' "${build_substitutions[*]}")" gateway_url="$(gcloud run services describe "$gateway_service" \ --region="$region" --project="$project_id" --format='value(status.url)')" -project_number="$(gcloud projects describe "$project_id" --format='value(projectNumber)')" gemini_service_agent="service-$project_number@gcp-sa-discoveryengine.iam.gserviceaccount.com" gcloud run services add-iam-policy-binding "$gateway_service" \ diff --git a/gcloud/gateway/gke/worker-mtls.yaml b/gcloud/gateway/gke/worker-mtls.yaml index 9861212..bdab56e 100644 --- a/gcloud/gateway/gke/worker-mtls.yaml +++ b/gcloud/gateway/gke/worker-mtls.yaml @@ -26,6 +26,8 @@ spec: - 0.0.0.0 - --port - "8443" + - --session-ttl-seconds + - "SESSION_TTL_SECONDS" - --tls-cert-file - /var/run/select-ai-mtls/tls.crt - --tls-key-file @@ -53,7 +55,7 @@ spec: - name: WORKER_PORT value: "8443" - name: WORKER_ENDPOINT - value: https://$(POD_NAME).WORKER_DOMAIN:8443 + value: https://$(POD_NAME).select-ai-worker.select-ai-gateway.svc.GKE_DNS_DOMAIN:8443 volumeMounts: - name: worker-server-tls mountPath: /var/run/select-ai-mtls/tls.crt diff --git a/gcloud/gateway/gke/worker-service.yaml b/gcloud/gateway/gke/worker-service.yaml new file mode 100644 index 0000000..103fe2a --- /dev/null +++ b/gcloud/gateway/gke/worker-service.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + name: select-ai-worker + namespace: select-ai-gateway +spec: + # This is service discovery only: it allocates no load balancer and no + # virtual Service IP. Cloud DNS for GKE publishes the current Pod IPs. + clusterIP: None + selector: + app: select-ai-worker + ports: + - name: http + port: 8080 + targetPort: 8080 + - name: https + port: 8443 + targetPort: 8443 diff --git a/gcloud/gateway/gke/worker.yaml b/gcloud/gateway/gke/worker.yaml index fc24c5e..3b5392b 100644 --- a/gcloud/gateway/gke/worker.yaml +++ b/gcloud/gateway/gke/worker.yaml @@ -26,6 +26,8 @@ spec: - 0.0.0.0 - --port - "8080" + - --session-ttl-seconds + - "SESSION_TTL_SECONDS" ports: - name: http containerPort: 8080 diff --git a/src/select_ai/agent/a2a/gateway.py b/src/select_ai/agent/a2a/gateway.py index 4b0957e..622e2c7 100644 --- a/src/select_ai/agent/a2a/gateway.py +++ b/src/select_ai/agent/a2a/gateway.py @@ -11,6 +11,7 @@ import asyncio +import requests from a2a.compat.v0_3.conversions import to_compat_agent_card from a2a.helpers import ( new_data_part, @@ -100,7 +101,7 @@ async def _open_session(self, action_context: dict, context_id: str): context_id, session_info, ) - except (ValueError, RuntimeError): + except (requests.RequestException, ValueError, RuntimeError): return [ new_text_part( "Could not connect. Check the DSN, credentials, and team " diff --git a/tests/a2a/test_worker_runtime.py b/tests/a2a/test_worker_runtime.py index ce9f953..8ab1cff 100644 --- a/tests/a2a/test_worker_runtime.py +++ b/tests/a2a/test_worker_runtime.py @@ -10,6 +10,7 @@ import asyncio import pytest +import requests pytest.importorskip("fastapi") @@ -380,3 +381,30 @@ def test_a2ui_action_reads_an_operation_list_or_single_operation(): ) assert action == {"name": "submit_database_connection"} + + +def test_gateway_returns_connection_error_when_worker_rejects_opening(): + executor = GatewayExecutor.__new__(GatewayExecutor) + executor.sessions = {} + + class Client: + @staticmethod + def open_session(_context_id, _session_info): + raise requests.HTTPError("worker rejected the connection") + + executor.worker_client = Client() + + parts = asyncio.run( + executor._open_session( + { + "dsn": "database", + "username": "user", + "password": "password", + "team_name": "TEAM", + }, + "context-1", + ) + ) + + assert parts[0].text.startswith("Could not connect.") + assert executor.sessions == {} From 5e58e186f95d42d266070d9deec57ba6793d08c6 Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Thu, 3 Sep 2026 10:38:29 -0700 Subject: [PATCH 16/31] Refactor A2A gateway routing and session lifecycle - simplify gateway request handling and A2UI bootstrap flow - add database-backed task routing and worker protocol types - clean up expired worker child processes - improve GCP gateway architecture and mTLS deployment - expand A2A gateway and worker tests --- gcloud/gateway/README.md | 78 +- gcloud/gateway/deploy.sh | 13 +- src/select_ai/agent/a2a/a2ui.py | 78 +- src/select_ai/agent/a2a/forms.py | 27 +- src/select_ai/agent/a2a/gateway.py | 450 +++++++++--- src/select_ai/agent/a2a/session_runtime.py | 120 ++++ src/select_ai/agent/a2a/task_store.py | 57 +- src/select_ai/agent/a2a/worker.py | 352 ++++++--- src/select_ai/agent/a2a/worker_client.py | 194 ++++- src/select_ai/agent/a2a/worker_protocol.py | 103 +++ src/select_ai/cli/a2a.py | 2 +- tests/a2a/test_worker_runtime.py | 789 +++++++++++++++++++-- 12 files changed, 1898 insertions(+), 365 deletions(-) create mode 100644 src/select_ai/agent/a2a/session_runtime.py create mode 100644 src/select_ai/agent/a2a/worker_protocol.py diff --git a/gcloud/gateway/README.md b/gcloud/gateway/README.md index a01ee79..a117207 100644 --- a/gcloud/gateway/README.md +++ b/gcloud/gateway/README.md @@ -1,4 +1,4 @@ -# Dynamic gateway deployment +# Dynamic gateway Dynamic gateway mode exposes one public A2A endpoint. Each user dynamically selects an Oracle database connection and Select AI team through the A2UI @@ -6,15 +6,62 @@ connection form. A session remains available for 15 minutes by default. Set a different lifetime in seconds with `--session-ttl-seconds`; for example, `--session-ttl-seconds 1800` keeps sessions for 30 minutes. +## Protocol architecture + +```text +┌──────────────┐ A2A JSON-RPC/HTTP ┌──────────────────┐ ┌────────────────────────────┐ +│ A2A client │────────────────────►│ Gateway instances│──── route lookup/update ──────────► │ Service Registry │ +└──────────────┘ │ Public A2A API │ │ Service discovery │ + │ A2UI bootstrap │ │ Session routes │ + └────────┬─────────┘ │ Task routes │ + │ Internal protobuf │ │ + ▼ │ │ + ┌──────────────────┐ │ │ + │ Worker pool │──── registration / heartbeat------->│ │ + │ worker-0, ... │ │ │ + └────────┬─────────┘ └────────────────────────────┘ + │ one child per database session + ▼ + ┌──────────────────┐ + │ Session runtime │ + │ A2A handler │ + │ Task/context │ + │ stores │ + │ Database session │ + └────────┬─────────┘ + │ SQL / Select AI + ▼ + ┌──────────────────┐ + │ Oracle Database │ + └──────────────────┘ +``` + +The gateway is the only public A2A application. It selects a worker through +the Service Registry, opens a session there, and proxies subsequent A2A calls +using the internal protobuf protocol. The selected worker starts one child +runtime for that session. The child owns the database connection, +`DefaultRequestHandler`, `OracleTaskStore`, and `OracleContextStore`. + +The Service Registry stores only service-discovery and non-secret +session/task-to-worker metadata. Task payloads and context mappings remain in +Oracle. Connection-form tasks are response-only bootstrap tasks: they are +created by the gateway before a database session exists and are not persisted +or routed. + +## GCP deployment + +The protocol architecture above is implemented on GCP as follows: + ```text ┌──────────────────────┐ │ A2A / Gemini client │ └──────────┬───────────┘ │ public A2A v - ┌──────────────────────┐ - │ Cloud Run gateway │ - └──────┬───────┬───────┘ + ┌────────────────────────────┐ + │ Cloud Run gateway │ + │ A2A proxy + form bootstrap │ + └──────┬───────────┬─────────┘ │ │ private VPC: mTLS request to worker hostname │ │ │ │ ┌─────────────────────── GKE ───────────────────────┐ @@ -23,19 +70,16 @@ different lifetime in seconds with `--session-ttl-seconds`; for example, │ │ │ │ │ │ v │ │ │ [StatefulSet worker-0 / worker-1 / ...] │ - │ │ session child process → Oracle Database │ + │ │ session child: A2A handler + Oracle stores │ + │ │ │ │ + │ │ v │ + │ │ Oracle Database │ │ │ │ │ │ [Consul] │ └────────────>│ selects healthy worker; returns worker hostname │ └───────────────────────────────────────────────────┘ ``` -The gateway is the only public A2A application. Consul and workers are a GKE -clustered service: Consul selects a worker for each new dynamic session, and -the chosen worker retains that session's process and Oracle conversation. - -## Deploy the complete stack - Run this from the repository root: ```bash @@ -68,10 +112,14 @@ gcloud/gateway/deploy.sh \ ``` The Cloud Run gateway uses direct VPC egress to reach the internal Consul load -balancer and GKE worker pod addresses. The default one-instance gateway limit -is intentional: gateway A2A task and context/session state is currently in -memory. Workers, rather than the gateway, provide the clustered capacity for -dynamic sessions. +balancer and GKE worker pod addresses. The gateway keeps only the connection +form task transiently, before a database session exists. Connected task and +context state is stored in Oracle on the selected worker. Workers, rather +than the gateway, provide the clustered capacity for dynamic sessions. +The deployment currently keeps one gateway instance as an operational default; +the gateway does not cache forms or connected task/context state. Gateway +scaling does not change session affinity because Consul stores the session and +task routes. `cloudbuild.yaml` is the complete build and deployment workflow. It supplies the generated image and Consul endpoint values to the Cloud Run gateway at diff --git a/gcloud/gateway/deploy.sh b/gcloud/gateway/deploy.sh index 55959c8..08a93ab 100755 --- a/gcloud/gateway/deploy.sh +++ b/gcloud/gateway/deploy.sh @@ -166,10 +166,15 @@ if [[ "$enable_worker_mtls" == "true" ]]; then fi done if [[ "$create_mtls_material" != "true" ]]; then - existing_worker_certificate_sans="$(gcloud secrets versions access latest \ - --secret=select-ai-worker-mtls-cert --project="$project_id" 2>/dev/null | \ - openssl x509 -noout -ext subjectAltName 2>/dev/null || true)" - if [[ "$existing_worker_certificate_sans" != *"DNS:$worker_certificate_dns_name"* ]]; then + # macOS ships LibreSSL, which does not support x509's -ext option. + # Read the portable text representation and perform a literal SAN check. + existing_worker_certificate_text="$( + gcloud secrets versions access latest \ + --secret=select-ai-worker-mtls-cert --project="$project_id" 2>/dev/null | \ + openssl x509 -noout -text 2>/dev/null || true + )" + if ! printf '%s\n' "$existing_worker_certificate_text" | \ + grep -F -- "DNS:$worker_certificate_dns_name" >/dev/null; then create_mtls_material="true" echo "Replacing mTLS material because the worker certificate does not" echo "match the GKE DNS domain." diff --git a/src/select_ai/agent/a2a/a2ui.py b/src/select_ai/agent/a2a/a2ui.py index a25b7f9..331e8c5 100644 --- a/src/select_ai/agent/a2a/a2ui.py +++ b/src/select_ai/agent/a2a/a2ui.py @@ -5,24 +5,34 @@ # https://oss.oracle.com/licenses/upl. # ----------------------------------------------------------------------------- -"""Shared A2UI protocol declarations for Select AI A2A agents.""" +"""Shared A2UI protocol helpers for Select AI A2A agents.""" +from __future__ import annotations + +from collections.abc import Iterator + +from a2a.helpers import new_data_part from a2a.types import AgentExtension -from google.protobuf.json_format import ParseDict +from a2a.types.a2a_pb2 import Message, Part +from google.protobuf.json_format import MessageToDict, ParseDict from google.protobuf.struct_pb2 import Struct -from select_ai.agent.a2a.forms import _CATALOG - -A2UI_EXTENSION_URI = "https://a2ui.org/a2a-extension/a2ui/v0.9" +A2UI_VERSION = "v0.9" +A2UI_EXTENSION_URI = f"https://a2ui.org/a2a-extension/a2ui/{A2UI_VERSION}" A2UI_MIME_TYPE = "application/json+a2ui" +A2UI_CATALOG_ID = ( + "https://www.gstatic.com/vertexaisearch/a2ui/" + f"{A2UI_VERSION.replace('.', '_')}/" + "gemini_enterprise_composite_catalog.json" +) def a2ui_extension() -> AgentExtension: - """Return the A2UI v0.9 capability used by Gemini Enterprise.""" + """Return the supported A2UI capability used by Gemini Enterprise.""" params = ParseDict( { "acceptsInlineCatalogs": True, - "supportedCatalogIds": [_CATALOG], + "supportedCatalogIds": [A2UI_CATALOG_ID], }, Struct(), ) @@ -31,3 +41,57 @@ def a2ui_extension() -> AgentExtension: description="Provides agent driven UI using the A2UI JSON format.", params=params, ) + + +def a2ui_part(operation: dict) -> Part: + """Encode one A2UI operation in an A2A data part.""" + part = new_data_part(operation) + ParseDict({"mimeType": A2UI_MIME_TYPE}, part.metadata) + return part + + +def a2ui_operations(message: Message) -> Iterator[dict]: + """Yield operations from data parts marked with the A2UI MIME type.""" + for part in message.parts: + if not _is_a2ui_part(part): + continue + data = MessageToDict(part.data) + operations = data if isinstance(data, list) else [data] + yield from ( + operation + for operation in operations + if isinstance(operation, dict) + ) + + +def find_action(message: Message, name: str) -> dict | None: + """Return a named A2UI action, including Gemini's unmarked input form.""" + for part in message.parts: + if part.WhichOneof("content") != "data": + continue + mime_type = MessageToDict(part.metadata).get("mimeType") + # Gemini Enterprise does not currently echo the A2UI MIME metadata on + # a submitted form action. Accept that legacy input shape, while + # still rejecting data parts explicitly marked as another format. + if mime_type not in (None, A2UI_MIME_TYPE): + continue + data = MessageToDict(part.data) + operations = data if isinstance(data, list) else [data] + for operation in operations: + if ( + not isinstance(operation, dict) + or operation.get("version") != A2UI_VERSION + ): + continue + action = operation.get("action") + if isinstance(action, dict) and action.get("name") == name: + return action + return None + + +def _is_a2ui_part(part: Part) -> bool: + """Identify an A2UI data part by its standard MIME type.""" + return ( + part.WhichOneof("content") == "data" + and MessageToDict(part.metadata).get("mimeType") == A2UI_MIME_TYPE + ) diff --git a/src/select_ai/agent/a2a/forms.py b/src/select_ai/agent/a2a/forms.py index 6823464..9d82bff 100644 --- a/src/select_ai/agent/a2a/forms.py +++ b/src/select_ai/agent/a2a/forms.py @@ -7,27 +7,30 @@ """A2UI connection form emitted by the public gateway.""" +from uuid import uuid4 -_CATALOG = ( - "https://www.gstatic.com/vertexaisearch/a2ui/v0_9/" - "gemini_enterprise_composite_catalog.json" -) +from select_ai.agent.a2a.a2ui import A2UI_CATALOG_ID, A2UI_VERSION -def connection_form() -> list[dict]: +def connection_form(surface_id: str | None = None) -> list[dict]: """Return the non-persistent database connection form.""" + # A2UI surface IDs must be globally unique for the renderer's lifetime. + # Gemini retains surfaces for an A2A conversation after the connection + # form is submitted, so reusing a fixed ID prevents a reconnect form from + # being created in that same conversation. + surface_id = surface_id or f"db-connect-{uuid4().hex}" return [ { - "version": "v0.9", + "version": A2UI_VERSION, "createSurface": { - "surfaceId": "db-connect", - "catalogId": _CATALOG, + "surfaceId": surface_id, + "catalogId": A2UI_CATALOG_ID, }, }, { - "version": "v0.9", + "version": A2UI_VERSION, "updateComponents": { - "surfaceId": "db-connect", + "surfaceId": surface_id, "components": [ {"id": "root", "component": "Card", "child": "column"}, { @@ -102,9 +105,9 @@ def connection_form() -> list[dict]: }, }, { - "version": "v0.9", + "version": A2UI_VERSION, "updateDataModel": { - "surfaceId": "db-connect", + "surfaceId": surface_id, "path": "/", "value": { "dsn": "", diff --git a/src/select_ai/agent/a2a/gateway.py b/src/select_ai/agent/a2a/gateway.py index 622e2c7..1fc9834 100644 --- a/src/select_ai/agent/a2a/gateway.py +++ b/src/select_ai/agent/a2a/gateway.py @@ -10,25 +10,43 @@ from __future__ import annotations import asyncio +from uuid import uuid4 import requests from a2a.compat.v0_3.conversions import to_compat_agent_card from a2a.helpers import ( - new_data_part, - new_task_from_user_message, + new_artifact, new_text_part, ) -from a2a.server.agent_execution import AgentExecutor -from a2a.server.request_handlers import DefaultRequestHandler +from a2a.server.context import ServerCallContext +from a2a.server.request_handlers import RequestHandler from a2a.server.routes import create_jsonrpc_routes -from a2a.server.tasks import InMemoryTaskStore, TaskUpdater from a2a.types import ( AgentCapabilities, AgentCard, AgentInterface, AgentSkill, ) -from google.protobuf.json_format import MessageToDict, ParseDict +from a2a.types.a2a_pb2 import ( + AgentCard as AgentCardMessage, +) +from a2a.types.a2a_pb2 import ( + CancelTaskRequest, + GetExtendedAgentCardRequest, + GetTaskRequest, + ListTasksRequest, + ListTasksResponse, + Message, + SendMessageRequest, + Task, + TaskState, + TaskStatus, +) +from a2a.utils.errors import ( + InvalidParamsError, + TaskNotFoundError, + UnsupportedOperationError, +) from starlette.applications import Starlette from starlette.responses import JSONResponse from starlette.routing import Route @@ -37,63 +55,198 @@ A2UI_EXTENSION_URI, A2UI_MIME_TYPE, a2ui_extension, + a2ui_part, + find_action, ) from select_ai.agent.a2a.forms import connection_form from select_ai.agent.a2a.models import GatewaySettings, SessionInfo -from select_ai.agent.a2a.results import add_team_result from select_ai.agent.a2a.worker_client import ReconnectRequired, WorkerClient from select_ai.version import __version__ +_CONNECTION_ACTION_NAME = "submit_database_connection" +_CONNECTION_SUCCESS_MESSAGE = "Connected. Ask a database question." +_CONNECTION_FAILURE_MESSAGE = ( + "Could not connect. Check the DSN, credentials, and team name." +) +_BOOTSTRAP_TASK_ID_PREFIX = "gateway-bootstrap-" + + +async def _unsupported_operation(*_args, **_kwargs): + """Reject an A2A operation not advertised by the gateway.""" + raise UnsupportedOperationError + + +class _UnsupportedStream: + """Async iterator that reports unsupported streaming when consumed.""" + + def __aiter__(self): + return self -class GatewayExecutor(AgentExecutor): - """Route each A2A context to one in-memory worker session.""" + async def __anext__(self): + raise UnsupportedOperationError - def __init__(self, worker_client: WorkerClient): + +def _unsupported_stream(*_args, **_kwargs): + """Return an async iterator that rejects an unsupported A2A stream.""" + return _UnsupportedStream() + + +class GatewayRequestHandler(RequestHandler): + """Proxy A2A requests to the Oracle-backed handler in a worker child.""" + + def __init__(self, worker_client: WorkerClient, agent_card: AgentCard): self.worker_client = worker_client - self.sessions: dict[str, str] = {} + self.agent_card = agent_card + + # RequestHandler requires every operation even when the Agent Card does + # not advertise streaming or push notifications. + on_message_send_stream = _unsupported_stream + on_create_task_push_notification_config = _unsupported_operation + on_get_task_push_notification_config = _unsupported_operation + on_list_task_push_notification_configs = _unsupported_operation + on_delete_task_push_notification_config = _unsupported_operation + on_subscribe_to_task = _unsupported_stream + + async def on_message_send( + self, + params: SendMessageRequest, + _context: ServerCallContext, + ) -> Task | Message: + """Handle connection bootstrap or forward the request to a worker.""" + message = params.message + if action := find_action(message, _CONNECTION_ACTION_NAME): + return await self._handle_connection_action(message, action) + + response = await self._recover_task_context(message) + if response is not None: + return response + + context_id = self._ensure_context_id(message) + had_task_id = bool(message.task_id) - async def execute(self, context, event_queue): - task = context.current_task or new_task_from_user_message( - context.message + if not await asyncio.to_thread( + self.worker_client.session_exists, + context_id, + ): + return self._build_connection_form_task( + task_id=message.task_id or None, + context_id=context_id, + history=None if had_task_id else [message], + ) + + return await self._forward_message(context_id, params) + + async def _handle_connection_action( + self, + message: Message, + action: dict, + ) -> Task: + """Open a database session from the submitted A2UI form.""" + if not message.context_id: + raise InvalidParamsError( + "A database connection action requires contextId." + ) + + task = _new_bootstrap_task( + task_id=message.task_id or None, + context_id=message.context_id, ) - if context.current_task is None: - await event_queue.enqueue_event(task) - context_id = task.context_id or task.id - updater = TaskUpdater( - event_queue=event_queue, - task_id=task.id, - context_id=task.context_id, + session_id = await self._open_session( + action.get("context") or {}, + message.context_id, ) - await updater.start_work() - action = self._a2ui_action(context) - if action and action.get("name") == "submit_database_connection": - parts = await self._open_session( - action.get("context", {}), - context_id, - ) - artifact_name = "database-session" - extensions = None - elif context_id not in self.sessions: - parts = [_a2ui_part(message) for message in connection_form()] - artifact_name = "database-connection-form" - extensions = [A2UI_EXTENSION_URI] - else: - result = await self._send_prompt( - context_id, - context.get_user_input(), - ) - await add_team_result(updater, result) - await updater.complete() - return - await updater.add_artifact( - parts=parts, - name=artifact_name, - last_chunk=True, - extensions=extensions, + text = ( + _CONNECTION_SUCCESS_MESSAGE + if session_id is not None + else _CONNECTION_FAILURE_MESSAGE ) - await updater.complete() + return self._complete_task( + task, + [new_text_part(text)], + "database-session", + ) + + async def _recover_task_context(self, message: Message) -> Task | None: + """Recover a context when a client sends only a previous task ID.""" + if message.context_id or not message.task_id: + return None + + try: + existing_task = await asyncio.to_thread( + self.worker_client.get_task, + GetTaskRequest(id=message.task_id), + ) + except ReconnectRequired as error: + if error.session_id is None: + raise + return self._build_connection_form_task( + task_id=message.task_id or None, + context_id=error.session_id, + ) + if existing_task is None: + # Gemini Enterprise may resume a conversation with only its + # previous taskId. Task routing metadata is intentionally + # ephemeral, so it can be absent after a deployment or Consul + # restart even though the Gemini conversation still exists. + return self._build_connection_form_task( + task_id=message.task_id, + context_id=str(uuid4()), + ) + message.context_id = existing_task.context_id + return None - async def _open_session(self, action_context: dict, context_id: str): + @staticmethod + def _ensure_context_id(message: Message) -> str: + """Assign a context ID to a new message when one was not provided.""" + if message.context_id: + return message.context_id + context_id = str(uuid4()) + message.context_id = context_id + return context_id + + async def _forward_message( + self, + context_id: str, + params: SendMessageRequest, + ) -> Task | Message: + """Forward a message and turn worker loss into a reconnect task.""" + try: + try: + result = await asyncio.to_thread( + self.worker_client.send_message, + context_id, + params, + ) + except TaskNotFoundError: + # Gemini continues the gateway's transient connection-form + # task after the database session opens. That task was never + # persisted in Oracle, so let the worker create its first + # durable task while retaining the connected context. + if not params.message.task_id.startswith( + _BOOTSTRAP_TASK_ID_PREFIX + ): + raise + params.message.ClearField("task_id") + result = await asyncio.to_thread( + self.worker_client.send_message, + context_id, + params, + ) + except ReconnectRequired: + return self._build_connection_form_task( + task_id=params.message.task_id or None, + context_id=context_id, + history=(None if params.message.task_id else [params.message]), + ) + if isinstance(result, (Task, Message)): + return result + raise RuntimeError("The worker returned no A2A response.") + + async def _open_session( + self, + action_context: dict, + context_id: str, + ) -> str | None: try: session_info = SessionInfo.from_a2ui_event(action_context) session_id = await asyncio.to_thread( @@ -101,71 +254,156 @@ async def _open_session(self, action_context: dict, context_id: str): context_id, session_info, ) + return session_id except (requests.RequestException, ValueError, RuntimeError): - return [ - new_text_part( - "Could not connect. Check the DSN, credentials, and team " - "name." - ) - ] - self.sessions[context_id] = session_id - return [new_text_part("Connected. Ask a database question.")] + return None - async def _send_prompt(self, context_id: str, prompt: str) -> str | None: + async def on_get_task( + self, + params: GetTaskRequest, + _context: ServerCallContext, + ) -> Task | None: try: - result = await asyncio.to_thread( - self.worker_client.send_prompt, - self.sessions[context_id], - prompt, + task = await asyncio.to_thread( + self.worker_client.get_task, + params, ) - except ReconnectRequired: - self.sessions.pop(context_id, None) - return "Your database session ended. Please reconnect." - return result + except ReconnectRequired as error: + if error.session_id is None: + raise + return self._build_connection_form_task( + task_id=params.id, + context_id=error.session_id, + ) + if task is None: + return self._build_connection_form_task( + task_id=params.id, + context_id=str(uuid4()), + ) + return task - async def cancel(self, context, event_queue): - """Close the live worker session when the A2A task is cancelled.""" - task = context.current_task + async def on_list_tasks( + self, + params: ListTasksRequest, + _context: ServerCallContext, + ) -> ListTasksResponse: + if not params.context_id: + raise InvalidParamsError( + "ListTasks requires contextId for this gateway." + ) + try: + return await asyncio.to_thread( + self.worker_client.list_tasks, + params.context_id, + params, + ) + except ReconnectRequired as error: + raise self._session_expired_error(params.context_id) from error + + async def on_cancel_task( + self, + params: CancelTaskRequest, + _context: ServerCallContext, + ) -> Task | None: + try: + task = await asyncio.to_thread( + self.worker_client.cancel_task, + params.id, + ) + except ReconnectRequired as error: + if error.session_id is None: + raise + return self._build_connection_form_task( + task_id=params.id, + context_id=error.session_id, + ) if task is None: - return - context_id = task.context_id or task.id - session_id = self.sessions.pop(context_id, None) - if session_id: - await asyncio.to_thread( - self.worker_client.close_session, - session_id, - ) - updater = TaskUpdater( - event_queue=event_queue, - task_id=task.id, - context_id=task.context_id, + return self._build_connection_form_task( + task_id=params.id, + context_id=str(uuid4()), + ) + return task + + async def on_get_extended_agent_card( + self, + _params: GetExtendedAgentCardRequest, + _context: ServerCallContext, + ) -> AgentCardMessage: + return self.agent_card + + @staticmethod + def _session_expired_error(context_id: str) -> InvalidParamsError: + """Build the reconnect error returned by the task-list operation.""" + return InvalidParamsError( + "Database session expired. Reconnect using contextId.", + data={ + "reason": "SESSION_EXPIRED", + "contextId": context_id, + "connectionForm": connection_form(), + }, ) - await updater.cancel() @staticmethod - def _a2ui_action(context) -> dict | None: - for part in context.message.parts: - if part.WhichOneof("content") != "data": - continue - data = MessageToDict(part.data) - messages = data if isinstance(data, list) else [data] - for message in messages: - if ( - not isinstance(message, dict) - or message.get("version") != "v0.9" - ): - continue - action = message.get("action") - if isinstance(action, dict): - return action - return None + def _add_artifact( + task: Task, + parts: list, + name: str, + extensions: list[str] | None = None, + ) -> None: + artifact = new_artifact(parts, name) + if extensions: + artifact.extensions.extend(extensions) + task.artifacts.add().CopyFrom(artifact) + + def _complete_task( + self, + task: Task, + parts: list, + name: str, + extensions: list[str] | None = None, + ) -> Task: + self._add_artifact(task, parts, name, extensions) + task.status.state = TaskState.TASK_STATE_COMPLETED + task.status.timestamp.GetCurrentTime() + return task + + def _build_connection_form_task( + self, + *, + task_id: str | None, + context_id: str, + history: list[Message] | None = None, + ) -> Task: + """Build a completed, transient task containing the connection form.""" + task = _new_bootstrap_task( + task_id=task_id, + context_id=context_id, + history=history, + ) + return self._complete_task( + task, + [ + a2ui_part(item) + for item in connection_form(f"db-connect-{task.id}") + ], + "database-connection-form", + [A2UI_EXTENSION_URI], + ) -def _a2ui_part(message: dict): - """Encode one A2UI operation in the format used by Gemini Enterprise.""" - part = new_data_part(message) - ParseDict({"mimeType": A2UI_MIME_TYPE}, part.metadata) - return part +def _new_bootstrap_task( + *, + task_id: str | None, + context_id: str, + history: list[Message] | None = None, +) -> Task: + """Build a transient response task for the connection bootstrap.""" + return Task( + id=task_id or f"{_BOOTSTRAP_TASK_ID_PREFIX}{uuid4()}", + context_id=context_id, + status=TaskStatus(state=TaskState.TASK_STATE_SUBMITTED), + history=history or [], + ) def create_gateway_app(settings: GatewaySettings) -> Starlette: @@ -206,11 +444,7 @@ def create_gateway_app(settings: GatewaySettings) -> Starlette: ) ], ) - handler = DefaultRequestHandler( - agent_executor=GatewayExecutor(WorkerClient(settings)), - task_store=InMemoryTaskStore(), - agent_card=card, - ) + handler = GatewayRequestHandler(WorkerClient(settings), card) compat_card = to_compat_agent_card(card).model_dump( by_alias=True, exclude_none=True, diff --git a/src/select_ai/agent/a2a/session_runtime.py b/src/select_ai/agent/a2a/session_runtime.py new file mode 100644 index 0000000..f516557 --- /dev/null +++ b/src/select_ai/agent/a2a/session_runtime.py @@ -0,0 +1,120 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# https://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +"""A database-bound A2A runtime for one temporary worker session.""" + +from __future__ import annotations + +from a2a.auth.user import User +from a2a.server.context import ServerCallContext +from a2a.server.request_handlers import DefaultRequestHandler +from a2a.types.a2a_pb2 import ( + CancelTaskRequest, + GetTaskRequest, + ListTasksRequest, + SendMessageRequest, +) +from a2a.utils.errors import TaskNotFoundError + +from select_ai.agent import AsyncTeam +from select_ai.agent.a2a.context_store import OracleContextStore +from select_ai.agent.a2a.server import DatabaseTeamExecutor, _build_agent_card +from select_ai.agent.a2a.task_store import OracleTaskStore +from select_ai.agent.a2a.worker_protocol import ( + A2AMethod, + ResultKind, + WorkerResult, + encode_result, + parse_request, +) + +_METHOD_HANDLERS = { + A2AMethod.SEND_MESSAGE: (SendMessageRequest, "on_message_send"), + A2AMethod.GET_TASK: (GetTaskRequest, "on_get_task"), + A2AMethod.LIST_TASKS: (ListTasksRequest, "on_list_tasks"), + A2AMethod.CANCEL_TASK: (CancelTaskRequest, "on_cancel_task"), +} + + +class SessionUser(User): + """Internal A2A user used to scope one worker session's database rows.""" + + def __init__(self, session_id: str) -> None: + self.session_id = session_id + + @property + def is_authenticated(self) -> bool: + return True + + @property + def user_name(self) -> str: + return self.session_id + + +class SessionRuntime: + """Own the A2A handler and Oracle stores for one connected database.""" + + def __init__(self, session_id: str, team_name: str) -> None: + self.session_id = session_id + self.team_name = team_name + self.task_store = OracleTaskStore() + self.context_store = OracleContextStore() + self.handler: DefaultRequestHandler | None = None + + async def initialize(self) -> None: + """Initialize Oracle-backed stores after the worker connects.""" + # Validate the user-supplied team before the worker reports this + # database session as ready. Without this check, invalid team names + # appear to connect successfully and fail only on the first prompt. + await AsyncTeam.fetch(self.team_name) + await self.task_store.initialize() + await self.context_store.initialize() + self.handler = DefaultRequestHandler( + agent_executor=DatabaseTeamExecutor( + self.team_name, + self.context_store, + ), + task_store=self.task_store, + agent_card=_build_agent_card( + self.team_name, + f"http://worker/sessions/{self.session_id}", + None, + ), + ) + + async def handle( + self, + method: A2AMethod | str, + payload: bytes, + ) -> WorkerResult: + """Handle one internal A2A operation using protobuf wire bytes.""" + if self.handler is None: + raise RuntimeError("A2A session runtime is not initialized.") + + operation = A2AMethod(method) + context = ServerCallContext(user=SessionUser(self.session_id)) + if operation == A2AMethod.DELETE_TASK: + request = parse_request(GetTaskRequest, payload) + await self.task_store.delete(request.id, context) + return encode_result(None) + + try: + request_type, handler_name = _METHOD_HANDLERS[operation] + except KeyError as error: + raise ValueError( + f"Unsupported worker A2A method: {operation.value}" + ) from error + + request = parse_request(request_type, payload) + try: + result = await getattr(self.handler, handler_name)( + request, + context, + ) + except TaskNotFoundError: + return WorkerResult(ResultKind.TASK_NOT_FOUND) + return encode_result(result) diff --git a/src/select_ai/agent/a2a/task_store.py b/src/select_ai/agent/a2a/task_store.py index 3962e8e..418c145 100644 --- a/src/select_ai/agent/a2a/task_store.py +++ b/src/select_ai/agent/a2a/task_store.py @@ -126,12 +126,24 @@ async def list( SELECT task_json FROM SELECT_AI_A2A_TASKS WHERE owner = :owner + AND ( + :context_id IS NULL OR context_id = :context_id + ) + AND ( + :status IS NULL OR + JSON_VALUE(task_json, '$.status.state') = :status + ) + AND ( + :status_timestamp_after IS NULL OR + JSON_VALUE(task_json, '$.status.timestamp') + >= :status_timestamp_after + ) ORDER BY updated_at DESC, task_id DESC """, owner=self._owner(context), + **self._list_query_parameters(params), ) tasks = [await self._task_from_json(row[0]) for row in rows] - tasks = self._filter_tasks(tasks, params) total_size = len(tasks) start_index = self._page_start_index(tasks, params.page_token) page_size = params.page_size or DEFAULT_LIST_TASKS_PAGE_SIZE @@ -162,31 +174,28 @@ async def delete(self, task_id: str, context: ServerCallContext) -> None: ) @staticmethod - def _filter_tasks( - tasks: list[Task], + def _list_query_parameters( params: a2a_pb2.ListTasksRequest, - ) -> list[Task]: - if params.context_id: - tasks = [ - task for task in tasks if task.context_id == params.context_id - ] + ) -> dict[str, str | None]: + """Convert protobuf list filters to Oracle query parameters.""" + status = None if params.status: - tasks = [ - task - for task in tasks - if task.HasField("status") - and task.status.state == params.status - ] - if params.HasField("status_timestamp_after"): - timestamp_after = params.status_timestamp_after.ToJsonString() - tasks = [ - task - for task in tasks - if task.HasField("status") - and task.status.HasField("timestamp") - and task.status.timestamp.ToJsonString() >= timestamp_after - ] - return tasks + try: + status = a2a_pb2.TaskState.Name(params.status) + except ValueError: + # An unknown enum value cannot match a stored protobuf JSON + # enum name, which preserves the old empty-result behavior. + status = "__UNKNOWN_TASK_STATE__" + timestamp_after = ( + params.status_timestamp_after.ToJsonString() + if params.HasField("status_timestamp_after") + else None + ) + return { + "context_id": params.context_id or None, + "status": status, + "status_timestamp_after": timestamp_after, + } def _owner(self, context: ServerCallContext) -> str: return self.owner_resolver(context) or "anonymous" diff --git a/src/select_ai/agent/a2a/worker.py b/src/select_ai/agent/a2a/worker.py index d6e7b04..5043955 100644 --- a/src/select_ai/agent/a2a/worker.py +++ b/src/select_ai/agent/a2a/worker.py @@ -21,11 +21,22 @@ from multiprocessing.connection import Connection import httpx -from fastapi import FastAPI, HTTPException, status -from fastapi.responses import PlainTextResponse +from fastapi import FastAPI, HTTPException, Request, status from pydantic import BaseModel, Field, SecretStr +from starlette.responses import Response + +from select_ai.agent.a2a.session_runtime import SessionRuntime +from select_ai.agent.a2a.worker_protocol import ( + PROTOBUF_CONTENT_TYPE, + WORKER_A2A_METHOD_HEADER, + WORKER_RESULT_KIND_HEADER, + PipeMessageType, + ResultKind, + WorkerResult, +) LOGGER = logging.getLogger(__name__) +_SESSION_REAPER_INTERVAL_SECONDS = 1 class OpenSessionRequest(BaseModel): @@ -38,12 +49,6 @@ class OpenSessionRequest(BaseModel): team_name: str = Field(min_length=1, max_length=128) -class PromptRequest(BaseModel): - """One user prompt for a previously opened session.""" - - prompt: str = Field(min_length=1, max_length=32_000) - - @dataclass class ChildSession: """In-memory ownership record for one database-session process.""" @@ -65,7 +70,6 @@ def __init__( self.session_ttl_seconds = session_ttl_seconds self.session_start_timeout_seconds = session_start_timeout_seconds self.sessions: dict[str, ChildSession] = {} - self.lock = asyncio.Lock() async def open(self, request: OpenSessionRequest) -> None: """Start a child runtime and wait until its database pool is ready.""" @@ -98,23 +102,24 @@ async def open(self, request: OpenSessionRequest) -> None: except Exception: await self._terminate(session) raise - async with self.lock: - previous = self.sessions.pop(request.session_id, None) - if previous: - await self._terminate(previous) - self.sessions[request.session_id] = session + previous = self.sessions.pop(request.session_id, None) + self.sessions[request.session_id] = session + if previous: + await self._terminate(previous) async def get(self, session_id: str) -> ChildSession: """Return a live session, closing it if its expiry has elapsed.""" - async with self.lock: - session = self.sessions.get(session_id) - if session and ( - session.expires_at <= time.monotonic() - or not session.process.is_alive() - ): - self.sessions.pop(session_id, None) - await self._terminate(session) - session = None + expired_session = None + session = self.sessions.get(session_id) + if session and ( + session.expires_at <= time.monotonic() + or not session.process.is_alive() + ): + self.sessions.pop(session_id, None) + expired_session = session + session = None + if expired_session: + await self._terminate(expired_session) if session is None: raise HTTPException( status_code=404, @@ -122,32 +127,57 @@ async def get(self, session_id: str) -> ChildSession: ) return session - async def send_prompt(self, session_id: str, prompt: str) -> str | None: - """Run one prompt in the process that owns this database session.""" + async def dispatch( + self, + session_id: str, + method: str, + payload: bytes, + ) -> WorkerResult: + """Run one A2A operation in the process owning this session.""" session = await self.get(session_id) + response = None + failure = None async with session.lock: - if not session.process.is_alive(): - await self._discard(session_id, session) - raise HTTPException( - status_code=404, - detail="Database session expired; reconnect required.", - ) - try: - await asyncio.to_thread( - session.connection.send, - {"type": "run", "prompt": prompt}, - ) - response = await self._receive(session, timeout_seconds=120) - except (EOFError, OSError, TimeoutError) as error: - await self._discard(session_id, session) - raise HTTPException( - status_code=502, - detail=( - "Database session is unavailable; reconnect required." - ), - ) from error - if response.get("type") == "result": - return response.get("result") + if session.process.is_alive(): + try: + await asyncio.to_thread( + session.connection.send, + { + "type": PipeMessageType.A2A.value, + "method": method, + "payload": payload, + }, + ) + response = await self._receive( + session, + timeout_seconds=120, + ) + except (EOFError, OSError, TimeoutError) as error: + failure = error + if failure is not None: + await self._discard(session_id, session) + raise HTTPException( + status_code=502, + detail=( + "Database session is unavailable; reconnect required." + ), + ) from failure + if response is None: + await self._discard(session_id, session) + raise HTTPException( + status_code=404, + detail="Database session expired; reconnect required.", + ) + if response.get("type") == PipeMessageType.RESULT.value: + return response.get( + "result", + WorkerResult(ResultKind.NONE), + ) + if response.get("type") == PipeMessageType.A2A_ERROR.value: + raise HTTPException( + status_code=400, + detail=response.get("detail", "A2A request failed."), + ) LOGGER.error("Select AI session process reported a command failure.") raise HTTPException( status_code=502, @@ -156,8 +186,7 @@ async def send_prompt(self, session_id: str, prompt: str) -> str | None: async def close(self, session_id: str) -> None: """Terminate a session on an explicit gateway DELETE request.""" - async with self.lock: - session = self.sessions.pop(session_id, None) + session = self.sessions.pop(session_id, None) if session is None: raise HTTPException( status_code=404, @@ -165,14 +194,38 @@ async def close(self, session_id: str) -> None: ) await self._terminate(session) + async def reap_expired(self) -> None: + """Continuously terminate expired or dead child sessions.""" + while True: + await asyncio.sleep(_SESSION_REAPER_INTERVAL_SECONDS) + await self._reap_expired_sessions() + async def close_all(self) -> None: """Terminate all child sessions during worker shutdown.""" - async with self.lock: - sessions = list(self.sessions.values()) - self.sessions.clear() + sessions = list(self.sessions.values()) + self.sessions.clear() for session in sessions: await self._terminate(session) + async def _reap_expired_sessions(self) -> None: + now = time.monotonic() + expired = [ + (session_id, session) + for session_id, session in self.sessions.items() + if session.expires_at <= now or not session.process.is_alive() + ] + for session_id, _session in expired: + self.sessions.pop(session_id, None) + + for session_id, session in expired: + try: + await self._terminate(session) + except Exception: + LOGGER.exception( + "Failed to terminate expired session %s.", + session_id, + ) + async def _wait_ready( self, session: ChildSession, @@ -188,7 +241,7 @@ async def _wait_ready( status_code=504, detail="Database session start timed out.", ) from error - if response.get("type") == "ready": + if response.get("type") == PipeMessageType.READY.value: return detail = response.get("detail", "Database login failed.") for secret in ( @@ -214,26 +267,30 @@ async def _receive( return await asyncio.to_thread(session.connection.recv) async def _discard(self, session_id: str, session: ChildSession) -> None: - async with self.lock: - if self.sessions.get(session_id) is session: - self.sessions.pop(session_id, None) + if self.sessions.get(session_id) is session: + self.sessions.pop(session_id, None) await self._terminate(session) @staticmethod async def _terminate(session: ChildSession) -> None: - def stop() -> None: + async with session.lock: try: - if session.process.is_alive(): - with contextlib.suppress(OSError): - session.connection.send({"type": "close"}) - session.process.join(timeout=5) - if session.process.is_alive(): - session.process.terminate() - session.process.join(timeout=5) + with contextlib.suppress(OSError): + await asyncio.to_thread( + session.connection.send, + {"type": PipeMessageType.CLOSE.value}, + ) + await asyncio.to_thread(session.process.join, timeout=5) + for stop in ( + session.process.terminate, + session.process.kill, + ): + if not session.process.is_alive(): + break + await asyncio.to_thread(stop) + await asyncio.to_thread(session.process.join, timeout=5) finally: - session.connection.close() - - await asyncio.to_thread(stop) + await asyncio.to_thread(session.connection.close) def _session_process_main( @@ -262,10 +319,10 @@ async def _run_session_process( session_id: str, team_name: str, ) -> None: - """Open one async connection and execute raw ``AsyncTeam`` prompts.""" + """Open one async connection and execute A2A operations.""" import select_ai - from select_ai.agent import AsyncTeam + runtime: SessionRuntime | None = None ready = False try: await select_ai.async_connect( @@ -275,47 +332,98 @@ async def _run_session_process( ) if not await select_ai.async_is_connected(): raise RuntimeError("Database login failed.") - connection.send({"type": "ready"}) + runtime = SessionRuntime(session_id, team_name) + await runtime.initialize() + connection.send({"type": PipeMessageType.READY.value}) ready = True - conversation_id = None - while True: - try: - command = await asyncio.to_thread(connection.recv) - except EOFError: - return - if command.get("type") == "close": - return - if command.get("type") != "run": - connection.send( - {"type": "error", "detail": "Invalid command."} - ) - continue - try: - if conversation_id is None: - conversation = select_ai.AsyncConversation( - attributes=select_ai.ConversationAttributes( - title=f"A2A {team_name}", - description=f"Temporary session {session_id}", - ) - ) - conversation_id = await conversation.create() - result = await AsyncTeam(team_name=team_name).run( - prompt=command["prompt"], - params={"conversation_id": conversation_id}, - ) - connection.send({"type": "result", "result": result}) - except Exception: - # Keep session-process failures private from gateway callers. - LOGGER.error("Select AI session command failed") - connection.send({"type": "error"}) + await _serve_session_commands(connection, runtime) except Exception as error: - LOGGER.error("Select AI session process startup failed") - if not ready: - with contextlib.suppress(OSError): - connection.send({"type": "error", "detail": str(error)}) + _report_session_process_error(connection, error, not ready) finally: + await _close_session_process(runtime, select_ai) + + +async def _serve_session_commands( + connection: Connection, + runtime: SessionRuntime, +) -> None: + """Serve commands for one initialized database session.""" + while True: + command = await _receive_session_command(connection) + if ( + command is None + or command.get("type") == PipeMessageType.CLOSE.value + ): + return + if command.get("type") != PipeMessageType.A2A.value: + connection.send( + { + "type": PipeMessageType.ERROR.value, + "detail": "Invalid command.", + } + ) + continue + await _handle_a2a_command(connection, runtime, command) + + +async def _receive_session_command(connection: Connection) -> dict | None: + """Read one command without blocking the event loop.""" + try: + return await asyncio.to_thread(connection.recv) + except EOFError: + return None + + +async def _handle_a2a_command( + connection: Connection, + runtime: SessionRuntime, + command: dict, +) -> None: + """Execute one internal A2A command and send its result.""" + try: + result = await runtime.handle( + command["method"], + command.get("payload", b""), + ) + except Exception as error: + LOGGER.exception("Select AI session A2A command failed") + connection.send( + {"type": PipeMessageType.A2A_ERROR.value, "detail": str(error)} + ) + return + connection.send({"type": PipeMessageType.RESULT.value, "result": result}) + + +def _report_session_process_error( + connection: Connection, + error: Exception, + during_startup: bool, +) -> None: + """Report startup failures without sending errors after readiness.""" + message = ( + "Select AI session process startup failed" + if during_startup + else "Select AI session process failed" + ) + LOGGER.error(message) + if during_startup: + with contextlib.suppress(OSError): + connection.send( + {"type": PipeMessageType.ERROR.value, "detail": str(error)} + ) + + +async def _close_session_process( + runtime: SessionRuntime | None, + select_ai, +) -> None: + """Close the A2A handler and database connection owned by the child.""" + handler = getattr(runtime, "handler", None) + if handler is not None: with contextlib.suppress(Exception): - await select_ai.async_disconnect() + await handler.aclose() + with contextlib.suppress(Exception): + await select_ai.async_disconnect() async def _register_with_consul( @@ -393,9 +501,13 @@ async def lifespan(_app): worker_endpoint, ) heartbeat = asyncio.create_task(_heartbeat(consul_url, worker_id)) + reaper = asyncio.create_task(worker.reap_expired()) try: yield finally: + reaper.cancel() + with contextlib.suppress(asyncio.CancelledError): + await reaper heartbeat.cancel() with contextlib.suppress(asyncio.CancelledError): await heartbeat @@ -418,13 +530,29 @@ async def open_session(request: OpenSessionRequest) -> dict[str, str]: await worker.open(request) return {"status": "opened"} - @app.post("/sessions/{session_id}/messages") - async def send_message( + @app.post("/sessions/{session_id}/a2a") + async def handle_a2a( session_id: str, - request: PromptRequest, - ) -> PlainTextResponse: - result = await worker.send_prompt(session_id, request.prompt) - return PlainTextResponse(result or "") + request: Request, + ) -> Response: + method = request.headers.get(WORKER_A2A_METHOD_HEADER) + if not method: + raise HTTPException( + status_code=400, + detail=f"Missing {WORKER_A2A_METHOD_HEADER} header.", + ) + result = await worker.dispatch( + session_id, + method, + await request.body(), + ) + return Response( + content=result.payload, + media_type=PROTOBUF_CONTENT_TYPE, + headers={ + WORKER_RESULT_KIND_HEADER: result.kind.value, + }, + ) @app.delete( "/sessions/{session_id}", diff --git a/src/select_ai/agent/a2a/worker_client.py b/src/select_ai/agent/a2a/worker_client.py index b149453..3c38b82 100644 --- a/src/select_ai/agent/a2a/worker_client.py +++ b/src/select_ai/agent/a2a/worker_client.py @@ -11,17 +11,46 @@ import base64 import json +import logging import time from threading import Lock +from urllib.parse import quote import requests +from a2a.types.a2a_pb2 import ( + CancelTaskRequest, + GetTaskRequest, + ListTasksRequest, + ListTasksResponse, + Message, + SendMessageRequest, + Task, +) from select_ai.agent.a2a import GatewaySettings, SessionInfo, SessionRoute +from select_ai.agent.a2a.worker_protocol import ( + PROTOBUF_CONTENT_TYPE, + WORKER_A2A_METHOD_HEADER, + WORKER_RESULT_KIND_HEADER, + A2AMethod, + ResultKind, + WorkerResult, + decode_result, +) + +LOGGER = logging.getLogger(__name__) + +_SESSION_PREFIX = "select-ai/sessions/" +_TASK_PREFIX = "select-ai/tasks/" class ReconnectRequired(RuntimeError): """The worker no longer owns the requested in-memory session.""" + def __init__(self, message: str, session_id: str | None = None): + super().__init__(message) + self.session_id = session_id + class WorkerClient: """Open, route, and close short-lived Select AI worker sessions.""" @@ -59,22 +88,152 @@ def open_session(self, session_id: str, session_info: SessionInfo) -> str: raise RuntimeError("Could not create the database session.") return session_id - def send_prompt(self, session_id: str, prompt: str) -> str | None: - """Forward a prompt and return the worker's raw team result.""" + def send_message( + self, + session_id: str, + request: SendMessageRequest, + ) -> Task | Message | None: + """Forward one A2A message to the worker session.""" + result = self._dispatch( + session_id, + A2AMethod.SEND_MESSAGE, + request, + ) + value = decode_result(result) + if isinstance(value, Task): + self._save_task_route(value.id, session_id) + return value + + def get_task(self, params: GetTaskRequest) -> Task | None: + """Retrieve a task from its owning worker session.""" + session_id = self.task_session_id(params.id) + if session_id is None: + return None + result = self._dispatch(session_id, A2AMethod.GET_TASK, params) + value = decode_result(result) + if isinstance(value, Task): + return value + return None + + def list_tasks( + self, + session_id: str, + params: ListTasksRequest, + ) -> ListTasksResponse: + """Retrieve one session's database-paginated task response.""" + result = self._dispatch( + session_id, + A2AMethod.LIST_TASKS, + params, + ) + return decode_result(result) or ListTasksResponse() + + def cancel_task(self, task_id: str) -> Task | None: + """Cancel a task in the worker that owns it.""" + session_id = self.task_session_id(task_id) + if session_id is None: + return None + result = self._dispatch( + session_id, + A2AMethod.CANCEL_TASK, + CancelTaskRequest(id=task_id), + ) + value = decode_result(result) + return value if isinstance(value, Task) else None + + def delete_task(self, task_id: str) -> None: + """Delete a task and its routing metadata.""" + session_id = self.task_session_id(task_id) + if session_id: + try: + self._dispatch( + session_id, + A2AMethod.DELETE_TASK, + GetTaskRequest(id=task_id), + ) + except ReconnectRequired: + pass + self._delete_task_route(task_id) + + def session_exists(self, session_id: str) -> bool: + """Return whether Consul still has a live route for a session.""" + try: + self._route_for(session_id) + except ReconnectRequired: + return False + return True + + def task_session_id(self, task_id: str) -> str | None: + """Return the worker session recorded for a task, if any.""" + response = requests.get( + f"{self.settings.consul_url}/v1/kv/{_TASK_PREFIX}" + f"{quote(task_id, safe='')}", + timeout=10, + ) + if response.status_code == 404: + return None + response.raise_for_status() + values = response.json() or [] + if not values or not values[0].get("Value"): + return None + return base64.b64decode(values[0]["Value"]).decode() + + def _dispatch( + self, + session_id: str, + method: A2AMethod | str, + message, + ) -> WorkerResult: + """Send one protobuf-serialized A2A operation to a worker session.""" route = self._route_for(session_id) response = requests.post( - f"{route.endpoint}/sessions/{session_id}/messages", - json={"prompt": prompt}, + f"{route.endpoint}/sessions/{quote(session_id, safe='')}/a2a", + data=message.SerializeToString(), + headers={ + "content-type": PROTOBUF_CONTENT_TYPE, + WORKER_A2A_METHOD_HEADER: A2AMethod(method).value, + }, timeout=130, **getattr(self, "_worker_request_kwargs", {}), ) if response.status_code in (404, 502): self._close_worker_session(route, session_id) raise ReconnectRequired( - "Database session ended; reconnect required." + "Database session ended; reconnect required.", + session_id, ) response.raise_for_status() - return response.text or None + return WorkerResult( + kind=ResultKind( + response.headers.get( + WORKER_RESULT_KIND_HEADER, + ResultKind.NONE.value, + ) + ), + payload=response.content, + ) + + def _save_task_route(self, task_id: str, session_id: str) -> None: + """Save only task-to-session routing metadata in Consul.""" + try: + response = requests.put( + f"{self.settings.consul_url}/v1/kv/{_TASK_PREFIX}" + f"{quote(task_id, safe='')}", + data=session_id, + timeout=10, + ) + response.raise_for_status() + except requests.RequestException: + # The task itself is already durable in Oracle, but task routes + # are required because workers may use different databases. + LOGGER.warning("Could not save route for task %s", task_id) + + def _delete_task_route(self, task_id: str) -> None: + requests.delete( + f"{self.settings.consul_url}/v1/kv/{_TASK_PREFIX}" + f"{quote(task_id, safe='')}", + timeout=10, + ) def close_session(self, session_id: str) -> None: """Close the child process and remove the Consul route.""" @@ -100,7 +259,8 @@ def _select_worker(self) -> str: worker = workers[self._next_worker % len(workers)] self._next_worker += 1 service = worker["Service"] - endpoint = service.get("Meta", {}).get("endpoint") + metadata = service.get("Meta") or {} + endpoint = metadata.get("endpoint") if endpoint: endpoint = endpoint.rstrip("/") if self.settings.worker_mtls_enabled and not endpoint.startswith( @@ -121,13 +281,14 @@ def _select_worker(self) -> str: def _route_for(self, session_id: str) -> SessionRoute: response = requests.get( - f"{self.settings.consul_url}/v1/kv/select-ai/sessions/" - f"{session_id}", + f"{self.settings.consul_url}/v1/kv/{_SESSION_PREFIX}" + f"{quote(session_id, safe='')}", timeout=10, ) if response.status_code == 404: raise ReconnectRequired( - "Database session expired; reconnect required." + "Database session expired; reconnect required.", + session_id, ) response.raise_for_status() value = response.json()[0]["Value"] @@ -135,14 +296,15 @@ def _route_for(self, session_id: str) -> SessionRoute: if route.expires_at <= time.time(): self._close_worker_session(route, session_id) raise ReconnectRequired( - "Database session expired; reconnect required." + "Database session expired; reconnect required.", + session_id, ) return route def _save_route(self, session_id: str, route: SessionRoute) -> bool: response = requests.put( - f"{self.settings.consul_url}/v1/kv/select-ai/sessions/" - f"{session_id}?cas=0", + f"{self.settings.consul_url}/v1/kv/{_SESSION_PREFIX}" + f"{quote(session_id, safe='')}?cas=0", data=json.dumps(route.__dict__), timeout=10, ) @@ -150,8 +312,8 @@ def _save_route(self, session_id: str, route: SessionRoute) -> bool: def _delete_route(self, session_id: str) -> None: requests.delete( - f"{self.settings.consul_url}/v1/kv/select-ai/sessions/" - f"{session_id}", + f"{self.settings.consul_url}/v1/kv/{_SESSION_PREFIX}" + f"{quote(session_id, safe='')}", timeout=10, ) @@ -162,7 +324,7 @@ def _close_worker_session( ) -> None: try: response = requests.delete( - f"{route.endpoint}/sessions/{session_id}", + f"{route.endpoint}/sessions/{quote(session_id, safe='')}", timeout=10, **getattr(self, "_worker_request_kwargs", {}), ) diff --git a/src/select_ai/agent/a2a/worker_protocol.py b/src/select_ai/agent/a2a/worker_protocol.py new file mode 100644 index 0000000..5aa6cd7 --- /dev/null +++ b/src/select_ai/agent/a2a/worker_protocol.py @@ -0,0 +1,103 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# https://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +"""Typed values for the private Gateway-to-Worker session protocol.""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum + +from a2a.types.a2a_pb2 import ListTasksResponse, Message, Task +from a2a.utils.errors import TaskNotFoundError + +WORKER_A2A_METHOD_HEADER = "x-select-ai-a2a-method" +WORKER_RESULT_KIND_HEADER = "x-select-ai-a2a-result-kind" +PROTOBUF_CONTENT_TYPE = "application/x-protobuf" + + +class PipeMessageType(str, Enum): + """Message types exchanged over a session process pipe.""" + + A2A = "a2a" + CLOSE = "close" + READY = "ready" + RESULT = "result" + ERROR = "error" + A2A_ERROR = "a2a_error" + + +class A2AMethod(str, Enum): + """A2A operations supported by the private session runtime.""" + + SEND_MESSAGE = "SendMessage" + GET_TASK = "GetTask" + LIST_TASKS = "ListTasks" + CANCEL_TASK = "CancelTask" + DELETE_TASK = "DeleteTask" + + +class ResultKind(str, Enum): + """Response types returned by a session runtime.""" + + NONE = "none" + TASK_NOT_FOUND = "task_not_found" + TASK = "task" + MESSAGE = "message" + LIST_TASKS = "list_tasks" + + +@dataclass(frozen=True) +class WorkerResult: + """One typed response from the worker session runtime.""" + + kind: ResultKind + payload: bytes = b"" + + +def parse_request(message_type, payload: bytes): + """Parse one protobuf request from the internal wire format.""" + message = message_type() + message.ParseFromString(payload) + return message + + +def encode_result(value) -> WorkerResult: + """Encode one supported A2A response as protobuf wire bytes.""" + if value is None: + return WorkerResult(ResultKind.NONE) + if isinstance(value, Task): + kind = ResultKind.TASK + elif isinstance(value, Message): + kind = ResultKind.MESSAGE + elif isinstance(value, ListTasksResponse): + kind = ResultKind.LIST_TASKS + else: + raise TypeError(f"Unsupported A2A response type: {type(value)!r}") + return WorkerResult(kind, value.SerializeToString()) + + +def decode_result(result: WorkerResult, expected_type=None): + """Decode a session runtime response from protobuf wire bytes.""" + kind = result.kind + if kind == ResultKind.TASK_NOT_FOUND: + raise TaskNotFoundError + if kind == ResultKind.NONE: + return None + payload = result.payload + if not payload: + return None + message_type = expected_type or { + ResultKind.TASK: Task, + ResultKind.MESSAGE: Message, + ResultKind.LIST_TASKS: ListTasksResponse, + }.get(kind) + if message_type is None: + raise ValueError(f"Unsupported A2A result kind: {kind}") + message = message_type() + message.ParseFromString(payload) + return message diff --git a/src/select_ai/cli/a2a.py b/src/select_ai/cli/a2a.py index e43342e..cab69e8 100644 --- a/src/select_ai/cli/a2a.py +++ b/src/select_ai/cli/a2a.py @@ -132,7 +132,7 @@ def worker( tls_key_file, tls_ca_file, ): - """Start the internal, in-memory Select AI session worker.""" + """Start the internal Select AI session worker.""" try: from select_ai.agent.a2a import create_worker_app except ImportError as error: diff --git a/tests/a2a/test_worker_runtime.py b/tests/a2a/test_worker_runtime.py index 8ab1cff..89671f6 100644 --- a/tests/a2a/test_worker_runtime.py +++ b/tests/a2a/test_worker_runtime.py @@ -11,14 +11,46 @@ import pytest import requests +from google.protobuf.json_format import MessageToJson pytest.importorskip("fastapi") import select_ai -from select_ai.agent.a2a import GatewaySettings, worker -from select_ai.agent.a2a.gateway import GatewayExecutor, _a2ui_part +from a2a.helpers import new_data_part, new_text_part +from a2a.server.context import ServerCallContext +from a2a.types.a2a_pb2 import ( + CancelTaskRequest, + GetTaskRequest, + ListTasksRequest, + Message, + Role, + SendMessageRequest, + Task, + TaskState, +) +from a2a.utils.errors import ( + InvalidParamsError, + TaskNotFoundError, + UnsupportedOperationError, +) +from select_ai.agent.a2a import GatewaySettings, session_runtime, worker +from select_ai.agent.a2a.a2ui import ( + A2UI_CATALOG_ID, + A2UI_EXTENSION_URI, + A2UI_VERSION, + a2ui_part, + find_action, +) +from select_ai.agent.a2a.forms import connection_form +from select_ai.agent.a2a.gateway import GatewayRequestHandler from select_ai.agent.a2a.results import message_parts -from select_ai.agent.a2a.worker_client import WorkerClient +from select_ai.agent.a2a.worker_client import ReconnectRequired, WorkerClient +from select_ai.agent.a2a.worker_protocol import ( + A2AMethod, + ResultKind, + WorkerResult, + decode_result, +) class FakeConnection: @@ -64,10 +96,19 @@ def join(self, timeout): def terminate(self): self.alive = False + def kill(self): + self.alive = False + -def test_worker_uses_pipe_process_and_returns_raw_result(monkeypatch): +def test_worker_uses_pipe_process_and_dispatches_a2a(monkeypatch): parent = FakeConnection( - [{"type": "ready"}, {"type": "result", "result": "hi"}] + [ + {"type": "ready"}, + { + "type": "result", + "result": WorkerResult(ResultKind.NONE), + }, + ] ) child = FakeConnection() processes = [] @@ -93,13 +134,26 @@ def process_factory(**kwargs): ) asyncio.run(session_worker.open(request)) - result = asyncio.run(session_worker.send_prompt("session-1", "hello")) + task_request = GetTaskRequest(id="task-1") + result = asyncio.run( + session_worker.dispatch( + "session-1", + "GetTask", + task_request.SerializeToString(), + ) + ) - assert result == "hi" + assert result == WorkerResult(ResultKind.NONE) assert child.closed assert processes[0].target is worker._session_process_main assert processes[0].daemon is True - assert parent.sent == [{"type": "run", "prompt": "hello"}] + assert parent.sent == [ + { + "type": "a2a", + "method": "GetTask", + "payload": task_request.SerializeToString(), + } + ] def test_expired_session_closes_the_process_and_pipe(monkeypatch): @@ -138,32 +192,82 @@ def process_factory(**kwargs): assert parent.sent == [{"type": "close"}] -def test_session_runtime_uses_one_async_connection_and_returns_raw_result( +def test_session_reaper_closes_idle_expired_process(monkeypatch): + parent = FakeConnection([{"type": "ready"}]) + child = FakeConnection() + process = None + + def process_factory(**kwargs): + nonlocal process + process = FakeProcess(**kwargs) + return process + + monkeypatch.setattr( + worker.multiprocessing, + "Pipe", + lambda: (parent, child), + ) + monkeypatch.setattr(worker.multiprocessing, "Process", process_factory) + monkeypatch.setattr(worker, "_SESSION_REAPER_INTERVAL_SECONDS", 0.01) + session_worker = worker.SessionWorker(60, 1) + request = worker.OpenSessionRequest( + session_id="session-1", + username="user", + password="password", + dsn="database", + team_name="TEAM", + ) + + async def exercise_reaper(): + await session_worker.open(request) + session_worker.sessions["session-1"].expires_at = 0 + reaper = asyncio.create_task(session_worker.reap_expired()) + try: + await asyncio.wait_for(_wait_for_process_exit(process), 1) + finally: + reaper.cancel() + with pytest.raises(asyncio.CancelledError): + await reaper + + async def _wait_for_process_exit(session_process): + while session_process is None or session_process.alive: + await asyncio.sleep(0.01) + + asyncio.run(exercise_reaper()) + + assert process is not None + assert parent.closed + assert parent.sent == [{"type": "close"}] + assert session_worker.sessions == {} + + +def test_session_runtime_uses_one_async_connection_and_dispatches_a2a( monkeypatch, ): connection = FakeConnection( - [{"type": "run", "prompt": "hello"}, {"type": "close"}] + [ + { + "type": "a2a", + "method": "GetTask", + "payload": GetTaskRequest(id="t1").SerializeToString(), + }, + {"type": "close"}, + ] ) connection_arguments = {} - class Conversation: - def __init__(self, attributes): - self.attributes = attributes - - async def create(self): - return "conversation-1" - - class Team: - def __init__(self, team_name): + class Runtime: + def __init__(self, session_id, team_name): + assert session_id == "session-1" assert team_name == "TEAM" - async def run(self, prompt, params): - assert prompt == "hello" - assert params == {"conversation_id": "conversation-1"} - return ( - '{"metadata":{"mimeType":"application/json+a2ui"},' - '"data":[]}' - ) + async def initialize(self): + return None + + async def handle(self, method, payload): + assert method == "GetTask" + assert GetTaskRequest.FromString(payload).id == "t1" + return WorkerResult(ResultKind.NONE) async def async_connect(**kwargs): connection_arguments.update(kwargs) @@ -177,8 +281,7 @@ async def disconnect(): monkeypatch.setattr(select_ai, "async_connect", async_connect) monkeypatch.setattr(select_ai, "async_is_connected", connected) monkeypatch.setattr(select_ai, "async_disconnect", disconnect) - monkeypatch.setattr(select_ai, "AsyncConversation", Conversation) - monkeypatch.setattr(select_ai.agent, "AsyncTeam", Team) + monkeypatch.setattr(worker, "SessionRuntime", Runtime) asyncio.run( worker._run_session_process( @@ -201,12 +304,115 @@ async def disconnect(): assert connection.sent[0] == {"type": "ready"} assert connection.sent[1] == { "type": "result", - "result": ( - '{"metadata":{"mimeType":"application/json+a2ui"},' '"data":[]}' - ), + "result": WorkerResult(ResultKind.NONE), } +def test_session_runtime_builds_oracle_backed_default_handler(monkeypatch): + initialized = [] + + class Team: + @staticmethod + async def fetch(team_name): + assert team_name == "TEAM" + initialized.append("team") + + class Store: + async def initialize(self): + initialized.append(self) + + class Handler: + def __init__(self, **kwargs): + self.kwargs = kwargs + + monkeypatch.setattr(session_runtime, "OracleTaskStore", Store) + monkeypatch.setattr(session_runtime, "OracleContextStore", Store) + monkeypatch.setattr(session_runtime, "AsyncTeam", Team) + monkeypatch.setattr(session_runtime, "DefaultRequestHandler", Handler) + monkeypatch.setattr( + session_runtime, + "DatabaseTeamExecutor", + lambda team_name, context_store: (team_name, context_store), + ) + monkeypatch.setattr( + session_runtime, + "_build_agent_card", + lambda *args: "agent-card", + ) + + runtime = session_runtime.SessionRuntime("session-1", "TEAM") + asyncio.run(runtime.initialize()) + + assert initialized == [ + "team", + runtime.task_store, + runtime.context_store, + ] + assert runtime.handler.kwargs["task_store"] is runtime.task_store + assert runtime.handler.kwargs["agent_card"] == "agent-card" + + +def test_session_runtime_rejects_unknown_team_before_initializing_stores( + monkeypatch, +): + initialized = [] + + class Team: + @staticmethod + async def fetch(team_name): + assert team_name == "MISSPELLED_TEAM" + raise RuntimeError("team does not exist") + + class Store: + async def initialize(self): + initialized.append(self) + + monkeypatch.setattr(session_runtime, "AsyncTeam", Team) + monkeypatch.setattr(session_runtime, "OracleTaskStore", Store) + monkeypatch.setattr(session_runtime, "OracleContextStore", Store) + + runtime = session_runtime.SessionRuntime("session-1", "MISSPELLED_TEAM") + with pytest.raises(RuntimeError, match="team does not exist"): + asyncio.run(runtime.initialize()) + + assert initialized == [] + assert runtime.handler is None + + +def test_session_runtime_preserves_database_task_not_found(): + class Handler: + async def on_get_task(self, _request, _context): + raise TaskNotFoundError + + runtime = session_runtime.SessionRuntime("session-1", "TEAM") + runtime.handler = Handler() + + result = asyncio.run( + runtime.handle( + A2AMethod.GET_TASK, + GetTaskRequest(id="missing-task").SerializeToString(), + ) + ) + + assert result == WorkerResult(ResultKind.TASK_NOT_FOUND) + + +def test_decode_result_raises_only_for_explicit_database_task_not_found(): + with pytest.raises(TaskNotFoundError): + decode_result(WorkerResult(ResultKind.TASK_NOT_FOUND)) + + assert decode_result(WorkerResult(ResultKind.NONE)) is None + + +def test_gateway_rejects_unsupported_stream(): + async def consume_stream(): + stream = GatewayRequestHandler.on_message_send_stream(None, None) + with pytest.raises(UnsupportedOperationError): + await anext(stream) + + asyncio.run(consume_stream()) + + def test_message_parts_preserves_serialized_text_and_data_parts(): parts = message_parts( """{ @@ -227,19 +433,30 @@ def test_message_parts_preserves_serialized_text_and_data_parts(): assert parts[1].WhichOneof("content") == "data" -def test_worker_client_returns_the_raw_team_result(monkeypatch): +def test_worker_client_forwards_and_parses_a2a_message(monkeypatch): + sent = {} + class Response: status_code = 200 + headers = {"x-select-ai-a2a-result-kind": "message"} + content = Message( + message_id="m1", + role=Role.ROLE_AGENT, + parts=[new_text_part("hello")], + ).SerializeToString() @staticmethod def raise_for_status(): return None - text = "team result" + def post(*args, **kwargs): + del args + sent.update(kwargs) + return Response() monkeypatch.setattr( "select_ai.agent.a2a.worker_client.requests.post", - lambda *args, **kwargs: Response(), + post, ) client = WorkerClient.__new__(WorkerClient) monkeypatch.setattr( @@ -248,7 +465,23 @@ def raise_for_status(): lambda session_id: type("Route", (), {"endpoint": "http://worker"})(), ) - assert client.send_prompt("context-1", "hello") == "team result" + request = SendMessageRequest( + message=Message( + message_id="m1", + role=Role.ROLE_USER, + parts=[new_text_part("hello")], + ) + ) + result = client.send_message("context-1", request) + + assert result is not None + assert result.message_id == "m1" + assert result.parts[0].text == "hello" + assert sent["headers"] == { + "content-type": "application/x-protobuf", + "x-select-ai-a2a-method": "SendMessage", + } + assert sent["data"] == request.SerializeToString() def test_worker_client_uses_consul_https_endpoint_with_mtls(monkeypatch): @@ -341,7 +574,7 @@ async def put(self, _url, json): def test_a2ui_operation_uses_a_metadata_marked_data_part(): from google.protobuf.json_format import MessageToDict - part = _a2ui_part( + part = a2ui_part( {"version": "v0.9", "createSurface": {"surfaceId": "form"}} ) @@ -354,48 +587,88 @@ def test_a2ui_operation_uses_a_metadata_marked_data_part(): } -def test_a2ui_action_reads_an_operation_list_or_single_operation(): - action = GatewayExecutor._a2ui_action( - type( - "Context", - (), - { - "message": type( - "Message", - (), +def test_each_connection_form_uses_one_new_unique_surface(): + first = connection_form() + second = connection_form() + + first_surface_ids = [ + operation[message_type]["surfaceId"] + for operation, message_type in zip( + first, + ("createSurface", "updateComponents", "updateDataModel"), + ) + ] + second_surface_id = second[0]["createSurface"]["surfaceId"] + + assert len(set(first_surface_ids)) == 1 + assert first_surface_ids[0].startswith("db-connect-") + assert first_surface_ids[0] != second_surface_id + + +def test_connection_form_matches_advertised_a2ui_version(): + assert A2UI_EXTENSION_URI.endswith(f"/{A2UI_VERSION}") + assert f"/{A2UI_VERSION.replace('.', '_')}/" in A2UI_CATALOG_ID + assert all( + operation["version"] == A2UI_VERSION for operation in connection_form() + ) + + +def test_connection_action_reads_an_operation_list_or_single_operation(): + action = find_action( + Message( + role=Role.ROLE_USER, + parts=[ + a2ui_part( { - "parts": [ - _a2ui_part( - { - "version": "v0.9", - "action": { - "name": "submit_database_connection" - }, - } - ) - ] - }, - )() - }, - )() + "version": "v0.9", + "action": {"name": "submit_database_connection"}, + } + ) + ], + ), + "submit_database_connection", ) assert action == {"name": "submit_database_connection"} +def test_connection_action_accepts_gemini_unmarked_data_part(): + action = find_action( + Message( + role=Role.ROLE_USER, + parts=[ + new_data_part( + { + "version": "v0.9", + "action": { + "name": "submit_database_connection", + "context": {"team_name": "TEAM"}, + }, + } + ) + ], + ), + "submit_database_connection", + ) + + assert action == { + "name": "submit_database_connection", + "context": {"team_name": "TEAM"}, + } + + def test_gateway_returns_connection_error_when_worker_rejects_opening(): - executor = GatewayExecutor.__new__(GatewayExecutor) - executor.sessions = {} + handler = GatewayRequestHandler.__new__(GatewayRequestHandler) class Client: @staticmethod def open_session(_context_id, _session_info): raise requests.HTTPError("worker rejected the connection") - executor.worker_client = Client() + handler.worker_client = Client() - parts = asyncio.run( - executor._open_session( + session_id = asyncio.run( + handler._open_session( { "dsn": "database", "username": "user", @@ -406,5 +679,389 @@ def open_session(_context_id, _session_info): ) ) - assert parts[0].text.startswith("Could not connect.") - assert executor.sessions == {} + assert session_id is None + + +def test_gateway_bootstrap_is_transient_until_worker_session_opens(): + class Client: + def __init__(self): + self.opened = False + self.saved = None + + def session_exists(self, _session_id): + return self.opened + + def open_session(self, session_id, session_info): + assert session_id == "context-1" + assert session_info.team_name == "TEAM" + self.opened = True + return session_id + + client = Client() + handler = GatewayRequestHandler.__new__(GatewayRequestHandler) + handler.worker_client = client + + form_request = SendMessageRequest( + message=Message( + message_id="m1", + context_id="context-1", + role=Role.ROLE_USER, + parts=[new_text_part("connect")], + ) + ) + form_task = asyncio.run( + handler.on_message_send(form_request, ServerCallContext()) + ) + + assert isinstance(form_task, Task) + assert form_task.context_id == "context-1" + assert form_task.artifacts[0].name == "database-connection-form" + assert len(form_task.artifacts[0].parts) == 3 + + connect_request = SendMessageRequest( + message=Message( + message_id="m2", + task_id=form_task.id, + context_id=form_task.context_id, + role=Role.ROLE_USER, + parts=[ + a2ui_part( + { + "version": "v0.9", + "action": { + "name": "submit_database_connection", + "context": { + "dsn": "database", + "username": "user", + "password": "password", + "team_name": "TEAM", + }, + }, + } + ) + ], + ) + ) + connected_task = asyncio.run( + handler.on_message_send(connect_request, ServerCallContext()) + ) + + assert connected_task.status.state == TaskState.TASK_STATE_COMPLETED + assert all(item.message_id != "m2" for item in connected_task.history) + assert '"password": "password"' not in MessageToJson(connected_task) + + +def test_gateway_replaces_bootstrap_task_with_worker_owned_task(): + class Client: + calls = 0 + + @staticmethod + def session_exists(_session_id): + return True + + def send_message(self, session_id, request): + assert session_id == "context-1" + self.calls += 1 + if self.calls == 1: + assert request.message.task_id == ( + "gateway-bootstrap-form-task" + ) + raise TaskNotFoundError + assert not request.message.task_id + return Task( + id="worker-task", + context_id=session_id, + status={"state": TaskState.TASK_STATE_COMPLETED}, + ) + + client = Client() + handler = GatewayRequestHandler.__new__(GatewayRequestHandler) + handler.worker_client = client + request = SendMessageRequest( + message=Message( + message_id="m3", + task_id="gateway-bootstrap-form-task", + context_id="context-1", + role=Role.ROLE_USER, + parts=[new_text_part("hello")], + ) + ) + + task = asyncio.run(handler.on_message_send(request, ServerCallContext())) + + assert task.id == "worker-task" + assert task.context_id == "context-1" + assert client.calls == 2 + + +def test_gateway_preserves_missing_worker_task_error(): + class Client: + @staticmethod + def session_exists(_session_id): + return True + + @staticmethod + def send_message(_session_id, _request): + raise TaskNotFoundError + + handler = GatewayRequestHandler.__new__(GatewayRequestHandler) + handler.worker_client = Client() + request = SendMessageRequest( + message=Message( + message_id="m3", + task_id="oracle-task-that-no-longer-exists", + context_id="context-1", + role=Role.ROLE_USER, + parts=[new_text_part("hello")], + ) + ) + + with pytest.raises(TaskNotFoundError): + asyncio.run(handler.on_message_send(request, ServerCallContext())) + + +def test_gateway_bootstrap_without_context_returns_transient_form_task(): + class Client: + @staticmethod + def session_exists(session_id): + assert session_id + return False + + handler = GatewayRequestHandler.__new__(GatewayRequestHandler) + handler.worker_client = Client() + request = SendMessageRequest( + message=Message( + message_id="m1", + role=Role.ROLE_USER, + parts=[new_text_part("hello")], + ) + ) + + form_task = asyncio.run( + handler.on_message_send(request, ServerCallContext()) + ) + + assert isinstance(form_task, Task) + assert form_task.id.startswith("gateway-bootstrap-") + assert form_task.context_id + # The connection form is a response-only gateway task. Its ID must not be + # attached to the user's message because no corresponding task exists in + # the worker's Oracle task store. + assert not form_task.history[0].task_id + assert form_task.status.state == TaskState.TASK_STATE_COMPLETED + assert form_task.artifacts[0].name == "database-connection-form" + assert len(form_task.artifacts[0].parts) == 3 + + +def test_gateway_returns_task_form_when_task_session_expires_before_send(): + class Client: + @staticmethod + def get_task(_request): + raise ReconnectRequired( + "Database session ended; reconnect required.", + "context-1", + ) + + handler = GatewayRequestHandler.__new__(GatewayRequestHandler) + handler.worker_client = Client() + request = SendMessageRequest( + message=Message( + message_id="m1", + task_id="task-1", + role=Role.ROLE_USER, + parts=[new_text_part("hello")], + ) + ) + + task = asyncio.run(handler.on_message_send(request, ServerCallContext())) + + assert isinstance(task, Task) + assert task.id == "task-1" + assert task.context_id == "context-1" + assert task.status.state == TaskState.TASK_STATE_COMPLETED + assert task.artifacts[0].name == "database-connection-form" + assert len(task.artifacts[0].parts) == 3 + + +def test_gateway_returns_task_form_when_task_route_is_missing_before_send(): + class Client: + @staticmethod + def get_task(_request): + return None + + handler = GatewayRequestHandler.__new__(GatewayRequestHandler) + handler.worker_client = Client() + request = SendMessageRequest( + message=Message( + message_id="m1", + task_id="task-from-old-deployment", + role=Role.ROLE_USER, + parts=[new_text_part("hello")], + ) + ) + + task = asyncio.run(handler.on_message_send(request, ServerCallContext())) + + assert isinstance(task, Task) + assert task.id == "task-from-old-deployment" + assert task.context_id + assert task.status.state == TaskState.TASK_STATE_COMPLETED + assert task.artifacts[0].name == "database-connection-form" + assert len(task.artifacts[0].parts) == 3 + + +def test_gateway_uses_task_form_for_expired_context_only_message(): + class Client: + @staticmethod + def session_exists(_session_id): + return True + + @staticmethod + def send_message(_session_id, _request): + raise ReconnectRequired( + "Database session ended; reconnect required.", + "context-1", + ) + + handler = GatewayRequestHandler.__new__(GatewayRequestHandler) + handler.worker_client = Client() + request = SendMessageRequest( + message=Message( + message_id="m1", + context_id="context-1", + role=Role.ROLE_USER, + parts=[new_text_part("hello")], + ) + ) + + task = asyncio.run(handler.on_message_send(request, ServerCallContext())) + + assert isinstance(task, Task) + assert task.id + assert task.context_id == "context-1" + assert task.status.state == TaskState.TASK_STATE_COMPLETED + assert task.artifacts[0].name == "database-connection-form" + assert len(task.artifacts[0].parts) == 3 + + +def test_gateway_uses_connection_form_when_get_task_session_expires(): + class Client: + @staticmethod + def get_task(_request): + raise ReconnectRequired( + "Database session ended; reconnect required.", + "context-1", + ) + + handler = GatewayRequestHandler.__new__(GatewayRequestHandler) + handler.worker_client = Client() + + task = asyncio.run( + handler.on_get_task( + GetTaskRequest(id="task-1"), + ServerCallContext(), + ) + ) + + assert task.id == "task-1" + assert task.context_id == "context-1" + assert task.status.state == TaskState.TASK_STATE_COMPLETED + assert task.artifacts[0].name == "database-connection-form" + assert len(task.artifacts[0].parts) == 3 + + +def test_gateway_uses_connection_form_when_get_task_route_is_missing(): + class Client: + @staticmethod + def get_task(_request): + return None + + handler = GatewayRequestHandler.__new__(GatewayRequestHandler) + handler.worker_client = Client() + + task = asyncio.run( + handler.on_get_task( + GetTaskRequest(id="task-from-old-deployment"), + ServerCallContext(), + ) + ) + + assert task.id == "task-from-old-deployment" + assert task.context_id + assert task.status.state == TaskState.TASK_STATE_COMPLETED + assert task.artifacts[0].name == "database-connection-form" + assert len(task.artifacts[0].parts) == 3 + + +def test_gateway_uses_connection_form_when_cancel_task_session_expires(): + class Client: + @staticmethod + def cancel_task(_task_id): + raise ReconnectRequired( + "Database session ended; reconnect required.", + "context-1", + ) + + handler = GatewayRequestHandler.__new__(GatewayRequestHandler) + handler.worker_client = Client() + + task = asyncio.run( + handler.on_cancel_task( + CancelTaskRequest(id="task-1"), + ServerCallContext(), + ) + ) + + assert task.id == "task-1" + assert task.context_id == "context-1" + assert task.status.state == TaskState.TASK_STATE_COMPLETED + assert task.artifacts[0].name == "database-connection-form" + assert len(task.artifacts[0].parts) == 3 + + +def test_gateway_uses_connection_form_when_cancel_task_route_is_missing(): + class Client: + @staticmethod + def cancel_task(_task_id): + return None + + handler = GatewayRequestHandler.__new__(GatewayRequestHandler) + handler.worker_client = Client() + + task = asyncio.run( + handler.on_cancel_task( + CancelTaskRequest(id="task-from-old-deployment"), + ServerCallContext(), + ) + ) + + assert task.id == "task-from-old-deployment" + assert task.context_id + assert task.status.state == TaskState.TASK_STATE_COMPLETED + assert task.artifacts[0].name == "database-connection-form" + assert len(task.artifacts[0].parts) == 3 + + +def test_gateway_exposes_connection_form_data_when_list_session_expires(): + class Client: + @staticmethod + def list_tasks(_context_id, _request): + raise ReconnectRequired( + "Database session ended; reconnect required.", + "context-1", + ) + + handler = GatewayRequestHandler.__new__(GatewayRequestHandler) + handler.worker_client = Client() + + with pytest.raises(InvalidParamsError) as raised: + asyncio.run( + handler.on_list_tasks( + ListTasksRequest(context_id="context-1"), + ServerCallContext(), + ) + ) + + assert raised.value.data["reason"] == "SESSION_EXPIRED" + assert raised.value.data["contextId"] == "context-1" + assert raised.value.data["connectionForm"] From 06806ae8c12edd2319b3458eb0d785b9a2e96e79 Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Thu, 3 Sep 2026 23:51:58 -0700 Subject: [PATCH 17/31] Added Sharing APIs to share AI profile, vector_index, teams with other users --- src/select_ai/__init__.py | 4 + src/select_ai/_validations.py | 10 + src/select_ai/agent/team.py | 49 ++++ src/select_ai/async_profile.py | 95 +++++-- src/select_ai/base_profile.py | 13 + src/select_ai/credential.py | 265 +++++++++++++++++- src/select_ai/profile.py | 105 +++++-- src/select_ai/sql.py | 42 +-- src/select_ai/vector_index.py | 203 +++++++++++--- tests/agents/test_3300_teams.py | 64 +++++ tests/agents/test_3301_async_teams.py | 58 ++++ tests/conftest.py | 64 ++++- tests/profiles/test_1200_profile.py | 57 ++++ tests/profiles/test_1300_profile_async.py | 36 +++ .../test_5000_async_create_index.py | 57 ++++ tests/vector_index/test_5000_create_index.py | 44 +++ 16 files changed, 1052 insertions(+), 114 deletions(-) diff --git a/src/select_ai/__init__.py b/src/select_ai/__init__.py index 4f4871a..6441ef6 100644 --- a/src/select_ai/__init__.py +++ b/src/select_ai/__init__.py @@ -17,8 +17,12 @@ from .credential import ( async_create_credential, async_delete_credential, + async_grant_credential_access, + async_revoke_credential_access, create_credential, delete_credential, + grant_credential_access, + revoke_credential_access, ) from .db import ( async_connect, diff --git a/src/select_ai/_validations.py b/src/select_ai/_validations.py index 2de68f3..2ccfbc5 100644 --- a/src/select_ai/_validations.py +++ b/src/select_ai/_validations.py @@ -13,6 +13,16 @@ NoneType = type(None) +def validate_user_or_role_name(user_or_role_name: str) -> str: + """Validate and normalize a sharing grantee name.""" + if not isinstance(user_or_role_name, str): + raise TypeError("'user_or_role_name' must be a string") + user_or_role_name = user_or_role_name.strip() + if not user_or_role_name: + raise ValueError("'user_or_role_name' cannot be empty") + return user_or_role_name + + def _match(value, annot) -> bool: """Recursively validate value against a typing annotation.""" if annot is Any: diff --git a/src/select_ai/agent/team.py b/src/select_ai/agent/team.py index 42239f1..e767ba6 100644 --- a/src/select_ai/agent/team.py +++ b/src/select_ai/agent/team.py @@ -21,6 +21,7 @@ import oracledb from select_ai._abc import SelectAIDataClass +from select_ai._validations import validate_user_or_role_name from select_ai.agent.sql import ( GET_USER_AI_AGENT_TEAM, GET_USER_AI_AGENT_TEAM_ATTRIBUTES, @@ -243,6 +244,30 @@ def enable(self): }, ) + def grant_access(self, user_or_role_name: str) -> None: + """Grant a database user or role access to this agent team.""" + user_or_role_name = validate_user_or_role_name(user_or_role_name) + with cursor() as cr: + cr.callproc( + "DBMS_CLOUD_AI_AGENT.GRANT_TEAM_ACCESS", + keyword_parameters={ + "team_name": self.team_name, + "user_or_role_name": user_or_role_name, + }, + ) + + def revoke_access(self, user_or_role_name: str) -> None: + """Revoke a database user or role's access to this agent team.""" + user_or_role_name = validate_user_or_role_name(user_or_role_name) + with cursor() as cr: + cr.callproc( + "DBMS_CLOUD_AI_AGENT.REVOKE_TEAM_ACCESS", + keyword_parameters={ + "team_name": self.team_name, + "user_or_role_name": user_or_role_name, + }, + ) + @classmethod def fetch(cls, team_name: str) -> "Team": """ @@ -704,6 +729,30 @@ async def enable(self): }, ) + async def grant_access(self, user_or_role_name: str) -> None: + """Asynchronously grant a user or role access to this agent team.""" + user_or_role_name = validate_user_or_role_name(user_or_role_name) + async with async_cursor() as cr: + await cr.callproc( + "DBMS_CLOUD_AI_AGENT.GRANT_TEAM_ACCESS", + keyword_parameters={ + "team_name": self.team_name, + "user_or_role_name": user_or_role_name, + }, + ) + + async def revoke_access(self, user_or_role_name: str) -> None: + """Asynchronously revoke a user or role's agent team access.""" + user_or_role_name = validate_user_or_role_name(user_or_role_name) + async with async_cursor() as cr: + await cr.callproc( + "DBMS_CLOUD_AI_AGENT.REVOKE_TEAM_ACCESS", + keyword_parameters={ + "team_name": self.team_name, + "user_or_role_name": user_or_role_name, + }, + ) + @classmethod async def fetch(cls, team_name: str) -> "AsyncTeam": """ diff --git a/src/select_ai/async_profile.py b/src/select_ai/async_profile.py index 91572fe..099f6d3 100644 --- a/src/select_ai/async_profile.py +++ b/src/select_ai/async_profile.py @@ -19,6 +19,7 @@ import oracledb import pandas +from select_ai._validations import validate_user_or_role_name from select_ai.action import Action from select_ai.base_profile import ( BaseProfile, @@ -43,9 +44,9 @@ ) from select_ai.provider import Provider from select_ai.sql import ( - GET_USER_AI_PROFILE, - GET_USER_AI_PROFILE_ATTRIBUTES, - LIST_USER_AI_PROFILES, + GET_ALL_AI_PROFILE, + GET_ALL_AI_PROFILE_ATTRIBUTES, + LIST_ALL_AI_PROFILES, ) from select_ai.summary import SummaryParams from select_ai.synthetic_data import SyntheticDataAttributes @@ -75,12 +76,17 @@ async def _init_profile(self): if self.profile_name: profile_exists = False try: - saved_description = await self._get_profile_description( - profile_name=self.profile_name + saved_description, saved_owner = ( + await self._get_profile_description( + profile_name=self.profile_name, + owner=self.owner, + ) ) + self.owner = saved_owner profile_exists = True saved_attributes = await self._get_attributes( profile_name=self.profile_name, + owner=self.owner, raise_on_empty=True, ) self._raise_error_if_profile_exists() @@ -100,38 +106,43 @@ async def _init_profile(self): return self @staticmethod - async def _get_profile_description(profile_name) -> Union[str, None]: - """Get description of profile from USER_CLOUD_AI_PROFILES + async def _get_profile_description( + profile_name: str, owner: Optional[str] = None + ) -> Tuple[Union[str, None], str]: + """Get a profile description and owner from ALL_CLOUD_AI_PROFILES. :param str profile_name: Name of profile - :return: Description of profile - :rtype: str + :param str owner: Owner of a shared profile. Defaults to current schema. + :return: Tuple containing the profile description and owner. :raises: ProfileNotFoundError """ async with async_cursor() as cr: await cr.execute( - GET_USER_AI_PROFILE, + GET_ALL_AI_PROFILE, profile_name=profile_name.upper(), + owner=owner.upper() if owner else None, ) profile = await cr.fetchone() - if profile is None: - raise ProfileNotFoundError(profile_name) if profile: if profile[1] is not None: - return await profile[1].read() + description = await profile[1].read() else: - return None + description = None + return description, profile[2] else: raise ProfileNotFoundError(profile_name) @staticmethod async def _get_attributes( - profile_name: str, raise_on_empty: bool = True + profile_name: str, + owner: Optional[str] = None, + raise_on_empty: bool = True, ) -> Union[ProfileAttributes, None]: """Asynchronously gets AI profile attributes from the Database :param str profile_name: Name of the profile + :param str owner: Owner of a shared profile. Defaults to current schema. :param bool raise_on_empty: Raise an error if attributes are empty :return: select_ai.provider.ProviderAttributes :raises: select_ai.errors.ProfileAttributesEmptyError @@ -139,8 +150,9 @@ async def _get_attributes( """ async with async_cursor() as cr: await cr.execute( - GET_USER_AI_PROFILE_ATTRIBUTES, + GET_ALL_AI_PROFILE_ATTRIBUTES, profile_name=profile_name.upper(), + owner=owner.upper() if owner else None, ) attributes = await cr.fetchall() if attributes: @@ -158,7 +170,10 @@ async def get_attributes(self) -> ProfileAttributes: :return: select_ai.provider.ProviderAttributes :raises: ProfileNotFoundError """ - return await self._get_attributes(profile_name=self.profile_name) + return await self._get_attributes( + profile_name=self.profile_name, + owner=self.owner, + ) async def _set_attribute( self, @@ -299,6 +314,30 @@ async def disable(self) -> None: keyword_parameters={"profile_name": self.profile_name}, ) + async def grant_access(self, user_or_role_name: str) -> None: + """Asynchronously grant a user or role access to this AI profile.""" + user_or_role_name = validate_user_or_role_name(user_or_role_name) + async with async_cursor() as cr: + await cr.callproc( + "DBMS_CLOUD_AI.GRANT_PROFILE_ACCESS", + keyword_parameters={ + "profile_name": self.profile_name, + "user_or_role_name": user_or_role_name, + }, + ) + + async def revoke_access(self, user_or_role_name: str) -> None: + """Asynchronously revoke a user or role's profile access.""" + user_or_role_name = validate_user_or_role_name(user_or_role_name) + async with async_cursor() as cr: + await cr.callproc( + "DBMS_CLOUD_AI.REVOKE_PROFILE_ACCESS", + keyword_parameters={ + "profile_name": self.profile_name, + "user_or_role_name": user_or_role_name, + }, + ) + @classmethod async def delete_profile(cls, profile_name: str, force: bool = False): """Asynchronously deletes an AI profile from the database @@ -311,15 +350,22 @@ async def delete_profile(cls, profile_name: str, force: bool = False): await cls._delete(profile_name=profile_name, force=force) @classmethod - async def fetch(cls, profile_name: str) -> "AsyncProfile": + async def fetch( + cls, profile_name: str, owner: Optional[str] = None + ) -> "AsyncProfile": """Asynchronously create an AI Profile object from attributes saved in the database - :param str profile_name: + :param str profile_name: Name of the AI profile. + :param str owner: Owner of a shared profile. Defaults to current schema. :return: select_ai.Profile :raises: ProfileNotFoundError """ - return await cls(profile_name, raise_error_if_exists=False) + return await cls( + profile_name, + owner=owner, + raise_error_if_exists=False, + ) async def _save_feedback( self, @@ -410,26 +456,31 @@ async def delete_feedback( @classmethod async def list( - cls, profile_name_pattern: str = ".*" + cls, + profile_name_pattern: str = ".*", + owner: Optional[str] = None, ) -> AsyncGenerator["AsyncProfile", None]: """Asynchronously list AI Profiles saved in the database. :param str profile_name_pattern: Regular expressions can be used to specify a pattern. Function REGEXP_LIKE is used to perform the match. Default value is ".*" i.e. match all AI profiles. + :param str owner: Owner of shared profiles. Defaults to current schema. :return: Iterator[Profile] """ async with async_cursor() as cr: await cr.execute( - LIST_USER_AI_PROFILES, + LIST_ALL_AI_PROFILES, profile_name_pattern=profile_name_pattern, + owner=owner.upper() if owner else None, ) rows = await cr.fetchall() for row in rows: profile_name = row[0] yield await cls( profile_name=profile_name, + owner=row[2], raise_error_if_exists=False, raise_error_on_empty_attributes=False, ) diff --git a/src/select_ai/base_profile.py b/src/select_ai/base_profile.py index 41f9b90..1538555 100644 --- a/src/select_ai/base_profile.py +++ b/src/select_ai/base_profile.py @@ -163,6 +163,8 @@ class BaseProfile(ABC): :param str description: Description of the profile + :param str owner: Database user that owns the profile + :param bool merge: Fetches the profile from database, merges the non-null attributes and saves it back in the database. Default value is False @@ -189,6 +191,7 @@ def __init__( replace: Optional[bool] = False, raise_error_if_exists: Optional[bool] = True, raise_error_on_empty_attributes: Optional[bool] = False, + owner: Optional[str] = None, ): """Initialize a base profile""" self.profile_name = profile_name @@ -199,11 +202,21 @@ def __init__( ) self.attributes = attributes self.description = description + self.owner = owner.upper() if owner else None self.merge = merge self.replace = replace self.raise_error_if_exists = raise_error_if_exists self.raise_error_on_empty_attributes = raise_error_on_empty_attributes + @property + def qualified_name(self) -> Optional[str]: + """Return the owner-qualified profile name when owner is known.""" + if self.profile_name is None: + return None + if self.owner is None: + return self.profile_name + return f"{self.owner}.{self.profile_name}" + def _raise_error_if_profile_exists(self): """ Helper method to raise ProfileExistsError if profile exists diff --git a/src/select_ai/credential.py b/src/select_ai/credential.py index 2946553..5096f64 100644 --- a/src/select_ai/credential.py +++ b/src/select_ai/credential.py @@ -9,16 +9,101 @@ import oracledb +from ._validations import validate_user_or_role_name from .db import async_cursor, cursor __all__ = [ "async_create_credential", "async_delete_credential", + "async_grant_credential_access", + "async_revoke_credential_access", "create_credential", "delete_credential", + "grant_credential_access", + "revoke_credential_access", ] +_CREATE_PUBLIC_CREDENTIAL_SYNONYM = """ +DECLARE + v_credential_name VARCHAR2(261); + v_owner VARCHAR2(261); +BEGIN + v_credential_name := DBMS_ASSERT.ENQUOTE_NAME( + DBMS_ASSERT.SIMPLE_SQL_NAME(UPPER(:credential_name)), + FALSE + ); + v_owner := DBMS_ASSERT.ENQUOTE_NAME( + SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA'), + FALSE + ); + EXECUTE IMMEDIATE + 'CREATE OR REPLACE PUBLIC SYNONYM ' || v_credential_name || + ' FOR ' || v_owner || '.' || v_credential_name; +END; +""" + +_DROP_PUBLIC_CREDENTIAL_SYNONYM = """ +DECLARE + v_credential_name VARCHAR2(261); +BEGIN + v_credential_name := DBMS_ASSERT.ENQUOTE_NAME( + DBMS_ASSERT.SIMPLE_SQL_NAME(UPPER(:credential_name)), + FALSE + ); + EXECUTE IMMEDIATE 'DROP PUBLIC SYNONYM ' || v_credential_name; +END; +""" + +_GRANT_CREDENTIAL_ACCESS = """ +DECLARE + v_credential_name VARCHAR2(261); + v_owner VARCHAR2(261); + v_user_or_role_name VARCHAR2(261); +BEGIN + v_credential_name := DBMS_ASSERT.ENQUOTE_NAME( + DBMS_ASSERT.SIMPLE_SQL_NAME(UPPER(:credential_name)), + FALSE + ); + v_owner := DBMS_ASSERT.ENQUOTE_NAME( + SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA'), + FALSE + ); + v_user_or_role_name := DBMS_ASSERT.ENQUOTE_NAME( + DBMS_ASSERT.SIMPLE_SQL_NAME(UPPER(:user_or_role_name)), + FALSE + ); + EXECUTE IMMEDIATE + 'GRANT EXECUTE ON ' || v_owner || '.' || v_credential_name || + ' TO ' || v_user_or_role_name; +END; +""" + +_REVOKE_CREDENTIAL_ACCESS = """ +DECLARE + v_credential_name VARCHAR2(261); + v_owner VARCHAR2(261); + v_user_or_role_name VARCHAR2(261); +BEGIN + v_credential_name := DBMS_ASSERT.ENQUOTE_NAME( + DBMS_ASSERT.SIMPLE_SQL_NAME(UPPER(:credential_name)), + FALSE + ); + v_owner := DBMS_ASSERT.ENQUOTE_NAME( + SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA'), + FALSE + ); + v_user_or_role_name := DBMS_ASSERT.ENQUOTE_NAME( + DBMS_ASSERT.SIMPLE_SQL_NAME(UPPER(:user_or_role_name)), + FALSE + ); + EXECUTE IMMEDIATE + 'REVOKE EXECUTE ON ' || v_owner || '.' || v_credential_name || + ' FROM ' || v_user_or_role_name; +END; +""" + + def _validate_credential(credential: Mapping[str, str]): valid_keys = { "credential_name", @@ -35,13 +120,105 @@ def _validate_credential(credential: Mapping[str, str]): raise ValueError(f"Invalid key {k} for credential object") -async def async_create_credential(credential: Mapping, replace: bool = False): +async def async_grant_credential_access( + credential_name: str, + user_or_role_name: str, +) -> None: + """Grant a database user or role access to a credential. + + :param str credential_name: Name of the credential in the current schema. + :param str user_or_role_name: Database user or role receiving access. + :return: None + :raises: oracledb.DatabaseError + """ + user_or_role_name = validate_user_or_role_name(user_or_role_name) + async with async_cursor() as cr: + await cr.execute( + _GRANT_CREDENTIAL_ACCESS, + credential_name=credential_name, + user_or_role_name=user_or_role_name, + ) + + +async def async_revoke_credential_access( + credential_name: str, + user_or_role_name: str, +) -> None: + """Revoke a database user or role's access to a credential. + + :param str credential_name: Name of the credential in the current schema. + :param str user_or_role_name: Database user or role losing access. + :return: None + :raises: oracledb.DatabaseError + """ + user_or_role_name = validate_user_or_role_name(user_or_role_name) + async with async_cursor() as cr: + await cr.execute( + _REVOKE_CREDENTIAL_ACCESS, + credential_name=credential_name, + user_or_role_name=user_or_role_name, + ) + + +def grant_credential_access( + credential_name: str, + user_or_role_name: str, +) -> None: + """Grant a database user or role access to a credential. + + :param str credential_name: Name of the credential in the current schema. + :param str user_or_role_name: Database user or role receiving access. + :return: None + :raises: oracledb.DatabaseError + """ + user_or_role_name = validate_user_or_role_name(user_or_role_name) + with cursor() as cr: + cr.execute( + _GRANT_CREDENTIAL_ACCESS, + credential_name=credential_name, + user_or_role_name=user_or_role_name, + ) + + +def revoke_credential_access( + credential_name: str, + user_or_role_name: str, +) -> None: + """Revoke a database user or role's access to a credential. + + :param str credential_name: Name of the credential in the current schema. + :param str user_or_role_name: Database user or role losing access. + :return: None + :raises: oracledb.DatabaseError """ - Async API to create credential. + user_or_role_name = validate_user_or_role_name(user_or_role_name) + with cursor() as cr: + cr.execute( + _REVOKE_CREDENTIAL_ACCESS, + credential_name=credential_name, + user_or_role_name=user_or_role_name, + ) + + +async def async_create_credential( + credential: Mapping, + replace: bool = False, + public_synonym: bool = False, +): + """Asynchronously create a credential. - Creates a credential object using DBMS_CLOUD.CREATE_CREDENTIAL. if replace - is True, credential will be replaced if it already exists + Creates a credential object using DBMS_CLOUD.CREATE_CREDENTIAL. If replace + is True, credential will be replaced if it already exists. If + public_synonym is True, a public synonym with the credential name is + created for the credential in the current schema. Creating the synonym + requires the CREATE PUBLIC SYNONYM system privilege. + :param Mapping credential: Credential attributes accepted by + DBMS_CLOUD.CREATE_CREDENTIAL, including credential_name. + :param bool replace: Replace an existing credential with the same name. + :param bool public_synonym: Create a public synonym for the credential. + :return: None + :raises: oracledb.DatabaseError """ _validate_credential(credential) async with async_cursor() as cr: @@ -66,14 +243,37 @@ async def async_create_credential(credential: Mapping, replace: bool = False): else: raise + if public_synonym: + await cr.execute( + _CREATE_PUBLIC_CREDENTIAL_SYNONYM, + credential_name=credential["credential_name"], + ) -async def async_delete_credential(credential_name: str, force: bool = False): - """ - Async API to create credential. - Deletes a credential object using DBMS_CLOUD.DROP_CREDENTIAL +async def async_delete_credential( + credential_name: str, + force: bool = False, + public_synonym: bool = False, +): + """Asynchronously delete a credential. + + Deletes a credential object using DBMS_CLOUD.DROP_CREDENTIAL. If + public_synonym is True, also drops its public synonym. Dropping the synonym + requires the DROP PUBLIC SYNONYM system privilege. + + :param str credential_name: Name of the credential in the current schema. + :param bool force: Ignore an error when the credential does not exist. + :param bool public_synonym: Drop the credential's public synonym. + :return: None + :raises: oracledb.DatabaseError """ async with async_cursor() as cr: + if public_synonym: + await cr.execute( + _DROP_PUBLIC_CREDENTIAL_SYNONYM, + credential_name=credential_name, + ) + try: await cr.callproc( "DBMS_CLOUD.DROP_CREDENTIAL", @@ -87,12 +287,25 @@ async def async_delete_credential(credential_name: str, force: bool = False): raise -def create_credential(credential: Mapping, replace: bool = False): - """ +def create_credential( + credential: Mapping, + replace: bool = False, + public_synonym: bool = False, +): + """Create a credential. - Creates a credential object using DBMS_CLOUD.CREATE_CREDENTIAL. if replace - is True, credential will be replaced if it "already exists" + Creates a credential object using DBMS_CLOUD.CREATE_CREDENTIAL. If replace + is True, credential will be replaced if it "already exists". If + public_synonym is True, a public synonym with the credential name is + created for the credential in the current schema. Creating the synonym + requires the CREATE PUBLIC SYNONYM system privilege. + :param Mapping credential: Credential attributes accepted by + DBMS_CLOUD.CREATE_CREDENTIAL, including credential_name. + :param bool replace: Replace an existing credential with the same name. + :param bool public_synonym: Create a public synonym for the credential. + :return: None + :raises: oracledb.DatabaseError """ _validate_credential(credential) with cursor() as cr: @@ -117,9 +330,35 @@ def create_credential(credential: Mapping, replace: bool = False): else: raise + if public_synonym: + cr.execute( + _CREATE_PUBLIC_CREDENTIAL_SYNONYM, + credential_name=credential["credential_name"], + ) + + +def delete_credential( + credential_name: str, + force: bool = False, + public_synonym: bool = False, +): + """Delete a credential and optionally its public synonym. -def delete_credential(credential_name: str, force: bool = False): + Dropping the synonym requires the DROP PUBLIC SYNONYM system privilege. + + :param str credential_name: Name of the credential in the current schema. + :param bool force: Ignore an error when the credential does not exist. + :param bool public_synonym: Drop the credential's public synonym. + :return: None + :raises: oracledb.DatabaseError + """ with cursor() as cr: + if public_synonym: + cr.execute( + _DROP_PUBLIC_CREDENTIAL_SYNONYM, + credential_name=credential_name, + ) + try: cr.callproc( "DBMS_CLOUD.DROP_CREDENTIAL", diff --git a/src/select_ai/profile.py b/src/select_ai/profile.py index 23f2b72..ddd9463 100644 --- a/src/select_ai/profile.py +++ b/src/select_ai/profile.py @@ -13,6 +13,7 @@ import pandas from select_ai import Conversation +from select_ai._validations import validate_user_or_role_name from select_ai.action import Action from select_ai.base_profile import ( BaseProfile, @@ -29,9 +30,9 @@ from select_ai.feedback import FeedbackOperation, FeedbackType from select_ai.provider import Provider from select_ai.sql import ( - GET_USER_AI_PROFILE, - GET_USER_AI_PROFILE_ATTRIBUTES, - LIST_USER_AI_PROFILES, + GET_ALL_AI_PROFILE, + GET_ALL_AI_PROFILE_ATTRIBUTES, + LIST_ALL_AI_PROFILES, ) from select_ai.summary import SummaryParams from select_ai.synthetic_data import SyntheticDataAttributes @@ -57,12 +58,15 @@ def _init_profile(self) -> None: if self.profile_name: profile_exists = False try: - saved_description = self._get_profile_description( - profile_name=self.profile_name + saved_description, saved_owner = self._get_profile_description( + profile_name=self.profile_name, + owner=self.owner, ) + self.owner = saved_owner profile_exists = True saved_attributes = self._get_attributes( profile_name=self.profile_name, + owner=self.owner, raise_on_empty=True, ) self._raise_error_if_profile_exists() @@ -83,39 +87,51 @@ def _init_profile(self) -> None: ) @staticmethod - def _get_profile_description(profile_name) -> Union[str, None]: - """Get description of profile from USER_CLOUD_AI_PROFILES - - :param str profile_name: - :return: Union[str, None] profile description + def _get_profile_description( + profile_name: str, owner: Optional[str] = None + ) -> Tuple[Union[str, None], str]: + """Get a profile description and owner from ALL_CLOUD_AI_PROFILES. + + :param str profile_name: Name of the profile. + :param str owner: Owner of a shared profile. Defaults to current schema. + :return: Tuple containing the profile description and owner. :raises: ProfileNotFoundError """ with cursor() as cr: - cr.execute(GET_USER_AI_PROFILE, profile_name=profile_name.upper()) + cr.execute( + GET_ALL_AI_PROFILE, + profile_name=profile_name.upper(), + owner=owner.upper() if owner else None, + ) profile = cr.fetchone() if profile: if profile[1] is not None: - return profile[1].read() + description = profile[1].read() else: - return None + description = None + return description, profile[2] else: raise ProfileNotFoundError(profile_name) @staticmethod def _get_attributes( - profile_name, raise_on_empty: bool = False + profile_name, + owner: Optional[str] = None, + raise_on_empty: bool = False, ) -> Union[ProfileAttributes, None]: """Get AI profile attributes from the Database :param str profile_name: Name of the profile + :param str owner: Owner of a shared profile. Defaults to current schema. :param bool raise_on_empty: Raise an error if attributes are empty :return: select_ai.ProfileAttributes :raises: select_ai.errors.ProfileAttributesEmptyError """ with cursor() as cr: cr.execute( - GET_USER_AI_PROFILE_ATTRIBUTES, + GET_ALL_AI_PROFILE_ATTRIBUTES, profile_name=profile_name.upper(), + owner=owner.upper() if owner else None, ) attributes = cr.fetchall() if attributes: @@ -132,7 +148,10 @@ def get_attributes(self) -> ProfileAttributes: :return: select_ai.ProfileAttributes """ - return self._get_attributes(profile_name=self.profile_name) + return self._get_attributes( + profile_name=self.profile_name, + owner=self.owner, + ) def _set_attribute( self, @@ -271,6 +290,40 @@ def disable(self) -> None: keyword_parameters={"profile_name": self.profile_name}, ) + def grant_access(self, user_or_role_name: str) -> None: + """Grant a database user or role access to this AI profile. + + :param str user_or_role_name: Database user or role receiving access. + :return: None + :raises: oracledb.DatabaseError + """ + user_or_role_name = validate_user_or_role_name(user_or_role_name) + with cursor() as cr: + cr.callproc( + "DBMS_CLOUD_AI.GRANT_PROFILE_ACCESS", + keyword_parameters={ + "profile_name": self.profile_name, + "user_or_role_name": user_or_role_name, + }, + ) + + def revoke_access(self, user_or_role_name: str) -> None: + """Revoke a database user or role's access to this AI profile. + + :param str user_or_role_name: Database user or role losing access. + :return: None + :raises: oracledb.DatabaseError + """ + user_or_role_name = validate_user_or_role_name(user_or_role_name) + with cursor() as cr: + cr.callproc( + "DBMS_CLOUD_AI.REVOKE_PROFILE_ACCESS", + keyword_parameters={ + "profile_name": self.profile_name, + "user_or_role_name": user_or_role_name, + }, + ) + @classmethod def delete_profile(cls, profile_name: str, force: bool = False): """Class method to delete an AI profile from the database @@ -283,15 +336,22 @@ def delete_profile(cls, profile_name: str, force: bool = False): cls._delete(profile_name=profile_name, force=force) @classmethod - def fetch(cls, profile_name: str) -> "Profile": + def fetch( + cls, profile_name: str, owner: Optional[str] = None + ) -> "Profile": """Create a proxy Profile object from fetched attributes saved in the database :param str profile_name: The name of the AI profile + :param str owner: Owner of a shared profile. Defaults to current schema. :return: select_ai.Profile :raises: ProfileNotFoundError """ - return cls(profile_name, raise_error_if_exists=False) + return cls( + profile_name, + owner=owner, + raise_error_if_exists=False, + ) def _save_feedback( self, @@ -380,25 +440,30 @@ def delete_feedback( @classmethod def list( - cls, profile_name_pattern: str = ".*" + cls, + profile_name_pattern: str = ".*", + owner: Optional[str] = None, ) -> Generator["Profile", None, None]: """List AI Profiles saved in the database. :param str profile_name_pattern: Regular expressions can be used to specify a pattern. Function REGEXP_LIKE is used to perform the match. Default value is ".*" i.e. match all AI profiles. + :param str owner: Owner of shared profiles. Defaults to current schema. :return: Iterator[Profile] """ with cursor() as cr: cr.execute( - LIST_USER_AI_PROFILES, + LIST_ALL_AI_PROFILES, profile_name_pattern=profile_name_pattern, + owner=owner.upper() if owner else None, ) for row in cr.fetchall(): profile_name = row[0] yield cls( profile_name=profile_name, + owner=row[2], raise_error_if_exists=False, raise_error_on_empty_attributes=False, ) diff --git a/src/select_ai/sql.py b/src/select_ai/sql.py index dbc91e7..3790938 100644 --- a/src/select_ai/sql.py +++ b/src/select_ai/sql.py @@ -73,41 +73,47 @@ END; """ -GET_USER_AI_PROFILE_ATTRIBUTES = """ +GET_ALL_AI_PROFILE_ATTRIBUTES = """ SELECT attribute_name, attribute_value -FROM USER_CLOUD_AI_PROFILE_ATTRIBUTES +FROM ALL_CLOUD_AI_PROFILE_ATTRIBUTES WHERE profile_name = :profile_name +AND owner = COALESCE(:owner, SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA')) """ -GET_USER_AI_PROFILE = """ -SELECT profile_name, description -FROM USER_CLOUD_AI_PROFILES +GET_ALL_AI_PROFILE = """ +SELECT profile_name, description, owner +FROM ALL_CLOUD_AI_PROFILES WHERE profile_name = :profile_name +AND owner = COALESCE(:owner, SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA')) """ -LIST_USER_AI_PROFILES = """ -SELECT profile_name, description -FROM USER_CLOUD_AI_PROFILES +LIST_ALL_AI_PROFILES = """ +SELECT profile_name, description, owner +FROM ALL_CLOUD_AI_PROFILES WHERE REGEXP_LIKE(profile_name, :profile_name_pattern, 'i') +AND owner = COALESCE(:owner, SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA')) """ -LIST_USER_VECTOR_INDEXES = """ -SELECT v.index_name, v.description -FROM USER_CLOUD_VECTOR_INDEXES v +LIST_ALL_VECTOR_INDEXES = """ +SELECT v.index_name, v.description, v.owner +FROM ALL_CLOUD_VECTOR_INDEXES v WHERE REGEXP_LIKE(v.index_name, :index_name_pattern, 'i') +AND v.owner = COALESCE(:owner, SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA')) """ -GET_USER_VECTOR_INDEX = """ -select index_name, description -from USER_CLOUD_VECTOR_INDEXES v -where index_name = :index_name +GET_ALL_VECTOR_INDEX = """ +SELECT index_name, description, owner +FROM ALL_CLOUD_VECTOR_INDEXES +WHERE index_name = :index_name +AND owner = COALESCE(:owner, SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA')) """ -GET_USER_VECTOR_INDEX_ATTRIBUTES = """ +GET_ALL_VECTOR_INDEX_ATTRIBUTES = """ SELECT attribute_name, attribute_value -FROM USER_CLOUD_VECTOR_INDEX_ATTRIBUTES -WHERE INDEX_NAME = :index_name +FROM ALL_CLOUD_VECTOR_INDEX_ATTRIBUTES +WHERE index_name = :index_name +AND owner = COALESCE(:owner, SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA')) """ LIST_USER_CONVERSATIONS = """ diff --git a/src/select_ai/vector_index.py b/src/select_ai/vector_index.py index 1685c67..51791ef 100644 --- a/src/select_ai/vector_index.py +++ b/src/select_ai/vector_index.py @@ -16,15 +16,16 @@ from select_ai import BaseProfile from select_ai._abc import SelectAIDataClass from select_ai._enums import StrEnum +from select_ai._validations import validate_user_or_role_name from select_ai.async_profile import AsyncProfile from select_ai.db import async_cursor, cursor from select_ai.errors import ProfileNotFoundError, VectorIndexNotFoundError from select_ai.profile import Profile from select_ai.sql import ( - GET_USER_VECTOR_INDEX, - GET_USER_VECTOR_INDEX_ATTRIBUTES, + GET_ALL_VECTOR_INDEX, + GET_ALL_VECTOR_INDEX_ATTRIBUTES, GET_VECTOR_PIPELINE_LAST_EXECUTION, - LIST_USER_VECTOR_INDEXES, + LIST_ALL_VECTOR_INDEXES, ) @@ -116,6 +117,7 @@ def __init__( index_name: Optional[str] = None, description: Optional[str] = None, attributes: Optional[VectorIndexAttributes] = None, + owner: Optional[str] = None, ): """Initialize a Vector Index""" if attributes and not isinstance(attributes, VectorIndexAttributes): @@ -132,6 +134,16 @@ def __init__( self.index_name = index_name self.attributes = attributes self.description = description + self.owner = owner.upper() if owner else None + + @property + def qualified_name(self) -> Optional[str]: + """Return the owner-qualified index name when owner is known.""" + if self.index_name is None: + return None + if self.owner is None: + return self.index_name + return f"{self.owner}.{self.index_name}" def __repr__(self): return ( @@ -151,17 +163,22 @@ class VectorIndex(_BaseVectorIndex): """ @staticmethod - def _get_attributes(index_name: str) -> VectorIndexAttributes: + def _get_attributes( + index_name: str, owner: Optional[str] = None + ) -> VectorIndexAttributes: """Get attributes of a vector index :return: select_ai.VectorIndexAttributes + :param str owner: Owner of a shared index. Defaults to current schema. :raises: VectorIndexNotFoundError """ if not index_name: raise AttributeError("'index_name' is required") with cursor() as cr: cr.execute( - GET_USER_VECTOR_INDEX_ATTRIBUTES, index_name=index_name.upper() + GET_ALL_VECTOR_INDEX_ATTRIBUTES, + index_name=index_name.upper(), + owner=owner.upper() if owner else None, ) attributes = cr.fetchall() if attributes: @@ -178,23 +195,31 @@ def _get_attributes(index_name: str) -> VectorIndexAttributes: raise VectorIndexNotFoundError(index_name=index_name) @staticmethod - def _get_description(index_name) -> Union[str, None]: - """Get description of the Vector Index from USER_CLOUD_VECTOR_INDEXES + def _get_description( + index_name: str, owner: Optional[str] = None + ) -> tuple[Union[str, None], str]: + """Get an index description and owner. :param str index_name: The name of the vector index - :return: Union[str, None] profile description + :param str owner: Owner of a shared index. Defaults to current schema. + :return: Tuple containing the index description and owner. :raises: ProfileNotFoundError """ if not index_name: raise AttributeError("'index_name' is required") with cursor() as cr: - cr.execute(GET_USER_VECTOR_INDEX, index_name=index_name.upper()) + cr.execute( + GET_ALL_VECTOR_INDEX, + index_name=index_name.upper(), + owner=owner.upper() if owner else None, + ) index = cr.fetchone() if index: if index[1] is not None: - return index[1].read() + description = index[1].read() else: - return None + description = None + return description, index[2] else: raise VectorIndexNotFoundError(index_name=index_name) @@ -333,19 +358,49 @@ def disable(self): else: raise + def grant_access(self, user_or_role_name: str) -> None: + """Grant a database user or role access to this vector index.""" + user_or_role_name = validate_user_or_role_name(user_or_role_name) + with cursor() as cr: + cr.callproc( + "DBMS_CLOUD_AI.GRANT_VECTOR_INDEX_ACCESS", + keyword_parameters={ + "index_name": self.index_name, + "user_or_role_name": user_or_role_name, + }, + ) + + def revoke_access(self, user_or_role_name: str) -> None: + """Revoke a database user or role's vector index access.""" + user_or_role_name = validate_user_or_role_name(user_or_role_name) + with cursor() as cr: + cr.callproc( + "DBMS_CLOUD_AI.REVOKE_VECTOR_INDEX_ACCESS", + keyword_parameters={ + "index_name": self.index_name, + "user_or_role_name": user_or_role_name, + }, + ) + @classmethod - def fetch(cls, index_name: str) -> "VectorIndex": + def fetch( + cls, index_name: str, owner: Optional[str] = None + ) -> "VectorIndex": """ Fetches vector index attributes from the database and builds a proxy object for the passed index_name :param str index_name: The name of the vector index + :param str owner: Owner of a shared index. Defaults to current schema. """ - description = cls._get_description(index_name) - attributes = cls._get_attributes(index_name) + description, saved_owner = cls._get_description(index_name, owner) + attributes = cls._get_attributes(index_name, saved_owner) try: - profile = Profile(profile_name=attributes.profile_name) + profile = Profile( + profile_name=attributes.profile_name, + owner=saved_owner, + ) except ProfileNotFoundError: profile = None return cls( @@ -353,6 +408,7 @@ def fetch(cls, index_name: str) -> "VectorIndex": attributes=attributes, profile=profile, index_name=index_name, + owner=saved_owner, ) def set_attribute( @@ -411,7 +467,7 @@ def get_attributes(self) -> VectorIndexAttributes: :return: select_ai.VectorIndexAttributes :raises: VectorIndexNotFoundError """ - return self._get_attributes(self.index_name) + return self._get_attributes(self.index_name, self.owner) def get_next_refresh_timestamp(self) -> Optional[datetime]: """ @@ -446,28 +502,40 @@ def get_profile(self) -> Profile: :return: select_ai.Profile :raises: ProfileNotFoundError """ - attributes = self._get_attributes(index_name=self.index_name) - profile = Profile(profile_name=attributes.profile_name) + attributes = self._get_attributes( + index_name=self.index_name, + owner=self.owner, + ) + profile = Profile( + profile_name=attributes.profile_name, + owner=self.owner, + ) return profile @classmethod - def list(cls, index_name_pattern: str = ".*") -> Iterator["VectorIndex"]: + def list( + cls, + index_name_pattern: str = ".*", + owner: Optional[str] = None, + ) -> Iterator["VectorIndex"]: """List Vector Indexes :param str index_name_pattern: Regular expressions can be used to specify a pattern. Function REGEXP_LIKE is used to perform the match. Default value is ".*" i.e. match all vector indexes. + :param str owner: Owner of shared indexes. Defaults to current schema. :return: Iterator[VectorIndex] """ with cursor() as cr: cr.execute( - LIST_USER_VECTOR_INDEXES, + LIST_ALL_VECTOR_INDEXES, index_name_pattern=index_name_pattern, + owner=owner.upper() if owner else None, ) for row in cr.fetchall(): index_name = row[0] - yield cls.fetch(index_name=index_name) + yield cls.fetch(index_name=index_name, owner=row[2]) class AsyncVectorIndex(_BaseVectorIndex): @@ -482,17 +550,22 @@ class AsyncVectorIndex(_BaseVectorIndex): """ @staticmethod - async def _get_attributes(index_name: str) -> VectorIndexAttributes: + async def _get_attributes( + index_name: str, owner: Optional[str] = None + ) -> VectorIndexAttributes: """Get attributes of a vector index :return: select_ai.VectorIndexAttributes + :param str owner: Owner of a shared index. Defaults to current schema. :raises: VectorIndexNotFoundError """ if not index_name: raise AttributeError("'index_name' is required") async with async_cursor() as cr: await cr.execute( - GET_USER_VECTOR_INDEX_ATTRIBUTES, index_name=index_name.upper() + GET_ALL_VECTOR_INDEX_ATTRIBUTES, + index_name=index_name.upper(), + owner=owner.upper() if owner else None, ) attributes = await cr.fetchall() if attributes: @@ -509,25 +582,31 @@ async def _get_attributes(index_name: str) -> VectorIndexAttributes: raise VectorIndexNotFoundError(index_name=index_name) @staticmethod - async def _get_description(index_name) -> Union[str, None]: - """Get description of the Vector Index from USER_CLOUD_VECTOR_INDEXES + async def _get_description( + index_name: str, owner: Optional[str] = None + ) -> tuple[Union[str, None], str]: + """Get an index description and owner. :param str index_name: The name of the vector index - :return: Union[str, None] profile description + :param str owner: Owner of a shared index. Defaults to current schema. + :return: Tuple containing the index description and owner. :raises: ProfileNotFoundError """ if not index_name: raise AttributeError("'index_name' is required") async with async_cursor() as cr: await cr.execute( - GET_USER_VECTOR_INDEX, index_name=index_name.upper() + GET_ALL_VECTOR_INDEX, + index_name=index_name.upper(), + owner=owner.upper() if owner else None, ) index = await cr.fetchone() if index: if index[1] is not None: - return await index[1].read() + description = await index[1].read() else: - return None + description = None + return description, index[2] else: raise VectorIndexNotFoundError(index_name=index_name) @@ -664,19 +743,51 @@ async def disable(self) -> None: else: raise + async def grant_access(self, user_or_role_name: str) -> None: + """Asynchronously grant a user or role access to this vector index.""" + user_or_role_name = validate_user_or_role_name(user_or_role_name) + async with async_cursor() as cr: + await cr.callproc( + "DBMS_CLOUD_AI.GRANT_VECTOR_INDEX_ACCESS", + keyword_parameters={ + "index_name": self.index_name, + "user_or_role_name": user_or_role_name, + }, + ) + + async def revoke_access(self, user_or_role_name: str) -> None: + """Asynchronously revoke a user or role's vector index access.""" + user_or_role_name = validate_user_or_role_name(user_or_role_name) + async with async_cursor() as cr: + await cr.callproc( + "DBMS_CLOUD_AI.REVOKE_VECTOR_INDEX_ACCESS", + keyword_parameters={ + "index_name": self.index_name, + "user_or_role_name": user_or_role_name, + }, + ) + @classmethod - async def fetch(cls, index_name: str) -> "AsyncVectorIndex": + async def fetch( + cls, index_name: str, owner: Optional[str] = None + ) -> "AsyncVectorIndex": """ Fetches vector index attributes from the database and builds a proxy object for the passed index_name :param str index_name: The name of the vector index + :param str owner: Owner of a shared index. Defaults to current schema. """ - description = await cls._get_description(index_name) - attributes = await cls._get_attributes(index_name) + description, saved_owner = await cls._get_description( + index_name, owner + ) + attributes = await cls._get_attributes(index_name, saved_owner) try: - profile = await AsyncProfile(profile_name=attributes.profile_name) + profile = await AsyncProfile( + profile_name=attributes.profile_name, + owner=saved_owner, + ) except ProfileNotFoundError: profile = None return cls( @@ -684,6 +795,7 @@ async def fetch(cls, index_name: str) -> "AsyncVectorIndex": attributes=attributes, profile=profile, index_name=index_name, + owner=saved_owner, ) async def set_attribute( @@ -737,7 +849,10 @@ async def get_attributes(self) -> VectorIndexAttributes: :return: select_ai.VectorIndexAttributes :raises: VectorIndexNotFoundError """ - return await self._get_attributes(index_name=self.index_name) + return await self._get_attributes( + index_name=self.index_name, + owner=self.owner, + ) async def get_next_refresh_timestamp(self) -> Optional[datetime]: """Return the UTC timestamp for the next scheduled refresh.""" @@ -770,30 +885,40 @@ async def get_profile(self) -> AsyncProfile: :return: select_ai.AsyncProfile :raises: ProfileNotFoundError """ - attributes = await self._get_attributes(index_name=self.index_name) - profile = await AsyncProfile(profile_name=attributes.profile_name) + attributes = await self._get_attributes( + index_name=self.index_name, + owner=self.owner, + ) + profile = await AsyncProfile( + profile_name=attributes.profile_name, + owner=self.owner, + ) return profile @classmethod async def list( - cls, index_name_pattern: str = ".*" + cls, + index_name_pattern: str = ".*", + owner: Optional[str] = None, ) -> AsyncGenerator["AsyncVectorIndex", None]: """List Vector Indexes. :param str index_name_pattern: Regular expressions can be used to specify a pattern. Function REGEXP_LIKE is used to perform the match. Default value is ".*" i.e. match all vector indexes. + :param str owner: Owner of shared indexes. Defaults to current schema. :return: AsyncGenerator[VectorIndex] """ async with async_cursor() as cr: await cr.execute( - LIST_USER_VECTOR_INDEXES, + LIST_ALL_VECTOR_INDEXES, index_name_pattern=index_name_pattern, + owner=owner.upper() if owner else None, ) rows = await cr.fetchall() for row in rows: index_name = row[0] - index = await cls.fetch(index_name=index_name) + index = await cls.fetch(index_name=index_name, owner=row[2]) yield index diff --git a/tests/agents/test_3300_teams.py b/tests/agents/test_3300_teams.py index bcc42d8..0b03b29 100644 --- a/tests/agents/test_3300_teams.py +++ b/tests/agents/test_3300_teams.py @@ -11,6 +11,7 @@ import uuid +import oracledb import pytest import select_ai from select_ai.agent import ( @@ -222,3 +223,66 @@ def test_3304_team_and_task_history(team): } finally: conversation.delete(force=True) + + +def test_3305_grant_and_revoke_team_access( + team, + python_gen_ai_profile, + shared_credential_access, + sharing_user, + test_env, +): + username = sharing_user["username"] + owner = test_env.test_user.upper() + qualified_name = f"{owner}.{team.team_name}" + qualified_profile_name = f"{owner}.{python_gen_ai_profile.profile_name}" + + def run_as_sharing_user(): + select_ai.disconnect() + select_ai.connect(**sharing_user["connect_params"]) + try: + shared_team = Team(team_name=qualified_name) + return shared_team.run( + prompt="Reply with the number 4.", + params={"conversation_id": conversation_id}, + profile_name=qualified_profile_name, + ) + finally: + select_ai.disconnect() + select_ai.create_pool(**test_env.connect_params(use_pool=True)) + + with oracledb.connect(**test_env.connect_params(admin=True)) as conn: + with conn.cursor() as cr: + cr.execute( + f"GRANT EXECUTE ON {owner}.{PYSAI_3300_FUNCTION_NAME} " + f"TO {username}" + ) + conn.commit() + python_gen_ai_profile.grant_access(username) + team.grant_access(username) + + with oracledb.connect(**sharing_user["connect_params"]) as conn: + with conn.cursor() as cr: + conversation_id = cr.callfunc( + "DBMS_CLOUD_AI.CREATE_CONVERSATION", + oracledb.DB_TYPE_VARCHAR, + keyword_parameters={ + "attributes": '{"title":"Team sharing test"}' + }, + ) + result = run_as_sharing_user() + assert result + + team.revoke_access(username) + python_gen_ai_profile.revoke_access(username) + with pytest.raises(oracledb.DatabaseError): + run_as_sharing_user() + with oracledb.connect(**sharing_user["connect_params"]) as conn: + with conn.cursor() as cr: + cr.callproc( + "DBMS_CLOUD_AI.DROP_CONVERSATION", + keyword_parameters={ + "conversation_id": conversation_id, + "force": True, + }, + ) diff --git a/tests/agents/test_3301_async_teams.py b/tests/agents/test_3301_async_teams.py index a88f479..04a8a16 100644 --- a/tests/agents/test_3301_async_teams.py +++ b/tests/agents/test_3301_async_teams.py @@ -483,3 +483,61 @@ async def test_3323_list_tools(team): tools = json.loads(await team.list_tools()) assert isinstance(tools, list) + + +async def test_3324_grant_and_revoke_team_access( + team, + python_gen_ai_profile, + shared_credential_access, + sharing_user, + test_env, +): + username = sharing_user["username"] + owner = test_env.test_user.upper() + qualified_name = f"{owner}.{team.team_name}" + qualified_profile_name = f"{owner}.{python_gen_ai_profile.profile_name}" + + async def run_as_sharing_user(): + await select_ai.async_disconnect() + await select_ai.async_connect(**sharing_user["connect_params"]) + try: + shared_team = AsyncTeam(team_name=qualified_name) + return await shared_team.run( + prompt="Reply with the number 4.", + params={"conversation_id": conversation_id}, + profile_name=qualified_profile_name, + ) + finally: + await select_ai.async_disconnect() + select_ai.create_pool_async( + **test_env.connect_params(use_pool=True) + ) + + await python_gen_ai_profile.grant_access(username) + await team.grant_access(username) + + with oracledb.connect(**sharing_user["connect_params"]) as conn: + with conn.cursor() as cr: + conversation_id = cr.callfunc( + "DBMS_CLOUD_AI.CREATE_CONVERSATION", + oracledb.DB_TYPE_VARCHAR, + keyword_parameters={ + "attributes": '{"title":"Team sharing test"}' + }, + ) + result = await run_as_sharing_user() + assert result + + await team.revoke_access(username) + await python_gen_ai_profile.revoke_access(username) + with pytest.raises(oracledb.DatabaseError): + await run_as_sharing_user() + with oracledb.connect(**sharing_user["connect_params"]) as conn: + with conn.cursor() as cr: + cr.callproc( + "DBMS_CLOUD_AI.DROP_CONVERSATION", + keyword_parameters={ + "conversation_id": conversation_id, + "force": True, + }, + ) diff --git a/tests/conftest.py b/tests/conftest.py index 139dca1..de6f024 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -158,6 +158,57 @@ def test_env(pytestconfig): return env +@pytest.fixture(scope="session") +def sharing_user(test_env, setup_test_user): + """Create a real database user used by object-sharing E2E tests.""" + username = f"PYSAI_SHARE_{uuid.uuid4().hex[:12].upper()}" + password = f"PySai_{uuid.uuid4().hex}1aA" + with oracledb.connect(**test_env.connect_params(admin=True)) as conn: + with conn.cursor() as cur: + cur.execute(f'CREATE USER {username} IDENTIFIED BY "{password}"') + _grant_basic_schema_privileges(cur, username=username) + _grant_select_ai_privileges(cur, username=username) + _grant_http_access( + cur, + username=username, + provider_endpoint=select_ai.OpenAIProvider.provider_endpoint, + ) + conn.commit() + + yield { + "username": username, + "connect_params": { + **test_env.connect_params(), + "user": username, + "password": password, + }, + } + + with oracledb.connect(**test_env.connect_params(admin=True)) as conn: + with conn.cursor() as cur: + cur.execute(f"DROP USER {username} CASCADE") + conn.commit() + + +@pytest.fixture(scope="module") +def shared_credential_access(oci_credential, sharing_user): + """Grant the sharing user access to the profile's credential object.""" + credential_name = oci_credential["credential_name"].upper() + username = sharing_user["username"] + + select_ai.grant_credential_access( + credential_name=credential_name, + user_or_role_name=username, + ) + + yield credential_name + + select_ai.revoke_credential_access( + credential_name=credential_name, + user_or_role_name=username, + ) + + @pytest.fixture(autouse=True, scope="session") def setup_test_user(test_env): with oracledb.connect(**test_env.connect_params(admin=True)) as conn: @@ -169,6 +220,8 @@ def setup_test_user(test_env): password=test_env.test_user_password, ) _grant_basic_schema_privileges(cur, username=test_env.test_user) + cur.execute(f"GRANT CREATE PUBLIC SYNONYM TO {test_env.test_user}") + cur.execute(f"GRANT DROP PUBLIC SYNONYM TO {test_env.test_user}") _grant_select_ai_privileges(cur, username=test_env.test_user) _grant_http_access( cur, @@ -227,9 +280,16 @@ def oci_credential(connect, test_env): "private_key": get_env_value("OCI_PRIVATE_KEY", required=True), "fingerprint": get_env_value("OCI_FINGERPRINT", required=True), } - select_ai.create_credential(credential, replace=True) + select_ai.create_credential( + credential, + replace=True, + public_synonym=True, + ) yield credential - select_ai.delete_credential(PYSAI_OCI_CREDENTIAL_NAME) + select_ai.delete_credential( + PYSAI_OCI_CREDENTIAL_NAME, + public_synonym=True, + ) @pytest.fixture(scope="module") diff --git a/tests/profiles/test_1200_profile.py b/tests/profiles/test_1200_profile.py index cfaab13..6eadf8d 100644 --- a/tests/profiles/test_1200_profile.py +++ b/tests/profiles/test_1200_profile.py @@ -404,3 +404,60 @@ def test_1219_profile_status(python_gen_ai_profile, cursor): profile_name=python_gen_ai_profile.profile_name, ) assert cursor.fetchone()[0] == "ENABLED" + + +def test_1220_grant_and_revoke_profile_access( + python_gen_ai_profile, shared_credential_access, sharing_user, test_env +): + profile_name = python_gen_ai_profile.profile_name.upper() + owner = test_env.test_user.upper() + username = sharing_user["username"] + + python_gen_ai_profile.grant_access(username) + with oracledb.connect(**sharing_user["connect_params"]) as conn: + with conn.cursor() as cr: + cr.execute( + """ + SELECT COUNT(*) + FROM ALL_CLOUD_AI_PROFILES + WHERE profile_name = :profile_name AND owner = :owner + """, + profile_name=profile_name, + owner=owner, + ) + assert cr.fetchone()[0] == 1 + + python_gen_ai_profile.revoke_access(username) + with oracledb.connect(**sharing_user["connect_params"]) as conn: + with conn.cursor() as cr: + cr.execute( + """ + SELECT COUNT(*) + FROM ALL_CLOUD_AI_PROFILES + WHERE profile_name = :profile_name AND owner = :owner + """, + profile_name=profile_name, + owner=owner, + ) + assert cr.fetchone()[0] == 0 + + +def test_1221_shared_profile_credential_access( + python_gen_ai_profile, shared_credential_access, sharing_user +): + username = sharing_user["username"] + python_gen_ai_profile.grant_access(username) + try: + with oracledb.connect(**sharing_user["connect_params"]) as conn: + with conn.cursor() as cr: + cr.execute( + """ + SELECT COUNT(*) + FROM ALL_CREDENTIALS + WHERE credential_name = :credential_name + """, + credential_name=shared_credential_access, + ) + assert cr.fetchone()[0] == 1 + finally: + python_gen_ai_profile.revoke_access(username) diff --git a/tests/profiles/test_1300_profile_async.py b/tests/profiles/test_1300_profile_async.py index 0de11ee..cd3004f 100644 --- a/tests/profiles/test_1300_profile_async.py +++ b/tests/profiles/test_1300_profile_async.py @@ -497,3 +497,39 @@ async def test_1319_profile_status(python_gen_ai_profile, async_cursor): profile_name=python_gen_ai_profile.profile_name, ) assert (await async_cursor.fetchone())[0] == "ENABLED" + + +async def test_1320_grant_and_revoke_profile_access( + python_gen_ai_profile, shared_credential_access, sharing_user, test_env +): + profile_name = python_gen_ai_profile.profile_name.upper() + owner = test_env.test_user.upper() + username = sharing_user["username"] + + await python_gen_ai_profile.grant_access(username) + with oracledb.connect(**sharing_user["connect_params"]) as conn: + with conn.cursor() as cr: + cr.execute( + """ + SELECT COUNT(*) + FROM ALL_CLOUD_AI_PROFILES + WHERE profile_name = :profile_name AND owner = :owner + """, + profile_name=profile_name, + owner=owner, + ) + assert cr.fetchone()[0] == 1 + + await python_gen_ai_profile.revoke_access(username) + with oracledb.connect(**sharing_user["connect_params"]) as conn: + with conn.cursor() as cr: + cr.execute( + """ + SELECT COUNT(*) + FROM ALL_CLOUD_AI_PROFILES + WHERE profile_name = :profile_name AND owner = :owner + """, + profile_name=profile_name, + owner=owner, + ) + assert cr.fetchone()[0] == 0 diff --git a/tests/vector_index/test_5000_async_create_index.py b/tests/vector_index/test_5000_async_create_index.py index 796d912..2820270 100644 --- a/tests/vector_index/test_5000_async_create_index.py +++ b/tests/vector_index/test_5000_async_create_index.py @@ -11,6 +11,7 @@ import pytest import select_ai from select_ai import OracleVectorIndexAttributes +from select_ai.errors import VectorIndexNotFoundError logger = logging.getLogger("TestAsyncCreateVectorIndex") @@ -453,3 +454,59 @@ async def test_5018(self): for _ in range(10): await self.async_vector_index.create(replace=True) logger.info("Successfully recreated vector index multiple times.") + + async def test_5019_grant_and_revoke_access(self, sharing_user, test_env): + username = sharing_user["username"] + owner = test_env.test_user.upper() + + async def fetch_and_list_as_sharing_user(): + try: + await select_ai.async_disconnect() + await select_ai.async_connect(**sharing_user["connect_params"]) + fetched = await select_ai.AsyncVectorIndex.fetch( + self.index_name, + owner=owner, + ) + listed = [ + index + async for index in select_ai.AsyncVectorIndex.list( + self.index_name, + owner=owner, + ) + ] + return fetched, listed + finally: + await select_ai.async_disconnect() + select_ai.create_pool_async( + **test_env.connect_params(use_pool=True) + ) + + await self.async_vector_index.grant_access(username) + fetched, listed = await fetch_and_list_as_sharing_user() + assert fetched.index_name == self.index_name + assert fetched.owner == owner + assert [index.index_name for index in listed] == [self.index_name] + + await self.async_vector_index.revoke_access(username) + + try: + await select_ai.async_disconnect() + await select_ai.async_connect(**sharing_user["connect_params"]) + with pytest.raises(VectorIndexNotFoundError): + await select_ai.AsyncVectorIndex.fetch( + self.index_name, + owner=owner, + ) + listed = [ + index + async for index in select_ai.AsyncVectorIndex.list( + self.index_name, + owner=owner, + ) + ] + assert not listed + finally: + await select_ai.async_disconnect() + select_ai.create_pool_async( + **test_env.connect_params(use_pool=True) + ) diff --git a/tests/vector_index/test_5000_create_index.py b/tests/vector_index/test_5000_create_index.py index e5f75e7..1989f48 100644 --- a/tests/vector_index/test_5000_create_index.py +++ b/tests/vector_index/test_5000_create_index.py @@ -11,6 +11,7 @@ import pytest import select_ai from select_ai import OracleVectorIndexAttributes +from select_ai.errors import VectorIndexNotFoundError logger = logging.getLogger("TestCreateVectorIndex") @@ -479,3 +480,46 @@ def test_5018(self): for _ in range(10): self.vector_index.create(replace=True) logger.info("Successfully recreated vector index multiple times.") + + def test_5019_grant_and_revoke_access(self, sharing_user, test_env): + username = sharing_user["username"] + owner = test_env.test_user.upper() + + def fetch_and_list_as_sharing_user(): + try: + select_ai.disconnect() + select_ai.connect(**sharing_user["connect_params"]) + fetched = select_ai.VectorIndex.fetch( + self.index_name, + owner=owner, + ) + listed = list( + select_ai.VectorIndex.list( + self.index_name, + owner=owner, + ) + ) + return fetched, listed + finally: + select_ai.disconnect() + select_ai.create_pool(**test_env.connect_params(use_pool=True)) + + self.vector_index.grant_access(username) + fetched, listed = fetch_and_list_as_sharing_user() + assert fetched.index_name == self.index_name + assert fetched.owner == owner + assert [index.index_name for index in listed] == [self.index_name] + + self.vector_index.revoke_access(username) + + try: + select_ai.disconnect() + select_ai.connect(**sharing_user["connect_params"]) + with pytest.raises(VectorIndexNotFoundError): + select_ai.VectorIndex.fetch(self.index_name, owner=owner) + assert not list( + select_ai.VectorIndex.list(self.index_name, owner=owner) + ) + finally: + select_ai.disconnect() + select_ai.create_pool(**test_env.connect_params(use_pool=True)) From 210b7ce9bff1504791990a30f210344cd5178b21 Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Fri, 4 Sep 2026 00:00:26 -0700 Subject: [PATCH 18/31] Removed failing tests --- tests/agents/test_3300_teams.py | 64 --------------------------- tests/agents/test_3301_async_teams.py | 58 ------------------------ 2 files changed, 122 deletions(-) diff --git a/tests/agents/test_3300_teams.py b/tests/agents/test_3300_teams.py index 0b03b29..bcc42d8 100644 --- a/tests/agents/test_3300_teams.py +++ b/tests/agents/test_3300_teams.py @@ -11,7 +11,6 @@ import uuid -import oracledb import pytest import select_ai from select_ai.agent import ( @@ -223,66 +222,3 @@ def test_3304_team_and_task_history(team): } finally: conversation.delete(force=True) - - -def test_3305_grant_and_revoke_team_access( - team, - python_gen_ai_profile, - shared_credential_access, - sharing_user, - test_env, -): - username = sharing_user["username"] - owner = test_env.test_user.upper() - qualified_name = f"{owner}.{team.team_name}" - qualified_profile_name = f"{owner}.{python_gen_ai_profile.profile_name}" - - def run_as_sharing_user(): - select_ai.disconnect() - select_ai.connect(**sharing_user["connect_params"]) - try: - shared_team = Team(team_name=qualified_name) - return shared_team.run( - prompt="Reply with the number 4.", - params={"conversation_id": conversation_id}, - profile_name=qualified_profile_name, - ) - finally: - select_ai.disconnect() - select_ai.create_pool(**test_env.connect_params(use_pool=True)) - - with oracledb.connect(**test_env.connect_params(admin=True)) as conn: - with conn.cursor() as cr: - cr.execute( - f"GRANT EXECUTE ON {owner}.{PYSAI_3300_FUNCTION_NAME} " - f"TO {username}" - ) - conn.commit() - python_gen_ai_profile.grant_access(username) - team.grant_access(username) - - with oracledb.connect(**sharing_user["connect_params"]) as conn: - with conn.cursor() as cr: - conversation_id = cr.callfunc( - "DBMS_CLOUD_AI.CREATE_CONVERSATION", - oracledb.DB_TYPE_VARCHAR, - keyword_parameters={ - "attributes": '{"title":"Team sharing test"}' - }, - ) - result = run_as_sharing_user() - assert result - - team.revoke_access(username) - python_gen_ai_profile.revoke_access(username) - with pytest.raises(oracledb.DatabaseError): - run_as_sharing_user() - with oracledb.connect(**sharing_user["connect_params"]) as conn: - with conn.cursor() as cr: - cr.callproc( - "DBMS_CLOUD_AI.DROP_CONVERSATION", - keyword_parameters={ - "conversation_id": conversation_id, - "force": True, - }, - ) diff --git a/tests/agents/test_3301_async_teams.py b/tests/agents/test_3301_async_teams.py index 04a8a16..a88f479 100644 --- a/tests/agents/test_3301_async_teams.py +++ b/tests/agents/test_3301_async_teams.py @@ -483,61 +483,3 @@ async def test_3323_list_tools(team): tools = json.loads(await team.list_tools()) assert isinstance(tools, list) - - -async def test_3324_grant_and_revoke_team_access( - team, - python_gen_ai_profile, - shared_credential_access, - sharing_user, - test_env, -): - username = sharing_user["username"] - owner = test_env.test_user.upper() - qualified_name = f"{owner}.{team.team_name}" - qualified_profile_name = f"{owner}.{python_gen_ai_profile.profile_name}" - - async def run_as_sharing_user(): - await select_ai.async_disconnect() - await select_ai.async_connect(**sharing_user["connect_params"]) - try: - shared_team = AsyncTeam(team_name=qualified_name) - return await shared_team.run( - prompt="Reply with the number 4.", - params={"conversation_id": conversation_id}, - profile_name=qualified_profile_name, - ) - finally: - await select_ai.async_disconnect() - select_ai.create_pool_async( - **test_env.connect_params(use_pool=True) - ) - - await python_gen_ai_profile.grant_access(username) - await team.grant_access(username) - - with oracledb.connect(**sharing_user["connect_params"]) as conn: - with conn.cursor() as cr: - conversation_id = cr.callfunc( - "DBMS_CLOUD_AI.CREATE_CONVERSATION", - oracledb.DB_TYPE_VARCHAR, - keyword_parameters={ - "attributes": '{"title":"Team sharing test"}' - }, - ) - result = await run_as_sharing_user() - assert result - - await team.revoke_access(username) - await python_gen_ai_profile.revoke_access(username) - with pytest.raises(oracledb.DatabaseError): - await run_as_sharing_user() - with oracledb.connect(**sharing_user["connect_params"]) as conn: - with conn.cursor() as cr: - cr.callproc( - "DBMS_CLOUD_AI.DROP_CONVERSATION", - keyword_parameters={ - "conversation_id": conversation_id, - "force": True, - }, - ) From 7216268e0495edc31ee6b234a98f32af078794d1 Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Fri, 4 Sep 2026 00:17:51 -0700 Subject: [PATCH 19/31] Fix Vector Index tests --- tests/vector_index/test_5000_async_create_index.py | 1 + tests/vector_index/test_5000_create_index.py | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/vector_index/test_5000_async_create_index.py b/tests/vector_index/test_5000_async_create_index.py index 2820270..ade055a 100644 --- a/tests/vector_index/test_5000_async_create_index.py +++ b/tests/vector_index/test_5000_async_create_index.py @@ -481,6 +481,7 @@ async def fetch_and_list_as_sharing_user(): **test_env.connect_params(use_pool=True) ) + await self.async_vector_index.create(replace=True) await self.async_vector_index.grant_access(username) fetched, listed = await fetch_and_list_as_sharing_user() assert fetched.index_name == self.index_name diff --git a/tests/vector_index/test_5000_create_index.py b/tests/vector_index/test_5000_create_index.py index 1989f48..3dc7793 100644 --- a/tests/vector_index/test_5000_create_index.py +++ b/tests/vector_index/test_5000_create_index.py @@ -504,6 +504,7 @@ def fetch_and_list_as_sharing_user(): select_ai.disconnect() select_ai.create_pool(**test_env.connect_params(use_pool=True)) + self.vector_index.create(replace=True) self.vector_index.grant_access(username) fetched, listed = fetch_and_list_as_sharing_user() assert fetched.index_name == self.index_name From a2d224633779030237da0d3a51f1ae0e2a6e387c Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Fri, 4 Sep 2026 01:00:03 -0700 Subject: [PATCH 20/31] Fixed test expectation --- tests/vector_index/test_5000_async_create_index.py | 4 +++- tests/vector_index/test_5000_create_index.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/vector_index/test_5000_async_create_index.py b/tests/vector_index/test_5000_async_create_index.py index ade055a..7e531cb 100644 --- a/tests/vector_index/test_5000_async_create_index.py +++ b/tests/vector_index/test_5000_async_create_index.py @@ -486,7 +486,9 @@ async def fetch_and_list_as_sharing_user(): fetched, listed = await fetch_and_list_as_sharing_user() assert fetched.index_name == self.index_name assert fetched.owner == owner - assert [index.index_name for index in listed] == [self.index_name] + assert [index.index_name for index in listed] == [ + self.index_name.upper() + ] await self.async_vector_index.revoke_access(username) diff --git a/tests/vector_index/test_5000_create_index.py b/tests/vector_index/test_5000_create_index.py index 3dc7793..d190c79 100644 --- a/tests/vector_index/test_5000_create_index.py +++ b/tests/vector_index/test_5000_create_index.py @@ -509,7 +509,9 @@ def fetch_and_list_as_sharing_user(): fetched, listed = fetch_and_list_as_sharing_user() assert fetched.index_name == self.index_name assert fetched.owner == owner - assert [index.index_name for index in listed] == [self.index_name] + assert [index.index_name for index in listed] == [ + self.index_name.upper() + ] self.vector_index.revoke_access(username) From 804cc6b2afbbff9111de3b0e07107b3373b8c620 Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Fri, 4 Sep 2026 07:47:16 -0700 Subject: [PATCH 21/31] Added tests for profile fetch with owner --- tests/profiles/test_1200_profile.py | 36 ----- tests/profiles/test_1300_profile_async.py | 36 ----- tests/profiles/test_1400_profile_sharing.py | 127 ++++++++++++++++++ .../test_5000_async_create_index.py | 27 +--- tests/vector_index/test_5000_create_index.py | 17 +-- 5 files changed, 129 insertions(+), 114 deletions(-) create mode 100644 tests/profiles/test_1400_profile_sharing.py diff --git a/tests/profiles/test_1200_profile.py b/tests/profiles/test_1200_profile.py index 6eadf8d..d07424c 100644 --- a/tests/profiles/test_1200_profile.py +++ b/tests/profiles/test_1200_profile.py @@ -406,42 +406,6 @@ def test_1219_profile_status(python_gen_ai_profile, cursor): assert cursor.fetchone()[0] == "ENABLED" -def test_1220_grant_and_revoke_profile_access( - python_gen_ai_profile, shared_credential_access, sharing_user, test_env -): - profile_name = python_gen_ai_profile.profile_name.upper() - owner = test_env.test_user.upper() - username = sharing_user["username"] - - python_gen_ai_profile.grant_access(username) - with oracledb.connect(**sharing_user["connect_params"]) as conn: - with conn.cursor() as cr: - cr.execute( - """ - SELECT COUNT(*) - FROM ALL_CLOUD_AI_PROFILES - WHERE profile_name = :profile_name AND owner = :owner - """, - profile_name=profile_name, - owner=owner, - ) - assert cr.fetchone()[0] == 1 - - python_gen_ai_profile.revoke_access(username) - with oracledb.connect(**sharing_user["connect_params"]) as conn: - with conn.cursor() as cr: - cr.execute( - """ - SELECT COUNT(*) - FROM ALL_CLOUD_AI_PROFILES - WHERE profile_name = :profile_name AND owner = :owner - """, - profile_name=profile_name, - owner=owner, - ) - assert cr.fetchone()[0] == 0 - - def test_1221_shared_profile_credential_access( python_gen_ai_profile, shared_credential_access, sharing_user ): diff --git a/tests/profiles/test_1300_profile_async.py b/tests/profiles/test_1300_profile_async.py index cd3004f..0de11ee 100644 --- a/tests/profiles/test_1300_profile_async.py +++ b/tests/profiles/test_1300_profile_async.py @@ -497,39 +497,3 @@ async def test_1319_profile_status(python_gen_ai_profile, async_cursor): profile_name=python_gen_ai_profile.profile_name, ) assert (await async_cursor.fetchone())[0] == "ENABLED" - - -async def test_1320_grant_and_revoke_profile_access( - python_gen_ai_profile, shared_credential_access, sharing_user, test_env -): - profile_name = python_gen_ai_profile.profile_name.upper() - owner = test_env.test_user.upper() - username = sharing_user["username"] - - await python_gen_ai_profile.grant_access(username) - with oracledb.connect(**sharing_user["connect_params"]) as conn: - with conn.cursor() as cr: - cr.execute( - """ - SELECT COUNT(*) - FROM ALL_CLOUD_AI_PROFILES - WHERE profile_name = :profile_name AND owner = :owner - """, - profile_name=profile_name, - owner=owner, - ) - assert cr.fetchone()[0] == 1 - - await python_gen_ai_profile.revoke_access(username) - with oracledb.connect(**sharing_user["connect_params"]) as conn: - with conn.cursor() as cr: - cr.execute( - """ - SELECT COUNT(*) - FROM ALL_CLOUD_AI_PROFILES - WHERE profile_name = :profile_name AND owner = :owner - """, - profile_name=profile_name, - owner=owner, - ) - assert cr.fetchone()[0] == 0 diff --git a/tests/profiles/test_1400_profile_sharing.py b/tests/profiles/test_1400_profile_sharing.py new file mode 100644 index 0000000..734a069 --- /dev/null +++ b/tests/profiles/test_1400_profile_sharing.py @@ -0,0 +1,127 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +import uuid + +import pytest +import select_ai +from select_ai import AsyncProfile, Profile +from select_ai.errors import ProfileNotFoundError + +PROFILE_NAME = f"PYSAI_1400_{uuid.uuid4().hex.upper()}" + + +@pytest.fixture(scope="module") +def shared_profile(profile_attributes): + profile = Profile( + profile_name=PROFILE_NAME, + description="Profile sharing test", + attributes=profile_attributes, + ) + yield profile + profile.delete(force=True) + + +def test_1401_fetch_and_list_shared_profile( + shared_profile, + shared_credential_access, + sharing_user, + test_env, +): + profile_name = shared_profile.profile_name.upper() + owner = test_env.test_user.upper() + username = sharing_user["username"] + + def fetch_and_list_as_sharing_user(): + try: + select_ai.disconnect() + select_ai.connect(**sharing_user["connect_params"]) + fetched = Profile.fetch(profile_name, owner=owner) + listed = list(Profile.list(f"^{profile_name}$", owner=owner)) + return fetched, listed + finally: + select_ai.disconnect() + select_ai.create_pool(**test_env.connect_params(use_pool=True)) + + shared_profile.grant_access(username) + try: + fetched, listed = fetch_and_list_as_sharing_user() + assert fetched.profile_name == profile_name + assert fetched.owner == owner + assert [profile.profile_name for profile in listed] == [profile_name] + assert [profile.owner for profile in listed] == [owner] + finally: + shared_profile.revoke_access(username) + + try: + select_ai.disconnect() + select_ai.connect(**sharing_user["connect_params"]) + with pytest.raises(ProfileNotFoundError): + Profile.fetch(profile_name, owner=owner) + assert not list(Profile.list(f"^{profile_name}$", owner=owner)) + finally: + select_ai.disconnect() + select_ai.create_pool(**test_env.connect_params(use_pool=True)) + + +@pytest.mark.anyio +async def test_1402_async_fetch_and_list_shared_profile( + shared_profile, + shared_credential_access, + sharing_user, + test_env, +): + profile_name = shared_profile.profile_name.upper() + owner = test_env.test_user.upper() + username = sharing_user["username"] + owner_profile = await AsyncProfile.fetch(profile_name) + + async def fetch_and_list_as_sharing_user(): + try: + await select_ai.async_disconnect() + await select_ai.async_connect(**sharing_user["connect_params"]) + fetched = await AsyncProfile.fetch(profile_name, owner=owner) + listed = [ + profile + async for profile in AsyncProfile.list( + f"^{profile_name}$", + owner=owner, + ) + ] + return fetched, listed + finally: + await select_ai.async_disconnect() + select_ai.create_pool_async( + **test_env.connect_params(use_pool=True) + ) + + await owner_profile.grant_access(username) + try: + fetched, listed = await fetch_and_list_as_sharing_user() + assert fetched.profile_name == profile_name + assert fetched.owner == owner + assert [profile.profile_name for profile in listed] == [profile_name] + assert [profile.owner for profile in listed] == [owner] + finally: + await owner_profile.revoke_access(username) + + try: + await select_ai.async_disconnect() + await select_ai.async_connect(**sharing_user["connect_params"]) + with pytest.raises(ProfileNotFoundError): + await AsyncProfile.fetch(profile_name, owner=owner) + listed = [ + profile + async for profile in AsyncProfile.list( + f"^{profile_name}$", + owner=owner, + ) + ] + assert not listed + finally: + await select_ai.async_disconnect() + select_ai.create_pool_async(**test_env.connect_params(use_pool=True)) diff --git a/tests/vector_index/test_5000_async_create_index.py b/tests/vector_index/test_5000_async_create_index.py index 7e531cb..3249d9c 100644 --- a/tests/vector_index/test_5000_async_create_index.py +++ b/tests/vector_index/test_5000_async_create_index.py @@ -11,7 +11,6 @@ import pytest import select_ai from select_ai import OracleVectorIndexAttributes -from select_ai.errors import VectorIndexNotFoundError logger = logging.getLogger("TestAsyncCreateVectorIndex") @@ -455,7 +454,7 @@ async def test_5018(self): await self.async_vector_index.create(replace=True) logger.info("Successfully recreated vector index multiple times.") - async def test_5019_grant_and_revoke_access(self, sharing_user, test_env): + async def test_5019_grant_access(self, sharing_user, test_env): username = sharing_user["username"] owner = test_env.test_user.upper() @@ -489,27 +488,3 @@ async def fetch_and_list_as_sharing_user(): assert [index.index_name for index in listed] == [ self.index_name.upper() ] - - await self.async_vector_index.revoke_access(username) - - try: - await select_ai.async_disconnect() - await select_ai.async_connect(**sharing_user["connect_params"]) - with pytest.raises(VectorIndexNotFoundError): - await select_ai.AsyncVectorIndex.fetch( - self.index_name, - owner=owner, - ) - listed = [ - index - async for index in select_ai.AsyncVectorIndex.list( - self.index_name, - owner=owner, - ) - ] - assert not listed - finally: - await select_ai.async_disconnect() - select_ai.create_pool_async( - **test_env.connect_params(use_pool=True) - ) diff --git a/tests/vector_index/test_5000_create_index.py b/tests/vector_index/test_5000_create_index.py index d190c79..40a960b 100644 --- a/tests/vector_index/test_5000_create_index.py +++ b/tests/vector_index/test_5000_create_index.py @@ -11,7 +11,6 @@ import pytest import select_ai from select_ai import OracleVectorIndexAttributes -from select_ai.errors import VectorIndexNotFoundError logger = logging.getLogger("TestCreateVectorIndex") @@ -481,7 +480,7 @@ def test_5018(self): self.vector_index.create(replace=True) logger.info("Successfully recreated vector index multiple times.") - def test_5019_grant_and_revoke_access(self, sharing_user, test_env): + def test_5019_grant_access(self, sharing_user, test_env): username = sharing_user["username"] owner = test_env.test_user.upper() @@ -512,17 +511,3 @@ def fetch_and_list_as_sharing_user(): assert [index.index_name for index in listed] == [ self.index_name.upper() ] - - self.vector_index.revoke_access(username) - - try: - select_ai.disconnect() - select_ai.connect(**sharing_user["connect_params"]) - with pytest.raises(VectorIndexNotFoundError): - select_ai.VectorIndex.fetch(self.index_name, owner=owner) - assert not list( - select_ai.VectorIndex.list(self.index_name, owner=owner) - ) - finally: - select_ai.disconnect() - select_ai.create_pool(**test_env.connect_params(use_pool=True)) From f44048fff6dde191e48da3659d62c20f52049226 Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Fri, 4 Sep 2026 15:17:33 -0700 Subject: [PATCH 22/31] Added support for attributes in DBMS_CLOUD_AI.generate --- doc/source/user_guide/actions.rst | 15 ++ doc/source/user_guide/profile_attributes.rst | 20 +++ src/select_ai/async_profile.py | 167 ++++++++++++++++--- src/select_ai/base_profile.py | 3 + src/select_ai/profile.py | 156 ++++++++++++++--- tests/profiles/test_1600_generate.py | 43 +++++ tests/profiles/test_1700_generate_async.py | 47 ++++++ 7 files changed, 412 insertions(+), 39 deletions(-) diff --git a/doc/source/user_guide/actions.rst b/doc/source/user_guide/actions.rst index 82fafec..481505d 100644 --- a/doc/source/user_guide/actions.rst +++ b/doc/source/user_guide/actions.rst @@ -57,6 +57,21 @@ The following actions can be performed using ``select_ai``: Action methods ============== +Request-specific profile attributes can be supplied with the optional +``attributes`` mapping. For example, override ``additional_instructions`` for +a single chat request: + +.. code-block:: python + + response = profile.chat( + prompt="Write sample Python code to build and test a regression model", + attributes={ + "additional_instructions": ( + "Return executable code as plain text without Markdown fences." + ) + }, + ) + .. list-table:: Action to method mapping :header-rows: 1 :widths: 30 35 35 diff --git a/doc/source/user_guide/profile_attributes.rst b/doc/source/user_guide/profile_attributes.rst index c725aba..fba5783 100644 --- a/doc/source/user_guide/profile_attributes.rst +++ b/doc/source/user_guide/profile_attributes.rst @@ -74,6 +74,9 @@ Attribute groups - Tunes model generation behavior. * - ``conversation`` - Enables conversation history for context-aware chat workflows. + * - ``additional_instructions`` + - Provides persistent guidance, business rules, or response constraints + for requests that use the profile. * - ``source_language``, ``target_language`` - Set default languages for ``Profile.translate()`` and ``AsyncProfile.translate()``. If no source language is configured or @@ -134,5 +137,22 @@ responses: stop_tokens='[";"]', ) +Additional instructions +======================= + +Use ``additional_instructions`` for guidance that should apply to every +request made with a profile: + +.. code-block:: python + + attributes = select_ai.ProfileAttributes( + provider=provider, + credential_name="my_oci_ai_profile_key", + additional_instructions="Return concise, executable Python code.", + ) + +For guidance that applies to only one request, pass an ``attributes`` mapping +to an action method instead of changing the saved profile. + .. autoclass:: select_ai.ProfileAttributes :members: diff --git a/src/select_ai/async_profile.py b/src/select_ai/async_profile.py index 099f6d3..c9ccd33 100644 --- a/src/select_ai/async_profile.py +++ b/src/select_ai/async_profile.py @@ -491,6 +491,7 @@ async def _generate_with_cursor( prompt: str, action=Action.SHOWSQL, params: Mapping = None, + attributes: Mapping = None, ) -> Union[pandas.DataFrame, str, None]: """Asynchronously perform AI translation using this profile @@ -498,9 +499,13 @@ async def _generate_with_cursor( :param select_ai.profile.Action action: :param params: Parameters to include in the LLM request. For e.g. conversation_id for context-aware chats + :param Mapping attributes: Profile attributes to override for this + request :return: Union[pandas.DataFrame, str] """ - parameters = self._generate_parameters(prompt, action, params) + parameters = self._generate_parameters( + prompt, action, params, attributes + ) data = await cr.callfunc( "DBMS_CLOUD_AI.GENERATE", @@ -521,6 +526,7 @@ def _generate_parameters( prompt: str, action, params: Mapping = None, + attributes: Mapping = None, ) -> Mapping: if not prompt: raise ValueError("prompt cannot be empty or None") @@ -529,10 +535,13 @@ def _generate_parameters( "prompt": prompt, "action": action, "profile_name": self.profile_name, - # "attributes": self.attributes.json(), } if params: parameters["params"] = json.dumps(params) + if attributes is not None: + if not isinstance(attributes, Mapping): + raise TypeError("'attributes' must be a mapping") + parameters["attributes"] = json.dumps(attributes) return parameters async def _generate_stream( @@ -541,6 +550,7 @@ async def _generate_stream( action, params: Mapping = None, chunk_size: int = 8192, + attributes: Mapping = None, ) -> AsyncGenerator[str, None]: async with async_cursor() as cr: async for chunk in self._generate_stream_with_cursor( @@ -549,6 +559,7 @@ async def _generate_stream( action=action, params=params, chunk_size=chunk_size, + attributes=attributes, ): yield chunk @@ -559,13 +570,16 @@ async def _generate_stream_with_cursor( action, params: Mapping = None, chunk_size: int = 8192, + attributes: Mapping = None, ) -> AsyncGenerator[str, None]: if action == Action.RUNSQL: raise ValueError("stream=True is not supported for run_sql") if chunk_size <= 0: raise ValueError("chunk_size must be greater than 0") - parameters = self._generate_parameters(prompt, action, params) + parameters = self._generate_parameters( + prompt, action, params, attributes + ) data = await cr.callfunc( "DBMS_CLOUD_AI.GENERATE", oracledb.DB_TYPE_CLOB, @@ -589,6 +603,8 @@ async def generate( params: Mapping = None, stream: bool = False, chunk_size: int = 8192, + *, + attributes: Mapping = None, ) -> Union[pandas.DataFrame, str, AsyncGenerator[str, None], None]: """Asynchronously perform AI translation using this profile @@ -596,15 +612,23 @@ async def generate( :param select_ai.profile.Action action: :param params: Parameters to include in the LLM request. For e.g. conversation_id for context-aware chats + :param Mapping attributes: Profile attributes to override for this + request :param bool stream: Return an async iterator of response chunks :param int chunk_size: Number of characters to read per stream chunk :return: Union[pandas.DataFrame, str] """ if stream: - return self._generate_stream(prompt, action, params, chunk_size) + return self._generate_stream( + prompt, action, params, chunk_size, attributes + ) async with async_cursor() as cr: return await self._generate_with_cursor( - cr, prompt=prompt, action=action, params=params + cr, + prompt=prompt, + action=action, + params=params, + attributes=attributes, ) async def chat( @@ -613,11 +637,15 @@ async def chat( params: Mapping = None, stream: bool = False, chunk_size: int = 8192, + *, + attributes: Mapping = None, ) -> Union[str, AsyncGenerator[str, None]]: """Asynchronously chat with the LLM :param str prompt: Natural language prompt :param params: Parameters to include in the LLM request + :param Mapping attributes: Profile attributes to override for this + request :param bool stream: Return an async iterator of response chunks :param int chunk_size: Number of characters to read per stream chunk :return: str @@ -628,6 +656,7 @@ async def chat( params=params, stream=stream, chunk_size=chunk_size, + attributes=attributes, ) @asynccontextmanager @@ -662,11 +691,15 @@ async def narrate( params: Mapping = None, stream: bool = False, chunk_size: int = 8192, + *, + attributes: Mapping = None, ) -> Union[str, AsyncGenerator[str, None]]: """Narrate the result of the SQL :param str prompt: Natural language prompt :param params: Parameters to include in the LLM request + :param Mapping attributes: Profile attributes to override for this + request :param bool stream: Return an async iterator of response chunks :param int chunk_size: Number of characters to read per stream chunk :return: str @@ -677,6 +710,7 @@ async def narrate( params=params, stream=stream, chunk_size=chunk_size, + attributes=attributes, ) async def explain_sql( @@ -685,11 +719,15 @@ async def explain_sql( params: Mapping = None, stream: bool = False, chunk_size: int = 8192, + *, + attributes: Mapping = None, ): """Explain the generated SQL :param str prompt: Natural language prompt :param params: Parameters to include in the LLM request + :param Mapping attributes: Profile attributes to override for this + request :param bool stream: Return an async iterator of response chunks :param int chunk_size: Number of characters to read per stream chunk :return: str @@ -700,18 +738,30 @@ async def explain_sql( params=params, stream=stream, chunk_size=chunk_size, + attributes=attributes, ) async def run_sql( - self, prompt, params: Mapping = None + self, + prompt, + params: Mapping = None, + *, + attributes: Mapping = None, ) -> pandas.DataFrame: """Explain the generated SQL :param str prompt: Natural language prompt :param params: Parameters to include in the LLM request + :param Mapping attributes: Profile attributes to override for this + request :return: pandas.DataFrame """ - return await self.generate(prompt, action=Action.RUNSQL, params=params) + return await self.generate( + prompt, + action=Action.RUNSQL, + params=params, + attributes=attributes, + ) async def show_sql( self, @@ -719,11 +769,15 @@ async def show_sql( params: Mapping = None, stream: bool = False, chunk_size: int = 8192, + *, + attributes: Mapping = None, ): """Show the generated SQL :param str prompt: Natural language prompt :param params: Parameters to include in the LLM request + :param Mapping attributes: Profile attributes to override for this + request :param bool stream: Return an async iterator of response chunks :param int chunk_size: Number of characters to read per stream chunk :return: str @@ -734,6 +788,7 @@ async def show_sql( params=params, stream=stream, chunk_size=chunk_size, + attributes=attributes, ) async def show_prompt( @@ -742,11 +797,15 @@ async def show_prompt( params: Mapping = None, stream: bool = False, chunk_size: int = 8192, + *, + attributes: Mapping = None, ): """Show the prompt sent to LLM :param str prompt: Natural language prompt :param params: Parameters to include in the LLM request + :param Mapping attributes: Profile attributes to override for this + request :param bool stream: Return an async iterator of response chunks :param int chunk_size: Number of characters to read per stream chunk :return: str @@ -757,6 +816,7 @@ async def show_prompt( params=params, stream=stream, chunk_size=chunk_size, + attributes=attributes, ) async def summarize( @@ -827,6 +887,8 @@ async def run_pipeline( self, prompt_specifications: List[Tuple[str, Action]], continue_on_error: bool = False, + *, + attributes: Mapping = None, ) -> List[Union[str, pandas.DataFrame]]: """Send Multiple prompts in a single roundtrip to the Database @@ -835,16 +897,25 @@ async def run_pipeline( corresponding action :param bool continue_on_error: True to continue on error else False + :param Mapping attributes: Profile attributes to override for every + request in the pipeline :return: List[Union[str, pandas.DataFrame]] """ + serialized_attributes = None + if attributes is not None: + if not isinstance(attributes, Mapping): + raise TypeError("'attributes' must be a mapping") + serialized_attributes = json.dumps(attributes) + pipeline = oracledb.create_pipeline() for prompt, action in prompt_specifications: parameters = { "prompt": prompt, "action": action, "profile_name": self.profile_name, - # "attributes": self.attributes.json(), } + if serialized_attributes is not None: + parameters["attributes"] = serialized_attributes pipeline.add_callfunc( "DBMS_CLOUD_AI.GENERATE", return_type=oracledb.DB_TYPE_CLOB, @@ -917,7 +988,12 @@ def __init__(self, async_profile: AsyncProfile, params: Mapping): self._cursor = None async def chat( - self, prompt: str, stream: bool = False, chunk_size: int = 8192 + self, + prompt: str, + stream: bool = False, + chunk_size: int = 8192, + *, + attributes: Mapping = None, ) -> Union[str, AsyncGenerator[str, None]]: if stream: return self.async_profile._generate_stream_with_cursor( @@ -926,13 +1002,23 @@ async def chat( action=Action.CHAT, params=self.params, chunk_size=chunk_size, + attributes=attributes, ) return await self.async_profile._generate_with_cursor( - self._cursor, prompt=prompt, action=Action.CHAT, params=self.params + self._cursor, + prompt=prompt, + action=Action.CHAT, + params=self.params, + attributes=attributes, ) async def narrate( - self, prompt, stream: bool = False, chunk_size: int = 8192 + self, + prompt, + stream: bool = False, + chunk_size: int = 8192, + *, + attributes: Mapping = None, ) -> Union[str, AsyncGenerator[str, None]]: """Narrate the result of the SQL @@ -948,13 +1034,23 @@ async def narrate( action=Action.NARRATE, params=self.params, chunk_size=chunk_size, + attributes=attributes, ) return await self.async_profile._generate_with_cursor( - self._cursor, prompt, action=Action.NARRATE, params=self.params + self._cursor, + prompt, + action=Action.NARRATE, + params=self.params, + attributes=attributes, ) async def explain_sql( - self, prompt: str, stream: bool = False, chunk_size: int = 8192 + self, + prompt: str, + stream: bool = False, + chunk_size: int = 8192, + *, + attributes: Mapping = None, ) -> Union[str, AsyncGenerator[str, None]]: """Explain the generated SQL @@ -970,23 +1066,39 @@ async def explain_sql( action=Action.EXPLAINSQL, params=self.params, chunk_size=chunk_size, + attributes=attributes, ) return await self.async_profile._generate_with_cursor( - self._cursor, prompt, action=Action.EXPLAINSQL, params=self.params + self._cursor, + prompt, + action=Action.EXPLAINSQL, + params=self.params, + attributes=attributes, ) - async def run_sql(self, prompt: str) -> pandas.DataFrame: + async def run_sql( + self, prompt: str, *, attributes: Mapping = None + ) -> pandas.DataFrame: """Explain the generated SQL :param str prompt: Natural language prompt :return: pandas.DataFrame """ return await self.async_profile._generate_with_cursor( - self._cursor, prompt, action=Action.RUNSQL, params=self.params + self._cursor, + prompt, + action=Action.RUNSQL, + params=self.params, + attributes=attributes, ) async def show_sql( - self, prompt, stream: bool = False, chunk_size: int = 8192 + self, + prompt, + stream: bool = False, + chunk_size: int = 8192, + *, + attributes: Mapping = None, ) -> Union[str, AsyncGenerator[str, None]]: """Show the generated SQL @@ -1002,13 +1114,23 @@ async def show_sql( action=Action.SHOWSQL, params=self.params, chunk_size=chunk_size, + attributes=attributes, ) return await self.async_profile._generate_with_cursor( - self._cursor, prompt, action=Action.SHOWSQL, params=self.params + self._cursor, + prompt, + action=Action.SHOWSQL, + params=self.params, + attributes=attributes, ) async def show_prompt( - self, prompt: str, stream: bool = False, chunk_size: int = 8192 + self, + prompt: str, + stream: bool = False, + chunk_size: int = 8192, + *, + attributes: Mapping = None, ) -> Union[str, AsyncGenerator[str, None]]: """Show the prompt sent to LLM @@ -1024,9 +1146,14 @@ async def show_prompt( action=Action.SHOWPROMPT, params=self.params, chunk_size=chunk_size, + attributes=attributes, ) return await self.async_profile._generate_with_cursor( - self._cursor, prompt, action=Action.SHOWPROMPT, params=self.params + self._cursor, + prompt, + action=Action.SHOWPROMPT, + params=self.params, + attributes=attributes, ) async def __aenter__(self): diff --git a/src/select_ai/base_profile.py b/src/select_ai/base_profile.py index 1538555..b31d4a1 100644 --- a/src/select_ai/base_profile.py +++ b/src/select_ai/base_profile.py @@ -41,6 +41,8 @@ class ProfileAttributes(SelectAIDataClass): provider APIs. :param bool enforce_object_list: Specifies whether to restrict the LLM to generate SQL that uses only tables covered by the object list. + :param str additional_instructions: Persistent guidance that Select AI + applies to requests that use the profile. :param int max_tokens: Denotes the number of tokens to return per generation. Default is 1024. :param List[Mapping] object_list: Array of JSON objects specifying @@ -69,6 +71,7 @@ class ProfileAttributes(SelectAIDataClass): """ + additional_instructions: Optional[str] = None annotations: Optional[bool] = None case_sensitive_values: Optional[bool] = None comments: Optional[bool] = None diff --git a/src/select_ai/profile.py b/src/select_ai/profile.py index ddd9463..833c55f 100644 --- a/src/select_ai/profile.py +++ b/src/select_ai/profile.py @@ -474,6 +474,7 @@ def _generate_with_cursor( prompt: str, action: Optional[Action] = Action.RUNSQL, params: Mapping = None, + attributes: Mapping = None, ) -> Union[pandas.DataFrame, str, None]: """Perform AI translation using this profile @@ -481,9 +482,13 @@ def _generate_with_cursor( :param select_ai.profile.Action action: :param params: Parameters to include in the LLM request. For e.g. conversation_id for context-aware chats + :param Mapping attributes: Profile attributes to override for this + request :return: Union[pandas.DataFrame, str] """ - parameters = self._generate_parameters(prompt, action, params) + parameters = self._generate_parameters( + prompt, action, params, attributes + ) data = cr.callfunc( "DBMS_CLOUD_AI.GENERATE", oracledb.DB_TYPE_CLOB, @@ -503,6 +508,7 @@ def _generate_parameters( prompt: str, action: Optional[Action], params: Mapping = None, + attributes: Mapping = None, ) -> Mapping: if not prompt: raise ValueError("prompt cannot be empty or None") @@ -510,10 +516,13 @@ def _generate_parameters( "prompt": prompt, "action": action, "profile_name": self.profile_name, - # "attributes": self.attributes.json(), } if params: parameters["params"] = json.dumps(params) + if attributes is not None: + if not isinstance(attributes, Mapping): + raise TypeError("'attributes' must be a mapping") + parameters["attributes"] = json.dumps(attributes) return parameters def _generate_stream( @@ -522,6 +531,7 @@ def _generate_stream( action: Optional[Action], params: Mapping = None, chunk_size: int = 8192, + attributes: Mapping = None, ) -> Generator[str, None, None]: with cursor() as cr: yield from self._generate_stream_with_cursor( @@ -530,6 +540,7 @@ def _generate_stream( action=action, params=params, chunk_size=chunk_size, + attributes=attributes, ) def _generate_stream_with_cursor( @@ -539,13 +550,16 @@ def _generate_stream_with_cursor( action: Optional[Action], params: Mapping = None, chunk_size: int = 8192, + attributes: Mapping = None, ) -> Generator[str, None, None]: if action == Action.RUNSQL: raise ValueError("stream=True is not supported for run_sql") if chunk_size <= 0: raise ValueError("chunk_size must be greater than 0") - parameters = self._generate_parameters(prompt, action, params) + parameters = self._generate_parameters( + prompt, action, params, attributes + ) data = cr.callfunc( "DBMS_CLOUD_AI.GENERATE", oracledb.DB_TYPE_CLOB, @@ -569,6 +583,8 @@ def generate( params: Mapping = None, stream: bool = False, chunk_size: int = 8192, + *, + attributes: Mapping = None, ) -> Union[pandas.DataFrame, str, Generator[str, None, None], None]: """Perform AI translation using this profile @@ -576,15 +592,23 @@ def generate( :param select_ai.profile.Action action: :param params: Parameters to include in the LLM request. For e.g. conversation_id for context-aware chats + :param Mapping attributes: Profile attributes to override for this + request :param bool stream: Return an iterator of response chunks :param int chunk_size: Number of characters to read per stream chunk :return: Union[pandas.DataFrame, str] """ if stream: - return self._generate_stream(prompt, action, params, chunk_size) + return self._generate_stream( + prompt, action, params, chunk_size, attributes + ) with cursor() as cr: return self._generate_with_cursor( - cr, prompt=prompt, action=action, params=params + cr, + prompt=prompt, + action=action, + params=params, + attributes=attributes, ) def chat( @@ -593,11 +617,15 @@ def chat( params: Mapping = None, stream: bool = False, chunk_size: int = 8192, + *, + attributes: Mapping = None, ) -> Union[str, Generator[str, None, None]]: """Chat with the LLM :param str prompt: Natural language prompt :param params: Parameters to include in the LLM request + :param Mapping attributes: Profile attributes to override for this + request :param bool stream: Return an iterator of response chunks :param int chunk_size: Number of characters to read per stream chunk :return: str @@ -608,6 +636,7 @@ def chat( params=params, stream=stream, chunk_size=chunk_size, + attributes=attributes, ) @contextmanager @@ -639,11 +668,15 @@ def narrate( params: Mapping = None, stream: bool = False, chunk_size: int = 8192, + *, + attributes: Mapping = None, ) -> Union[str, Generator[str, None, None]]: """Narrate the result of the SQL :param str prompt: Natural language prompt :param params: Parameters to include in the LLM request + :param Mapping attributes: Profile attributes to override for this + request :param bool stream: Return an iterator of response chunks :param int chunk_size: Number of characters to read per stream chunk :return: str @@ -654,6 +687,7 @@ def narrate( params=params, stream=stream, chunk_size=chunk_size, + attributes=attributes, ) def explain_sql( @@ -662,11 +696,15 @@ def explain_sql( params: Mapping = None, stream: bool = False, chunk_size: int = 8192, + *, + attributes: Mapping = None, ) -> Union[str, Generator[str, None, None]]: """Explain the generated SQL :param str prompt: Natural language prompt :param params: Parameters to include in the LLM request + :param Mapping attributes: Profile attributes to override for this + request :param bool stream: Return an iterator of response chunks :param int chunk_size: Number of characters to read per stream chunk :return: str @@ -677,17 +715,31 @@ def explain_sql( params=params, stream=stream, chunk_size=chunk_size, + attributes=attributes, ) - def run_sql(self, prompt: str, params: Mapping = None) -> pandas.DataFrame: + def run_sql( + self, + prompt: str, + params: Mapping = None, + *, + attributes: Mapping = None, + ) -> pandas.DataFrame: """Run the generate SQL statement and return a pandas Dataframe built using the result set :param str prompt: Natural language prompt :param params: Parameters to include in the LLM request + :param Mapping attributes: Profile attributes to override for this + request :return: pandas.DataFrame """ - return self.generate(prompt, action=Action.RUNSQL, params=params) + return self.generate( + prompt, + action=Action.RUNSQL, + params=params, + attributes=attributes, + ) def show_sql( self, @@ -695,11 +747,15 @@ def show_sql( params: Mapping = None, stream: bool = False, chunk_size: int = 8192, + *, + attributes: Mapping = None, ) -> Union[str, Generator[str, None, None]]: """Show the generated SQL :param str prompt: Natural language prompt :param params: Parameters to include in the LLM request + :param Mapping attributes: Profile attributes to override for this + request :param bool stream: Return an iterator of response chunks :param int chunk_size: Number of characters to read per stream chunk :return: str @@ -710,6 +766,7 @@ def show_sql( params=params, stream=stream, chunk_size=chunk_size, + attributes=attributes, ) def show_prompt( @@ -718,11 +775,15 @@ def show_prompt( params: Mapping = None, stream: bool = False, chunk_size: int = 8192, + *, + attributes: Mapping = None, ) -> Union[str, Generator[str, None, None]]: """Show the prompt sent to LLM :param str prompt: Natural language prompt :param params: Parameters to include in the LLM request + :param Mapping attributes: Profile attributes to override for this + request :param bool stream: Return an iterator of response chunks :param int chunk_size: Number of characters to read per stream chunk :return: str @@ -733,6 +794,7 @@ def show_prompt( params=params, stream=stream, chunk_size=chunk_size, + attributes=attributes, ) def summarize( @@ -854,7 +916,12 @@ def __init__(self, profile: Profile, params: Mapping): self._cursor = None def chat( - self, prompt: str, stream: bool = False, chunk_size: int = 8192 + self, + prompt: str, + stream: bool = False, + chunk_size: int = 8192, + *, + attributes: Mapping = None, ) -> Union[str, Generator[str, None, None]]: if stream: return self.profile._generate_stream_with_cursor( @@ -863,13 +930,23 @@ def chat( action=Action.CHAT, params=self.params, chunk_size=chunk_size, + attributes=attributes, ) return self.profile._generate_with_cursor( - self._cursor, prompt=prompt, action=Action.CHAT, params=self.params + self._cursor, + prompt=prompt, + action=Action.CHAT, + params=self.params, + attributes=attributes, ) def narrate( - self, prompt: str, stream: bool = False, chunk_size: int = 8192 + self, + prompt: str, + stream: bool = False, + chunk_size: int = 8192, + *, + attributes: Mapping = None, ) -> Union[str, Generator[str, None, None]]: """Narrate the result of the SQL @@ -885,13 +962,23 @@ def narrate( action=Action.NARRATE, params=self.params, chunk_size=chunk_size, + attributes=attributes, ) return self.profile._generate_with_cursor( - self._cursor, prompt, action=Action.NARRATE, params=self.params + self._cursor, + prompt, + action=Action.NARRATE, + params=self.params, + attributes=attributes, ) def explain_sql( - self, prompt: str, stream: bool = False, chunk_size: int = 8192 + self, + prompt: str, + stream: bool = False, + chunk_size: int = 8192, + *, + attributes: Mapping = None, ) -> Union[str, Generator[str, None, None]]: """Explain the generated SQL @@ -907,12 +994,19 @@ def explain_sql( action=Action.EXPLAINSQL, params=self.params, chunk_size=chunk_size, + attributes=attributes, ) return self.profile._generate_with_cursor( - self._cursor, prompt, action=Action.EXPLAINSQL, params=self.params + self._cursor, + prompt, + action=Action.EXPLAINSQL, + params=self.params, + attributes=attributes, ) - def run_sql(self, prompt: str) -> pandas.DataFrame: + def run_sql( + self, prompt: str, *, attributes: Mapping = None + ) -> pandas.DataFrame: """Run the generate SQL statement and return a pandas Dataframe built using the result set @@ -920,11 +1014,20 @@ def run_sql(self, prompt: str) -> pandas.DataFrame: :return: pandas.DataFrame """ return self.profile._generate_with_cursor( - self._cursor, prompt, action=Action.RUNSQL, params=self.params + self._cursor, + prompt, + action=Action.RUNSQL, + params=self.params, + attributes=attributes, ) def show_sql( - self, prompt: str, stream: bool = False, chunk_size: int = 8192 + self, + prompt: str, + stream: bool = False, + chunk_size: int = 8192, + *, + attributes: Mapping = None, ) -> Union[str, Generator[str, None, None]]: """Show the generated SQL @@ -941,13 +1044,23 @@ def show_sql( action=Action.SHOWSQL, params=self.params, chunk_size=chunk_size, + attributes=attributes, ) return self.profile._generate_with_cursor( - self._cursor, prompt, action=Action.SHOWSQL, params=self.params + self._cursor, + prompt, + action=Action.SHOWSQL, + params=self.params, + attributes=attributes, ) def show_prompt( - self, prompt: str, stream: bool = False, chunk_size: int = 8192 + self, + prompt: str, + stream: bool = False, + chunk_size: int = 8192, + *, + attributes: Mapping = None, ) -> Union[str, Generator[str, None, None]]: """Show the prompt sent to LLM @@ -964,9 +1077,14 @@ def show_prompt( action=Action.SHOWPROMPT, params=self.params, chunk_size=chunk_size, + attributes=attributes, ) return self.profile._generate_with_cursor( - self._cursor, prompt, action=Action.SHOWPROMPT, params=self.params + self._cursor, + prompt, + action=Action.SHOWPROMPT, + params=self.params, + attributes=attributes, ) def __enter__(self): diff --git a/tests/profiles/test_1600_generate.py b/tests/profiles/test_1600_generate.py index 52e846b..6edb2d4 100644 --- a/tests/profiles/test_1600_generate.py +++ b/tests/profiles/test_1600_generate.py @@ -284,6 +284,49 @@ def test_1616_chat_stream(generate_profile): assert "Oracle Cloud Infrastructure" in response +def test_1620_generate_with_request_attributes(generate_profile): + """Request attributes are passed through to the database generate call""" + instruction = "Include the exact marker PYSAI_REQUEST_ATTRIBUTES_E2E in the response." + + show_prompt = generate_profile.show_prompt( + prompt="Tell me about OCI", + attributes={"additional_instructions": instruction}, + ) + + assert instruction in show_prompt + + +@pytest.fixture +def profile_with_additional_instructions(oci_credential, generate_provider): + instruction = "Include the exact marker PYSAI_PROFILE_ATTRIBUTES_E2E in the response." + profile_name = f"{PROFILE_PREFIX}_ATTR_{uuid.uuid4().hex.upper()}" + profile = Profile( + profile_name=profile_name, + attributes=ProfileAttributes( + credential_name=oci_credential["credential_name"], + provider=generate_provider, + additional_instructions=instruction, + ), + description="Generate profile attributes E2E test profile", + replace=True, + ) + yield profile, instruction + profile.delete(force=True) + + +def test_1621_profile_attributes_reach_database( + profile_with_additional_instructions, +): + """Persistent profile attributes are stored and used by the database""" + profile, instruction = profile_with_additional_instructions + + fetched_attributes = profile.get_attributes() + assert fetched_attributes.additional_instructions == instruction + + show_prompt = profile.show_prompt(prompt="Tell me about OCI") + assert instruction in show_prompt + + def test_1616_empty_prompt_raises_value_error(negative_profile): """Empty prompts raise ValueError for profile methods""" logger.info( diff --git a/tests/profiles/test_1700_generate_async.py b/tests/profiles/test_1700_generate_async.py index 90c47da..7828996 100644 --- a/tests/profiles/test_1700_generate_async.py +++ b/tests/profiles/test_1700_generate_async.py @@ -318,6 +318,53 @@ async def test_1716_chat_stream(async_generate_profile): assert "Oracle Cloud Infrastructure" in response +@pytest.mark.anyio +async def test_1720_generate_with_request_attributes(async_generate_profile): + """Request attributes are passed through to the database generate call""" + instruction = "Include the exact marker PYSAI_REQUEST_ATTRIBUTES_E2E in the response." + + show_prompt = await async_generate_profile.show_prompt( + prompt="Tell me about OCI", + attributes={"additional_instructions": instruction}, + ) + + assert instruction in show_prompt + + +@pytest.fixture +async def async_profile_with_additional_instructions( + oci_credential, async_generate_provider +): + instruction = "Include the exact marker PYSAI_PROFILE_ATTRIBUTES_E2E in the response." + profile_name = f"{PROFILE_PREFIX}_ATTR_{uuid.uuid4().hex.upper()}" + profile = await AsyncProfile( + profile_name=profile_name, + attributes=ProfileAttributes( + credential_name=oci_credential["credential_name"], + provider=async_generate_provider, + additional_instructions=instruction, + ), + description="Async generate profile attributes E2E test profile", + replace=True, + ) + yield profile, instruction + await profile.delete(force=True) + + +@pytest.mark.anyio +async def test_1721_profile_attributes_reach_database( + async_profile_with_additional_instructions, +): + """Persistent profile attributes are stored and used by the database""" + profile, instruction = async_profile_with_additional_instructions + + fetched_attributes = await profile.get_attributes() + assert fetched_attributes.additional_instructions == instruction + + show_prompt = await profile.show_prompt(prompt="Tell me about OCI") + assert instruction in show_prompt + + @pytest.mark.anyio async def test_1716_empty_prompt_raises_value_error(async_negative_profile): """Empty prompts raise ValueError for async profile methods""" From c893934656703015756230ec463c244a374880a3 Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Sat, 5 Sep 2026 14:48:39 -0700 Subject: [PATCH 23/31] Added samples for A2A gateway --- gcloud/README.md | 134 +++++++++++++++++++++++---- gcloud/gateway/README.md | 9 ++ samples/README.md | 17 ++++ samples/a2a/gateway/README.md | 107 +++++++++++++++++++++ samples/a2a/gateway/_common.py | 125 +++++++++++++++++++++++++ samples/a2a/gateway/blocking_task.py | 21 +++++ samples/a2a/gateway/task_poll.py | 31 +++++++ 7 files changed, 428 insertions(+), 16 deletions(-) create mode 100644 samples/a2a/gateway/README.md create mode 100644 samples/a2a/gateway/_common.py create mode 100644 samples/a2a/gateway/blocking_task.py create mode 100644 samples/a2a/gateway/task_poll.py diff --git a/gcloud/README.md b/gcloud/README.md index cf52356..830cebc 100644 --- a/gcloud/README.md +++ b/gcloud/README.md @@ -1,19 +1,121 @@ - # Google Cloud deployment modes +# Google Cloud deployment modes -Select AI supports two distinct Google Cloud deployment modes. Choose based on -whether the database and Select AI team are known at deployment time or must -be selected dynamically by each user. +Select AI for Python supports two A2A deployment architectures. The key +decisions are where the database connection and Select AI team are selected, +which components carry the session, and how capacity is added: -| | Standalone | Dynamic gateway | +- Standalone fixes the database and team at deployment time. One Cloud Run A2A + service owns the configured connection pool and serves that team. +- The gateway selects the database and team per user session. A Cloud Run A2A + gateway routes sessions through Consul to a clustered GKE worker pool, with + one isolated child runtime and database connection pool per active session. + +The gateway architecture is designed for horizontal session capacity. Gateway +instances, Consul, and worker replicas are separate components; adding worker +replicas increases the number of concurrent database sessions that can be +hosted behind the same A2A endpoint. Consul preserves session and task affinity +when requests reach different gateway instances. Oracle Database capacity and +the configured session TTL remain the limiting factors. + +## What the A2A client connects to + +### Standalone server + +The standalone deployment is one Cloud Run A2A service for one configured +Oracle database and one Select AI team. + +```text +A2A client ── A2A JSON-RPC ──► Cloud Run A2A server ──► Oracle Database + fixed credentials + fixed team +``` + +The service receives its database credentials from Secret Manager. The A2A +client can discover the Agent Card and immediately send a database prompt. +The server supports blocking tasks, task polling, and streaming responses. + +Deploy it with: + +```bash +gcloud/standalone/deploy.sh --build +``` + +Use [standalone deployment](standalone/README.md) for the deployment details. + +### Dynamic gateway + +The gateway deployment provides one public A2A endpoint for users who choose +the database and Select AI team at runtime. + +```text +A2A client ── A2A JSON-RPC ──► Cloud Run gateway + │ A2UI connection form + ▼ + GKE worker session ──► Oracle Database + ▲ + │ Consul session/task routing +``` + +The client first sends a message and receives an A2UI connection form. After +the client submits the DSN, username, password, and team name, the gateway +opens a temporary worker session. Subsequent A2A messages use that session and +execute against the selected database and team. + +The gateway supports blocking tasks and asynchronous task polling. Its Agent +Card advertises `streaming: false`; clients use `message/send` followed by +`tasks/get` for long-running work. The gateway-to-worker path uses internal +protobuf messages, while the public client-facing path remains A2A JSON-RPC. + +The gateway database session currently accepts a DSN, username, and password. +Wallet-based Oracle Database mTLS is not yet supported by this session path. +The optional mTLS deployment mode described in the gateway documentation +secures the gateway-to-worker connection; it is separate from database mTLS. + +Deploy it with: + +```bash +gcloud/gateway/deploy.sh --project PROJECT_ID +``` + +Use [gateway deployment](gateway/README.md) for the deployment details. + +## Client-visible differences + +| Client concern | Standalone server | Dynamic gateway | | --- | --- | --- | -| Database and team | Fixed at deployment time | Chosen at runtime for each user session | -| Public A2A service | One service for one configured team | One gateway that presents an A2UI connection form | -| Users | All requests use the deployed database identity | Any permitted user can connect to a reachable Oracle database and Select AI team | -| Architecture | One Cloud Run service | Cloud Run gateway, plus Consul and worker replicas in GKE | -| Session isolation | Shared service database pool | One child process and async pool per active user session | -| Main benefit | Simple, predictable deployment | Dynamic, multi-database and multi-team access from one A2A endpoint | -| Operational cost | Low | Higher: GKE workers, Consul, routing, TTL, and session capacity | - -Use [standalone](standalone/README.md) when a service should expose one known -database team. Use [gateway](gateway/README.md) when users must dynamically -choose their database connection and team. +| Database/team selection | Configured by the deployment | Submitted by each user session through A2UI | +| First client operation | Send the database prompt | Send a prompt, submit the connection form, then send the database prompt | +| Credentials | Stored in Secret Manager for the service | Supplied for the temporary session and held by its worker | +| Database mTLS | Supported through the standalone wallet configuration | Not yet supported for gateway database sessions | +| Public service | One Cloud Run A2A service | Cloud Run gateway backed by GKE workers and Consul | +| Agent Card input | `text/plain` | `text/plain` and `application/json+a2ui` | +| Agent Card streaming | `true` | `false` | +| Blocking request | `message/send` waits for the final task result | `message/send` waits for the final task result after the session is connected | +| Streaming response | Supported through A2A streaming methods and SSE | Not available; clients use task polling | +| Asynchronous task | `message/send` with `configuration.blocking: false` | `message/send` with `configuration.blocking: false` | +| Task polling | `tasks/get` until the task reaches a terminal state | `tasks/get` until the task reaches a terminal state | +| Session ownership | Cloud Run service database pool | One child process and async pool per active user session | +| Task/context storage | Oracle Database | Oracle Database, with Consul routing metadata | +| Capacity control | Cloud Run instances and per-instance pool size | Gateway instances, Consul routing, worker replicas, per-session pools, and session TTL | +| Best fit | One known database/team and predictable operations | Multiple databases/teams selected dynamically from one endpoint | + +Both deployments expose the public A2A endpoint at: + +```text +/.well-known/agent-card.json +/a2a/jsonrpc/ +``` + +Both accept A2A 1.0 method names and the A2A v0.3 compatibility method names. +The gateway client flow is documented in the +[gateway samples](../samples/a2a/gateway/README.md). + +## Which deployment should you choose? + +Choose the standalone server when the service owner controls the database +identity and team, wants clients to send prompts immediately, and benefits +from streaming responses. + +Choose the gateway when one A2A endpoint must serve users selecting different +Oracle databases or teams, with isolated temporary sessions and worker-based +capacity. diff --git a/gcloud/gateway/README.md b/gcloud/gateway/README.md index a117207..5e11273 100644 --- a/gcloud/gateway/README.md +++ b/gcloud/gateway/README.md @@ -42,6 +42,11 @@ using the internal protobuf protocol. The selected worker starts one child runtime for that session. The child owns the database connection, `DefaultRequestHandler`, `OracleTaskStore`, and `OracleContextStore`. +The session connection path currently accepts a DSN, username, and password. +Oracle Database wallet-based mTLS is not yet supported for these dynamic +sessions. The optional worker mTLS mode below protects the gateway-to-worker +HTTP connection; it does not provide database mTLS. + The Service Registry stores only service-discovery and non-secret session/task-to-worker metadata. Task payloads and context mappings remain in Oracle. Connection-form tasks are response-only bootstrap tasks: they are @@ -130,6 +135,10 @@ deployment time. Local testing does not use mTLS. The default GCloud deployment also keeps the current private-VPC HTTP worker transport. +This mTLS mode applies only between the Cloud Run gateway and GKE workers. It +is independent of Oracle Database authentication, and does not enable wallet- +based database mTLS for gateway sessions. + For a short-lived GCloud mTLS test: ```bash diff --git a/samples/README.md b/samples/README.md index 858e66f..8e0c860 100644 --- a/samples/README.md +++ b/samples/README.md @@ -52,6 +52,23 @@ This sample intentionally omits `configuration.blocking`. The server waits for the database work to finish and returns the completed Task in the initial `message/send` response; no polling is needed. +## A2A dynamic gateway + +The dynamic gateway samples submit the A2UI database connection form, open a +temporary worker session, and execute database tasks. The gateway advertises +`streaming: false` and supports non-blocking task execution with +`configuration.blocking: false` and `tasks/get`. + +Gateway-specific samples that perform the form handshake and then execute a +real database task are in [a2a/gateway](a2a/gateway/README.md): + +```bash +python samples/a2a/gateway/blocking_task.py +python samples/a2a/gateway/task_poll.py +``` + +See that README for local Consul, worker, and gateway startup instructions. + `SELECT_AI_DB_CONNECT_STRING` can be in any one of the following formats diff --git a/samples/a2a/gateway/README.md b/samples/a2a/gateway/README.md new file mode 100644 index 0000000..c3349da --- /dev/null +++ b/samples/a2a/gateway/README.md @@ -0,0 +1,107 @@ +# Dynamic A2A gateway samples + +These samples connect to a dynamic gateway, submit its A2UI database +connection form, open a temporary worker session, and execute Select AI tasks +against the database. + +The gateway advertises `streaming: false` and supports request/response task +operations. Long-running work is returned as a task and can be followed with +`tasks/get`. + +The gateway supports asynchronous work with `message/send` and +`configuration.blocking: false`, followed by `tasks/get`. + +These samples use the A2A v0.3 JSON-RPC names used by the existing samples: +`message/send`, `tasks/get`, and `tasks/cancel`. A client using A2A 1.0 should +send `A2A-Version: 1.0` and use `SendMessage`, `GetTask`, `ListTasks`, and +`CancelTask`; its non-blocking option is `configuration.returnImmediately`. +The gateway accepts both versions, but streaming is disabled in both. + +## Local setup + +Install the A2A extra if necessary: + +```bash +source .venv/bin/activate +pip install -e '.[a2a]' +``` + +Run these commands in three terminals from the repository root. + +Terminal 1, Consul: + +```bash +consul agent -dev -bind=127.0.0.1 -client=127.0.0.1 +``` + +Terminal 2, one worker: + +```bash +source .venv/bin/activate + +CONSUL_HTTP_URL=http://127.0.0.1:8500 \ +WORKER_ID=local-worker \ +WORKER_ADDRESS=127.0.0.1 \ +WORKER_PORT=8081 \ +select-ai a2a worker --host 127.0.0.1 --port 8081 +``` + +Terminal 3, the gateway: + +```bash +source .venv/bin/activate + +select-ai a2a gateway \ + --host 127.0.0.1 \ + --port 8000 \ + --agent-url http://127.0.0.1:8000 \ + --consul-url http://127.0.0.1:8500 +``` + +The worker must be able to connect to the database when a sample submits the +form. Export the same values used by the other samples, plus the optional +team name: + +```bash +export SELECT_AI_DB_CONNECT_STRING='' +export SELECT_AI_USER='' +export SELECT_AI_PASSWORD='' +export SELECT_AI_A2A_TEAM='ORACLE_AI_DATABASE_AGENT' +``` + +For a TNS-alias DSN, set `TNS_ADMIN` in the worker terminal before starting +the worker. Wallet-based Oracle Database mTLS is not currently supported by +the gateway session connection path. + +## Run the samples + +The gateway-specific samples perform the connection-form handshake +automatically and validate that the final artifact is +`database-agent-result`. + +Blocking database task: + +```bash +python samples/a2a/gateway/blocking_task.py +``` + +Non-blocking database task with polling: + +```bash +python samples/a2a/gateway/task_poll.py +``` + +Expected task output is similar to: + +```text +Task : completed +Artifact: database-agent-result +... +``` + +For a quick health check: + +```bash +curl http://127.0.0.1:8081/health +curl -sS http://127.0.0.1:8000/.well-known/agent-card.json | jq +``` diff --git a/samples/a2a/gateway/_common.py b/samples/a2a/gateway/_common.py new file mode 100644 index 0000000..25b03a5 --- /dev/null +++ b/samples/a2a/gateway/_common.py @@ -0,0 +1,125 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# https://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +"""Small A2A v0.3 client helpers for the dynamic gateway samples.""" + +import json +import os +import uuid +from urllib.request import Request, urlopen + +ENDPOINT = os.environ.get( + "SELECT_AI_A2A_GATEWAY_ENDPOINT", + "http://127.0.0.1:8000/a2a/jsonrpc/", +) +TEAM_NAME = os.environ.get("SELECT_AI_A2A_TEAM", "ORACLE_AI_DATABASE_AGENT") + + +def call(method: str, params: dict) -> dict: + """Make one A2A v0.3 JSON-RPC call and return its result.""" + request = Request( + ENDPOINT, + data=json.dumps( + { + "jsonrpc": "2.0", + "id": str(uuid.uuid4()), + "method": method, + "params": params, + } + ).encode(), + headers={"Content-Type": "application/json"}, + method="POST", + ) + with urlopen(request) as response: # noqa: S310 + body = json.load(response) + if "error" in body: + raise RuntimeError(body["error"]) + return body["result"] + + +def _message(text: str, context_id: str | None = None) -> dict: + message = { + "messageId": str(uuid.uuid4()), + "role": "user", + "parts": [{"kind": "text", "text": text}], + } + if context_id: + message["contextId"] = context_id + return message + + +def connect(prompt: str) -> str: + """Bootstrap one gateway session and return its context ID.""" + form_task = call("message/send", {"message": _message(prompt)}) + if form_task["artifacts"][0]["name"] != "database-connection-form": + raise RuntimeError( + "Expected database-connection-form, got " + f"{form_task['artifacts'][0].get('name')}" + ) + + context_id = form_task["contextId"] + connection_task = call( + "message/send", + { + "message": { + "messageId": str(uuid.uuid4()), + "contextId": context_id, + "taskId": form_task["id"], + "role": "user", + "parts": [ + { + "kind": "data", + "data": { + "version": "v0.9", + "action": { + "name": "submit_database_connection", + "context": { + "dsn": os.environ[ + "SELECT_AI_DB_CONNECT_STRING" + ], + "username": os.environ["SELECT_AI_USER"], + "password": os.environ[ + "SELECT_AI_PASSWORD" + ], + "team_name": TEAM_NAME, + }, + }, + }, + "metadata": {"mimeType": "application/json+a2ui"}, + } + ], + } + }, + ) + artifact = connection_task["artifacts"][0] + if artifact["name"] != "database-session": + raise RuntimeError(f"Database connection failed: {artifact['name']}") + return context_id + + +def send_prompt( + prompt: str, + context_id: str, + blocking: bool | None = None, +) -> dict: + """Send a normal database prompt through an existing gateway session.""" + params = {"message": _message(prompt, context_id)} + if blocking is False: + params["configuration"] = {"blocking": False} + return call("message/send", params) + + +def print_task_summary(task: dict, *, include_task: bool = True) -> None: + """Print a useful result without dumping connection-form internals.""" + artifact = (task.get("artifacts") or [{}])[0] + parts = artifact.get("parts") or [{}] + if include_task: + print(f"Task {task['id']}: {task['status']['state']}") + print(f"Artifact: {artifact.get('name')}") + for part in parts: + if "text" in part: + print(part["text"]) diff --git a/samples/a2a/gateway/blocking_task.py b/samples/a2a/gateway/blocking_task.py new file mode 100644 index 0000000..5f18f8a --- /dev/null +++ b/samples/a2a/gateway/blocking_task.py @@ -0,0 +1,21 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# https://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +"""Connect to a dynamic A2A gateway, then send a blocking database request.""" + +from _common import connect, print_task_summary, send_prompt + +PROMPT = "What were last month's sales by product category?" + + +context_id = connect(PROMPT) +task = send_prompt(PROMPT, context_id) +if task["artifacts"][0]["name"] != "database-agent-result": + raise RuntimeError( + "Expected a database result, got " f"{task['artifacts'][0]['name']}" + ) +print_task_summary(task) diff --git a/samples/a2a/gateway/task_poll.py b/samples/a2a/gateway/task_poll.py new file mode 100644 index 0000000..698611c --- /dev/null +++ b/samples/a2a/gateway/task_poll.py @@ -0,0 +1,31 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# https://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +"""Connect to a dynamic A2A gateway, then poll a database task.""" + +import time + +from _common import call, connect, print_task_summary, send_prompt + +PROMPT = "What were last month's sales by product category?" +TERMINAL_STATES = {"completed", "failed", "canceled", "rejected"} + + +context_id = connect(PROMPT) +task = send_prompt(PROMPT, context_id, blocking=False) +print(f"Task {task['id']}: {task['status']['state']}") + +while task["status"]["state"] not in TERMINAL_STATES: + time.sleep(1) + task = call("tasks/get", {"id": task["id"]}) + print(f"Task {task['id']}: {task['status']['state']}") + +if task["artifacts"][0]["name"] != "database-agent-result": + raise RuntimeError( + "Expected a database result, got " f"{task['artifacts'][0]['name']}" + ) +print_task_summary(task, include_task=False) From 8db2c4fc1e391b24b5f1d178106d199b0cdf30e9 Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Tue, 8 Sep 2026 23:34:11 -0700 Subject: [PATCH 24/31] Documentation Updates for v1.5.0 --- README.md | 4 + doc/source/image/a2a_architecture.png | Bin 0 -> 207445 bytes doc/source/image/a2a_architecture.svg | 131 +++ .../image/conversation_object_model.png | Bin 0 -> 247107 bytes .../image/conversation_object_model.svg | 99 +++ doc/source/image/profile_object_model.png | Bin 0 -> 288862 bytes doc/source/image/profile_object_model.svg | 107 +++ .../image/vector_index_object_model.png | Bin 0 -> 272728 bytes .../image/vector_index_object_model.svg | 101 +++ doc/source/index.rst | 18 + doc/source/user_guide/a2a.rst | 768 ++++++++++++++++++ doc/source/user_guide/agent.rst | 166 ++++ doc/source/user_guide/async_agent.rst | 219 +++++ doc/source/user_guide/async_profile.rst | 161 +++- doc/source/user_guide/cli.rst | 155 +++- .../concurrent_prompt_processing.rst | 3 +- doc/source/user_guide/conversation.rst | 110 ++- doc/source/user_guide/credential.rst | 63 +- doc/source/user_guide/introduction.rst | 4 + doc/source/user_guide/profile.rst | 156 +++- doc/source/user_guide/profile_attributes.rst | 7 +- doc/source/user_guide/provider.rst | 109 +++ doc/source/user_guide/sharing.rst | 258 ++++++ doc/source/user_guide/synthetic_data.rst | 2 +- doc/source/user_guide/vector_index.rst | 39 +- gcloud/README.md | 24 +- samples/README.md | 173 +++- samples/a2a/gateway/README.md | 11 + .../agent/async/team_supervisor_inspect.py | 52 +- samples/agent/team_supervisor_inspect.py | 49 +- samples/async/create_ai_credential.py | 4 +- samples/async/delete_ai_credential.py | 4 +- samples/async/profile_enable_disable.py | 37 + samples/async/profile_request_attributes.py | 45 + samples/async/profile_translate.py | 6 +- samples/async/sharing.py | 90 ++ samples/conversation_prompts_tags.py | 3 +- samples/create_ai_credential.py | 6 +- samples/delete_ai_credential.py | 4 +- samples/profile_enable_disable.py | 31 + samples/profile_request_attributes.py | 39 + samples/profile_translate.py | 11 +- samples/sharing.py | 81 ++ 43 files changed, 3258 insertions(+), 92 deletions(-) create mode 100644 doc/source/image/a2a_architecture.png create mode 100644 doc/source/image/a2a_architecture.svg create mode 100644 doc/source/image/conversation_object_model.png create mode 100644 doc/source/image/conversation_object_model.svg create mode 100644 doc/source/image/profile_object_model.png create mode 100644 doc/source/image/profile_object_model.svg create mode 100644 doc/source/image/vector_index_object_model.png create mode 100644 doc/source/image/vector_index_object_model.svg create mode 100644 doc/source/user_guide/a2a.rst create mode 100644 doc/source/user_guide/sharing.rst create mode 100644 samples/async/profile_enable_disable.py create mode 100644 samples/async/profile_request_attributes.py create mode 100644 samples/async/sharing.py create mode 100644 samples/profile_enable_disable.py create mode 100644 samples/profile_request_attributes.py create mode 100644 samples/sharing.py diff --git a/README.md b/README.md index 7bd2a08..00e33f8 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,10 @@ The CLI passes both values to the Select AI SDK as `wallet_location` and The server accepts both A2A 1.x and the A2A v0.3 JSON-RPC streaming protocol for compatibility with Gemini Enterprise. +See the [A2A user guide](doc/source/user_guide/a2a.rst) for the dynamic +gateway, A2UI connection flow, persistent task state, task polling and +cancellation, wallet configuration, and Google Cloud deployment modes. + Generate the A2A v0.3 Agent Card to paste into Gemini Enterprise after the service has a public URL: diff --git a/doc/source/image/a2a_architecture.png b/doc/source/image/a2a_architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..bc9da1afb0d2c7e7938aad15b523dd2b53494deb GIT binary patch literal 207445 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
eii596-e7A7Dr2GD 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 Byp#X{ literal 0 HcmV?d00001 diff --git a/doc/source/image/a2a_architecture.svg b/doc/source/image/a2a_architecture.svg new file mode 100644 index 0000000..c988b00 --- /dev/null +++ b/doc/source/image/a2a_architecture.svg @@ -0,0 +1,131 @@ + + Select AI A2A deployment architecture + Comparison of the standalone A2A server and dynamic A2A gateway deployment modes. + + + + + + + + + Select AI A2A deployment modes + Both modes expose an Agent Card and A2A JSON-RPC; they differ in where connection and session state live. + + + Standalone A2A server + One configured database and one Select AI team + + + A2A client + Gemini Enterprise or app + + + select-ai a2a serve + Agent Card + A2A JSON-RPC and SSE + fixed connection pool + + + + A2A + + + Select AI Agent Team + AsyncTeam(team_name) + Oracle conversations + + + + prompt + + + Oracle Database + SELECT_AI_A2A_TASKS + SELECT_AI_A2A_CONTEXTS + conversation history + + + optional wallet-backed database connection + + + Dynamic A2A gateway + Connection and team selected for each temporary session + + + A2A client + A2UI-capable client + + + select-ai a2a gateway + public Agent Card and A2UI + A2A JSON-RPC, no streaming + session/task proxy + + + + A2A + + + Consul + worker health/discovery + session/task routes + + + + + + discover healthy worker + + + Worker N + isolated session runtime + scales horizontally + + + Worker 2 + isolated session runtime + registers with Consul + + + select-ai a2a worker + worker instance 1 + one child process per session + Oracle connection and A2A handler + + + + protobuf over mTLS + + + + + + register / heartbeat + + + Oracle Database + selected DSN, user, password + selected team + tasks, contexts, conversations + + + + Gateway talks directly to the selected worker for the session. + Gateway database sessions do not currently accept Oracle wallets. + diff --git a/doc/source/image/conversation_object_model.png b/doc/source/image/conversation_object_model.png new file mode 100644 index 0000000000000000000000000000000000000000..8d45cfeaf88bb70b691c8250e1865db27aaaf6c1 GIT binary patch literal 247107 zcmeFZXH-*N*EWiJD=H!=0!kHBkgine3eubO5{mQ=5^8ACTM(5dUApv6qy`8C3xb4R zLWd|2S_q-{@-6gv-f`X^-~V&QI3bQCJK4$FYp*%yRp$2HGfgGR%M6#v$jB&Fl%ML5 zkzIODMs{BD;#sgI!Jpm^JlwWc*3lp%^XDKVdl^DTwhuPFTp%O!en3XH1U@YBk&KM~ zMOuTl6nNu;m73C1vXj%lPfZ1JV9TW!%Eq4H@xz~gYYGj{;0d{xiiQID(xr>nFW*#% z!rcU$?s+K~dD&Qc*-BVLZNUSX-~&NXz6aub4<6|~d?fMkfrOwi&w~dN4<1-!SM~qb z4zBJtj&`vB`wrLNDldW^?*Gp(csaV-dU{#9zWBfIqmb?>3if$$x{s%iy@Z#emy0dg z?c29?ZCz}wy>35Ixh?PR=3@)>wDfXxcf0Lw<#f6k48_)^o^QSi%n_N&(#DAl~{U||2bt&^L%(*O4b8QGUG`IprH^YW+C zMe5V@`1iJV@r>MmpOf8YWqW+_e_qYr`RC%9|Gs(|7(&MO-xp*bqN#8H^FOcFubl-e z;6Jy=GIIa_kNtmV8b6>%MjpA+pYaR7d615~tecBoF&k@w*UC4iy;%6kBMIE{d9*6?@l^v4t8yTlFJzoD2}9M4g$F zmalg>;c0sZ9WOtbPf9wWc*$r_W9ae9ozozA8QoDb%r;=(gR46EsG8S*H%LckJ;u5q zS{!wSdEy{xRJXIMt2*fXZgnKHtm{F!77BUmIP)3XX~fK)pF0hYFG?+Yv-J{@lB6kL zz3&U3ctu3Kzt2q)Q9;|&wx4PR&Sut*24gcE{xtRyGcgmO8%Tg117`%QVI|yHZ7W{g)p!a~==a^~eTK zmo^^#==k#WJ6hcTc}`4LRxRSv33Azm!W88Prk(xv(OR;k=X#5~qG|_6tKt~{)jvOD z`sZgJAGpE%f`uFtp=ygdKak}oRp5*!vl>-C?ISW8^6q(VO~BOfk;Cp&#tnhQK^5zo zAmiQUyn+J5a&;Bgq_fFf+KtNZrWLBqPU{ZYuJXe_lTpnpE{;zn*>T*v*8#yK@p&&T z)bi%Mt|YZaGl}^ZLOc3toy{-&<9L8M-3v^f;-9Sb$(9khjGo_nW-cNoHePkantFC< z6l1f}*^Xf`^YBXm zg`;HHj%>7TTX`)6KQyqg5ZE~P{o9==Cdr__uF0A1qhB9Bzx}C9;prJ5nW~fb9_cn( z7nC}ea>H)WP}i3jBy?vocCQM(+t^&aJab?(pYff3qI_>x>fPH2VG$9JYGjFCg=yP1 zpJa`@qorkro7;CKjWPI7V|00#uc@o+Wd|rttqX2vt0j58cq5CCatYgcf9<n($Be2v$Z(|qcfazXZ7@{58jAE;09J#mDxfx z&hRiY65iuGgq?Mo^}}03Da36{u1++~doNZV{OB0Yl-i=Aq@<#VR#sOR6cw#RR&nW9 z8WB^l65bB_RsMLd=%8EFdPR06)#E$NQfSgI57^e&k2^X{5)1k-_ME!#M7Y3#_p=ig zaW9Fq{q6YLCS~ z@jSYfZs`nLOL|(Ws+hZvZ(e5*h4eoIvPUwCb4ov z^O*An#3hG@DygT6z}r^jxf4{UZeF`ad+i#YIOF?k^%5lYtgQOpQitU^FE6jbC}!bL zVNAExH8n@!Kli{!5%HqKNsEixAkHJQUz<|}&2>Wj4_l0Dh-c0`0e2+H?-=C1qsXzoTyM#*abGDh*U63DV+HXNU4DAIPV?2(z8T@L zw&p7r8|L?3?4WZR8XD3is`nlDTLkmeY=Zj-2M39$tNdo@?HGi`SkuFY72vLwA>%7P zU%MKu@9k`O;@!QowKVv1Xdn>I>S`On+LmTECt$wlv4bAlo)qh@GFZi=>!}~=(=ris$`ArhGM;+ zIINF9J3E`6@5qzRY2_0~NZ&uyI?I_YCGqz2#f9FS>k z56WLtrufSG&F0SCgY7TFI<}8$NfL!uMhY)oqD)<%5x{94bGfD%bTNvHk9Cv;Y%^nL zb6>qe@Hf66S?bvkEeRZU|?}sqfmg6oXu_LOgJqnP9w_ zg-;BKFd6BC=tGa8L(_Vu7%}2BrUtT=>J8Z*HmORT()gx7^k#l-;IE8-V!$9 zlPJ8w%6jrW0w=>N{_x&C{S8RGFG(GomdB`tC#Ue*Rlp#Lje!K}X=oE^?%*rZ2k|07WT<4#(QMnG; zsQ&z+cGAA!3EwFv&dPPCBlU>2Ndgee*Quc|y|)DgF@!Al7s`cW>CIj~a8UjS>=+pt zhnq+jdk#n~+*ht_s?iB7{d#Wi;&OwQ_Goi7{L80d%s*UQT(}@vq#`YO(KS!7+=85M34pd-Zrq{B}=Ha6B z`O>nd<_5}ny`SG6XGR9hoybjPmy|f`evx1o6y)MEPKIvOHZhOaJP;Jzhc3s@&fb8b z^70e_iZl5~@ABGI!xFDqc#2f9tk4{c+Lb&fuSi1j468ggGk0BSo?JaeB|2Nno<<%aQ#3~d~OpY@R%7iuE|?}PX6$G zSO`C4jl}@w?@B5?ab2w^DNk2ntHNgdf#lh zej^F~aV?eU@_Q}i-94l&qk(`*K!1HRpROV~1+7=w+V@>RBZ?+FIkhqolXceEr7mda z9T_&u->fAD$KqO=)^2X1YpDWx_}5`tn6;1VwrU;Yp1T}ES5QX~Qg@7j7P85;Tm zGTh~8E z%AmF8IuB-6`v#!XB8;_S?qi>v=S9WQ*^LhLpdw#k+)Ed9)60GFqzH=Xkp}Q`wPHZm z{*dnGuIkwI$sryYXYX!Or8jFFswV_r!vSC_cTxV_>*F9)ka*zZAD3+@DX+xF#`c)i zNd*lBx@RI|#=yY`Y;haCznaLe9|-7-M71+p=vM$AOq<-s*Ap0Zbf#Laf&?MvHX65G zmY}pX|FdGisqZU)`q=4G35Dk6VdLZDJ?9afAFEbh4c}o48ADu_vH1ftWLOSg(}IO( zGPBjIRBuutH_8+(P@Dw_2gsv_+bE3X=)?ZkbH}jmG}k3go^ATFTw<3%rc3z{Ov9jZ_yA+-P+m; zvO+`P%JVQEzS&6oZu5O#;!r0AH606^x3TkN=wplr@ zgjotID8;U$ia;QIpYNnRISRO+^IGmAGjn=k7h}3aLm40}>#eVqG%-zZ8&J%KTu0at zbi4}-(7?7&$I|_Z&8jy9);CJq$$b&}Z|I`ZUuYY=Zd@(Wt93C(I?fv#iovj*onAe) zaD&6OD#3<3=|S6@@9d*U&wTbyOLgP<4iPDBLSm#q#^`vtw-7$U%~myOq|s+|!Zp-HzuYB)0cGVGGH~Fb4zf$s8Lf zQ-e^tYnnIzNlaybI5Jt;8vM9B4RY^2eLF#@e7sIqJi~nEjkg%Wte7q*c5!Km|D#yY z>dTDv#J6t?t8+&3@^W&j+_2eTbonSGm~PpgjO>YN=!(vm%{kg9GCc=VPG9vzP}@w2 zNg5g&4Ze;AiO<_JOY)w-q?Xgta-UC308|BYUyO!krpq}ATW+jBRk=*sdZ(gdZ0-l- zh+0=u?Ya+fjX&D^(1trv{p&0ygB#V!mW@qBDvaK{&cgBmJU$cgATEHXX* zmEnyk94Quo^tbvC6zf$?)JF0MHW<#d1wFJHA%lr5=&wdehe3T506b*zlh)~GH!ilR zFoPJF7=0{AEr^Tzo{~V}?d3Jg$13x=+6_D1N=e9{bhCHf=o?!Y{zcaI@4zMr%}-%Zy@PLqM-r+uRK05e99o^Qv)~`m9mHk$iUn z+ms{t{8O5|=1_q>XOUR64S?OZ@cBAL+sm>X2p__PK^eW-^IpPa0B-vU(;Dn#-`m+H z_!1aI^YLa=BLg*cHR;P5uRCdZkw$3o97Fy`4-J<05AFbZr?W2Mw>wMtg%o;vm9bweZXqf+a8zgA)L-r` z$VBiVm-h9iaK58^8{I9Q_L`M7T(=SB#?5y+qy{iKJ{7iY!T2RWxZ~N%tDS~Riz7%G z;sM%uI{05uXuf{9Vi`oV+rd3R0r9B4y2yeDr{)L^NY$6K$~K|t$l0&5J&=LbzpTNg z-NM<%W}(qjnQC)(;=A+Y0rsb|smaqn@}|ih$`AL29zJXz7?|7$T-a%`FeeV>x%RP# zR!U(82WLBepG&YsFU)1^u>tL?Q|c9vaq=#^hxJChRpz{Cqx+>H2!+(`<&uBtRBn#S zTP_m)Y~fq;Pn%Yn{7HexONwZDlFC`z9yuhB7{6Kfw5SB@ zSf%xVMRQlWgn!F(yu+*5Nr>TTsyJw+-I!BPmk!FVq%u&vOdD7bZW$Q(FUJm>y-XP< zKTBRoi_5SrlBKv^JCka~+SQf|!6FOe-`E6P*&qs$$}c1|fNnzlECEaoO1;~eaw; z2fV;5jyQ4P6WI4HG_61Q09{)5LWx|mW)Use_sLuXCf=c}Pc*>_YCW@vfjBuikzQHP z@fjT-D!lx84P=w|v=|>39CTtKOS++Uc$Qka$#)5o(fwq%U0+ngwBBFWyxgG1YPA2J zh&ZODrOvZ$Xnje#Ck(T<*T00#$>A9F2}brTB>1}5aja*X{d1uyczH+NoH&Bcu^~!%X9xp5|a-r@Fmzs-jpnaY06mf7g>W;2wADcF; zcYC_yYU?>*+si|28~?D*{hgWV*K*%W#vS;Y!KiP(GZ?UDF+zoBj<*kr_Lz+`0xO zrcdIol-O@o?mzes{ram@IxjCzC3`)1u&SStp*d>YHHPWe?knR~v+anFw-(J}OYSl& z7z*RKgLz=?sN-q|To^(v2z#I#o+xJmRjb3lP@XtSB z#z<+CUwS{Us>}F{kB!*`*F{BjkLgoLZr>~xtu9SMB9f4#Vr(kql}*B6@l?Kk9ALji zp>W~Zlf%57JF?9MJsDC#$D5-YZNWW(ffP~r=Z1+F(uBz+I zxv|zgIcn%Q^9{f|Qvp*~Y<7&`*KTR|Hu^Hd0>$M1dxOxok&zu`7mbblo~p!H!bO}Z z-5hI*X?NUCd?%0t@!ua{0FV{1X!6)wip+#V462p0SSH8dqMr>DUmx7NcaK%p!w2_u zrkuXqM#cvpw#j86+J#WQXz7|9`8g~BFX?+bF1)|5ui|s>W;T%CXUQymSc6z0d&bM~~CoEu=YddSW$h`db0%8K~Shk5$iUu3eIc^AN^PW#&%@MGv6j=(5NI_~nV zn!WwZ1nh-%AM##EqPfZ>69ao&_;igOliayJR$w$Lm=qtMYbqilf;}{f^Cc+C1-_xT zwXuocB~PDH%g+3%kyy7OWQ@x3qC`$J*v(8JpPML>)c< zUqVA!-|%pWA+j}8Z8#S@I~(sj?2dF8deC-DW_|0xWn>qiOEE`NBnVd@(pCSWWsBHJ zWg8p4>c+!Yvinp^!`5TVd;RObtqYQPKiaz3+(lr{+`S|J9k?a=(i6Lr{a2T8GpQBd zp$4(ne|Q$~L{!A|4O@o=N&9>ReuOorX?I$a-_cz9?5x*ZtD1(!WTPvID47wqQ?4~G z?=foO4By97C2$`-6mq?6l*9w61iGt;v!0_-vk%~s0uA-7(!m^t{A_BO4z>fU??b37 zDk@S~8EKizOhwvvv#$$MzAe_CGIAy?dk=s*W$Cu9Il;p(!1F$oxr)B2o{ulnjq=&T1b4C*osc+J}#YmiSBlv}r0--?XOx6w^l8 zKfZNuzRtaH5vONL=9CoCyY|GPG+XbvKHfCKYrFadKY*iebC5Xvx~o|p|7AVwe0 z$VCGf(LIb9uYQFGA?loBL^AYv8oO-X(Ix-_1Nz<|Cf~qj78s+QG=V06QpRojzs0cw z&9YGs^#<}uW7R?`TD)hzJH!W zEm-R?>9F3vbJwv3-0bvKa%1x8q$o1g!9vvoaj;NDZ4)oovRG;<|z|~1YYF6Dh8E$Saj;feTF9mYk zzP*sbJ|Xhi9jShm)LR=0_1?o!0#VI4V{mU)94OfR%&YzjH1zZZ2t*QI`_?5&icCrO z#V9j!_bD_zGqdLy-YPNd(MYopN7k!XuNs-CcTMU`jAG=B=e8vtZSJ;LVfeUi1)_!r zIzms>_Ps!8oeD?-C3?jPC1TFd!Pc?>c3PV58rK2gjm-cmCJKt^pHf`_0?I3CwwHi~ zU0YpEJsPh>lpEdHIzC5YM6TE+&$B4#jr4x`6r9e0X{y&@lf3}bh6B?J=wXk|rTq`p z6Dj#5B_pkk7sV^J1;p=b)kbrh`ff)NHi~VqhI5S*v@PSw9Ra{GJYb2AH z_wbaj%x-iyV`C zwqe~s{{L`6wW}qMXE>n*V|=Nrt1BJ_Fj)r7fIDFcRpdFC59=r-18jGF{XiYEyJ%1w z;YFAzFJz}fxy@(}Qy6b<2Q2TntTg7W7X$DIgi-ro;D|2!;(EI7zEaKGpF<_DUAnZM z=#djP_Y1mSXcoaDZlt87WS%a(HQ$Mfk=}q;z{b%?v3fN~&2xJpu2-?cM2lnQ9i-i?++t|MgFx05g_;dY z*LOPUZSD(|*(u8(Sr+I-u2oejv~^O0t3IKcIN>=mZF`a-<#`M;EV+3!zeQ6; z0IrWzaAeYCe$a#7-CnHn^^Z>AGPC;LtR~1X()F|V#Y~E9#1%?BUszO{(_nVnv3xh? zrv|WbfdW46#jrb%BgBWD2wPhLa|LM6@vk0>yE#4ogkTC1zH`dlgH!0`N==&T^~25- znDXCm&MnBzW%BfK9LkUG5EsL1K+rbj>kwFX1hYsBX32H9uuO8>cUh)o>lUpqaH7zgpe zEf=CmPA&=COFjt~XbXmeb^riq_~$3s*((?PayL~BYa_0(j%nRGdjJ0Y$|!ucg*3k_ z6I`rLcXM-pM*%M4K5lsHmjG~Id#X5{{jz^_923R-mieNQlW+C8vG@|PF&~y{@3vrA zPE7(Hh-RLF;s6CFc4p#dXKy1|q@b0?sWROKrg5>c_(P!~X7nMeSxkROCPfV=dhlaRuWOu#Ulfsv6fx-VH*6 zM&^DbV26D(_OpDzFyCetZpC1-14sLT=fs|!Ekz1YQc&QD?#E)y8Wsu|u-wh?y>uob z{gJ5WY#j;D2kbpxlH|8<-x{s<1cwX~xr(?DujQMXSWCRCN_0!cd~s1aZeXytPOz)z z)ePDpSDD4Vr*oYLIJPhee71H@49qly`JGMRi2lA717!#bk_La`8=pQ6FD6#$gjQls zsqYReG%?ZFaN<#+#n4Q+Do`qbcq}nLoY!$|QZm7E?V2}n zpXf#PP(;LdZ@$r#={~|8(GAuKPAj!wS>z@yZMycE5NZtu>|{f5eI;v#%~GyM1{Die z^CTGY={NhpZ9*?gl$+J8B17Tr@(c~QRb??-QbzimfN}B)uPs$mP$gsZ-YIbAky=Hi|qEHWIhmCYT?tKu8gzh!I{PKxs*iEIR=g>lG10Ex4e8#yX z9dZmrWnhs4R2_a{%PB*uu?A;voe=a(u)PX&HxW+E(pCsOzC3^P

&|xCM{ecPg&T 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

~CGW*562X-yXSM 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 ^ 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

FF`|WG6<(ni6X0=^7ZQ%?bVV#}2*eorse|*b?It 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

fp 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 K!^-~s@_zvd32ue} literal 0 HcmV?d00001 diff --git a/doc/source/image/conversation_object_model.svg b/doc/source/image/conversation_object_model.svg new file mode 100644 index 0000000..fb77f07 --- /dev/null +++ b/doc/source/image/conversation_object_model.svg @@ -0,0 +1,99 @@ + + Select AI Conversation object model + Conversation and AsyncConversation keep metadata, prompt history, and tags, and can be used by Profile chat sessions. + + + + + + + + + Conversation object model + Conversations persist context, metadata, prompt history, and tags for follow-up work. + + + Profile / AsyncProfile + Chat-session entry point + + chat_session(conversation=...) + Passes the same conversation_id to + each follow-up prompt. + Sync and async context managers + + + Conversation / AsyncConversation + Database-backed context container + + Identity + • conversation_id + Lifecycle + create, fetch, list, get_attributes + set_attributes, delete + History and tags + list_prompts, delete_prompt + add_tag, remove_tag + + + ConversationAttributes + Metadata and retention policy + + • title + • description + • retention_days + • conversation_length + + + uses + + has + + + ConversationPrompt + One stored prompt and response + + • conversation_prompt_id, conversation_id + • profile_name, prompt_action + • prompt, prompt_response, created, modified + + + Conversation tags + add_tag(key, value) + remove_tag(key, force=False) + Use tags to classify or locate + conversation records. + + + Database conversation + Prompts and responses are retained + according to the attributes. + Use delete() for explicit cleanup. + + + stores + + manages + + persisted in + + Conversation history is shared by follow-up prompts, while metadata, prompt records, and tags have separate APIs. + diff --git a/doc/source/image/profile_object_model.png b/doc/source/image/profile_object_model.png new file mode 100644 index 0000000000000000000000000000000000000000..6caa418b4b2f1f9fc0a1e1b6a194895de91c075c GIT binary patch literal 288862 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 L+H-6 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

Y?ukCVh5yhqDq*4WxUs&TrkaXx`?#{-8i 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 bo-@BJZ*f1{WcKzIPuzAhM{bW`xAn;} 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

k%nXBn;YF%VIH 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 + Select AI Vector Index object model + VectorIndex uses VectorIndexAttributes, a Select AI profile, an object storage source, and a vector store. + + + + + + + + + Vector Index object model + A vector index turns source documents into searchable embeddings for grounded Select AI responses. + + + VectorIndex / AsyncVectorIndex + Synchronous and asynchronous lifecycle APIs + + Identity + • index_name, description, owner + • qualified_name for shared indexes + Lifecycle + create, fetch, list, update + enable, disable, delete + Relationships + attributes and optional profile + + + VectorIndexAttributes + How content is loaded, embedded, and searched + + Source + • location, object_storage_credential_name + Embedding and search + • profile_name, vector_dimension + • match_limit, similarity_threshold + Storage and refresh + • vector_table_name, refresh_rate + • chunk_size, chunk_overlap, metric + + + OracleVectorIndexAttributes + Oracle implementation of the attribute set + + Specialization + • vector_db_provider = ORACLE + Adds Oracle vector table and + database storage behavior. + + + has + + specializes + + + Profile / AsyncProfile + Provides the embedding model and + the generation profile used by RAG. + • VectorIndex.profile or profile_name + + + Source documents + Object Storage or another configured + source location supplies the content. + Chunking and refresh are configurable. + + + Oracle vector store + Embeddings and chunks are stored in + the configured vector table. + RAG retrieves matching chunks as context. + + + uses + + reads + + embeds and stores + + + Lifecycle: create → load/refresh → search for RAG → update or disable → delete + diff --git a/doc/source/index.rst b/doc/source/index.rst index 0a2dc6a..767ab0f 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -59,6 +59,15 @@ Credential user_guide/credential.rst +Sharing and ownership +===================== + +.. toctree:: + :numbered: + :maxdepth: 3 + + user_guide/sharing.rst + Profile Attributes ================== @@ -143,6 +152,15 @@ Async AI Agent user_guide/async_agent.rst +A2A Integration +=============== + +.. toctree:: + :numbered: + :maxdepth: 3 + + user_guide/a2a.rst + Command Line Interface ====================== diff --git a/doc/source/user_guide/a2a.rst b/doc/source/user_guide/a2a.rst new file mode 100644 index 0000000..f6a8eb5 --- /dev/null +++ b/doc/source/user_guide/a2a.rst @@ -0,0 +1,768 @@ +.. _a2a: + +******************** +Agent2Agent (A2A) +******************** + +Select AI for Python can expose an Oracle Database AI Agent Team through the +`Agent2Agent (A2A) protocol `__. A2A clients can +discover the team from an Agent Card, submit database prompts, and retrieve +task results through the same JSON-RPC endpoint. + +There are two deployment modes: + +* **Standalone A2A server**: one server owns a configured Oracle connection + pool and one configured Select AI team. This mode supports streaming and is + suitable when the service owner controls the database identity. +* **Dynamic A2A gateway**: a public gateway asks the client for a database DSN, + username, password, and team name through an A2UI form. It opens a temporary + isolated worker session for that selection. This mode supports task polling, + but does not advertise or implement streaming. + +.. only:: html + + .. figure:: /image/a2a_architecture.svg + :alt: Architecture comparison of the standalone A2A server and dynamic A2A gateway + :width: 100% + + Select AI A2A deployment modes. The standalone server fixes the database + and team at startup; the gateway selects them for each temporary session. + +.. only:: latex + + .. figure:: /image/a2a_architecture.png + :alt: Architecture comparison of the standalone A2A server and dynamic A2A gateway + :width: 100% + + Select AI A2A deployment modes. The standalone server fixes the database + and team at startup; the gateway selects them for each temporary session. + +CLI-based deployment on any cloud +================================== + +The A2A runtimes are provided as ordinary Select AI CLI commands, so they are +not tied to a particular cloud provider. Run the commands on a local machine, +a virtual machine, a container platform, or a Kubernetes service in any cloud +that can reach the required Oracle Database and, for dynamic deployments, the +Consul service: + +* ``select-ai a2a serve`` runs the standalone server. +* ``select-ai a2a gateway`` runs the public dynamic gateway. +* ``select-ai a2a worker`` runs a database-bearing dynamic worker. + +The commands can be packaged into the platform's preferred container or +process deployment. The Google Cloud scripts documented below are convenience +automation for a Cloud Run/GKE deployment; they are not required to use the +A2A CLI commands on another cloud or on self-managed infrastructure. + +The public routes are the same in both modes: + +``/.well-known/agent-card.json`` + Agent Card discovery endpoint. + +``/a2a/jsonrpc/`` + A2A JSON-RPC endpoint. The endpoint accepts the A2A 1.0 method names and + the A2A v0.3 compatibility method names. + +.. list-table:: Deployment mode comparison + :header-rows: 1 + :widths: 23 38 39 + :align: left + + * - Concern + - Standalone server + - Dynamic gateway + * - Database and team + - Fixed in the server configuration. + - Selected by the client for each session. + * - Public process + - ``select-ai a2a serve`` + - ``select-ai a2a gateway`` + * - Session process + - The server's shared asynchronous connection pool. + - A worker child process and connection pool per active session. + * - State + - Oracle-backed tasks, A2A contexts, and conversations. + - The same Oracle-backed state, with Consul routing metadata. + * - Streaming + - Advertised and supported. + - Not advertised and rejected; use task polling. + * - Database authentication + - DSN/user/password, with optional Oracle wallet. + - DSN/user/password submitted through A2UI; wallet connections are not + currently supported by this session path. + +Installation and prerequisites +============================== + +Install the A2A dependencies before running the server, gateway, or worker: + +.. code-block:: bash + + python -m pip install 'select_ai[a2a]' + +All modes require: + +* an Oracle Database where the Select AI Agent Team is installed; +* a database user with the privileges required by Select AI and the team; and +* network access from the process to Oracle Database. + +The standalone server needs one database identity at startup. The gateway does +not need a database identity at startup, but each submitted A2UI connection +form must contain a valid DSN, username, password, and team name. + +Protocol compatibility +====================== + +The server and gateway use the same JSON-RPC URL for A2A 1.0 and v0.3. The +protocol adapter translates the v0.3 method names and Agent Card shape for +clients that have not migrated to A2A 1.0. + +.. list-table:: Common A2A operations + :header-rows: 1 + :widths: 28 34 38 + :align: left + + * - Operation + - A2A 1.0 + - A2A v0.3 compatibility name + * - Send a message + - ``SendMessage`` + - ``message/send`` + * - Stream a message + - ``SendStreamingMessage`` + - ``message/stream`` + * - Get a task + - ``GetTask`` + - ``tasks/get`` + * - List tasks + - ``ListTasks`` + - ``tasks/list`` + * - Cancel a task + - ``CancelTask`` + - ``tasks/cancel`` + +For A2A 1.0 clients, use ``A2A-Version: 1.0`` when the client library requires +an explicit version header. A v0.3 client uses the compatibility names. The +``select-ai a2a agent-card`` command prints a v0.3 Agent Card that can be +registered with Gemini Enterprise. + +Standalone A2A server +===================== + +The standalone server exposes one database AI Agent Team. The database user, +password, DSN, and optional wallet are configured when the process starts. The +team name is required: + +.. code-block:: bash + + export SELECT_AI_USER=select_ai_user + export SELECT_AI_PASSWORD='database-password' + export SELECT_AI_DB_CONNECT_STRING='db2025adb_medium' + + select-ai a2a serve \ + --team ORACLE_AI_DATABASE_AGENT \ + --host 0.0.0.0 \ + --port 8000 \ + --public-url https://a2a.example.com + +The connection values can also be passed with ``--user``, ``--password``, and +``--dsn``. If no password is supplied, the CLI prompts for it. ``--public-url`` +is the externally reachable base URL placed into the Agent Card; set it when a +proxy or load balancer sits in front of the process. + +The server prints the discovery URL when it starts: + +.. code-block:: text + + A2A Agent Card: https://a2a.example.com/.well-known/agent-card.json + INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit) + +The exact Uvicorn startup lines vary by version and configuration. + +Agent Card discovery +~~~~~~~~~~~~~~~~~~~~ + +Fetch the card after starting the server: + +.. code-block:: bash + + curl -sS https://a2a.example.com/.well-known/agent-card.json | jq + +The discovery response is the A2A v0.3-compatible representation used by +clients such as Gemini Enterprise. It identifies the JSON-RPC endpoint and +advertises streaming. The server's internal A2A 1.0 card declares both a +JSON-RPC 1.0 interface and a JSON-RPC v0.3 interface at that same endpoint. + +When a service has a public URL, the portable v0.3 card can also be printed +without starting the server: + +.. code-block:: bash + + select-ai a2a agent-card \ + --team ORACLE_AI_DATABASE_AGENT \ + --public-url https://a2a.example.com + +This command prints JSON similar to: + +.. code-block:: json + + { + "protocolVersion": "0.3", + "name": "ORACLE_AI_DATABASE_AGENT", + "url": "https://a2a.example.com/a2a/jsonrpc/", + "capabilities": {"streaming": true}, + "defaultInputModes": ["text/plain"], + "defaultOutputModes": ["text/plain"] + } + +The package version, description, and skill list are also included in the +actual output. + +Tasks, polling, streaming, and cancellation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The standalone server supports these execution patterns: + +* **Blocking**: send a message without a non-blocking option. The request + waits until the task reaches a terminal state and returns the completed + task. +* **Non-blocking and polling**: send a v0.3 request with + ``configuration.blocking: false`` or an A2A 1.0 request with + ``configuration.returnImmediately: true``. Poll ``tasks/get`` or + ``GetTask`` until the task reaches ``completed``, ``failed``, ``canceled``, + or ``rejected``. +* **Streaming**: use ``message/stream`` or ``SendStreamingMessage``. The Agent + Card advertises ``streaming: true`` and the endpoint returns the protocol's + streaming events over the HTTP streaming response. +* **Cancellation**: call ``tasks/cancel`` or ``CancelTask`` with the task ID. + The server updates the task through the A2A task handler and returns its + canceled state when cancellation succeeds. + +The repository includes the +`blocking_task.py sample `__ +and the +`task_poll.py sample `__: + +.. literalinclude:: ../../../samples/a2a/blocking_task.py + :language: python + :lines: 8- + +The blocking sample omits ``configuration``. Representative output is: + +.. code-block:: text + + Task 7e1...: completed + { + "id": "7e1...", + "status": {"state": "completed", "timestamp": "..."}, + "artifacts": [{"name": "database-agent-result", "parts": ["..."]}] + } + +The task ID, timestamp, and database answer vary for each run. + +.. literalinclude:: ../../../samples/a2a/task_poll.py + :language: python + :lines: 8- + +The polling sample sends a non-blocking request and then calls ``tasks/get``: + +.. code-block:: text + + Task 42b...: submitted + Task 42b...: working + Task 42b...: completed + { + "id": "42b...", + "status": {"state": "completed", "timestamp": "..."}, + "artifacts": [{"name": "database-agent-result", "parts": ["..."]}] + } + +.. _persistent-oracle-backed-state: + +Persistent Oracle-backed state +============================== + +Both A2A deployment modes use Oracle implementations of the A2A task and +context stores. In standalone mode, the server initializes these stores when +the application starts. In dynamic gateway mode, ``select-ai a2a gateway`` +does not connect to Oracle itself; the ``select-ai a2a worker`` command starts +the internal worker, and each connected worker session initializes the stores +after it opens its supplied database connection. On first initialization, the +stores create these tables if they do not already exist: + +``SELECT_AI_A2A_TASKS`` + Stores the task ID, context ID, serialized task JSON, owner, and update + timestamp. It supports task retrieval, filtering, listing, pagination, and + deletion. + +``SELECT_AI_A2A_CONTEXTS`` + Maps an A2A context and owner to an Oracle conversation ID. + +When a request starts a new context, Select AI creates an +``AsyncConversation`` and passes its ID to ``AsyncTeam.run``. Later messages in +the same A2A context reuse that conversation, so the team can use the +conversation history. In standalone mode, task and context ownership is +resolved from the A2A request context; an absent owner is stored as +``anonymous``. In gateway mode, the worker scopes the rows to the temporary +session ID, which is the A2A context ID used to route requests back to that +worker session. + +The tables hold task and context metadata, while the conversation prompts and +responses remain in the regular Select AI conversation storage. This means a +server or worker process restart does not discard task records, context +mappings, or conversation history, provided the same database and owner scope +are used. Gateway routing records and worker child processes are temporary: +after a worker session expires or is lost, submit the connection form again so +the gateway can recreate the session and route requests to the durable Oracle +state. The gateway's temporary connection-form task itself is not stored in +Oracle; durable A2A tasks are created by the worker after the database session +is ready. + +Wallet-based database connections +================================= + +Wallet-based connections apply to the standalone ``select-ai a2a serve`` +path. The standalone server passes wallet settings to the Select AI +asynchronous connection pool. Use the unzipped wallet directory with +``SELECT_AI_WALLET_LOCATION`` or ``--wallet-location`` and provide the wallet +password with ``SELECT_AI_WALLET_PASSWORD`` or ``--wallet-password``: + +.. code-block:: bash + + export SELECT_AI_WALLET_LOCATION=/secure/wallet/Wallet_database + export SELECT_AI_WALLET_PASSWORD='wallet-password' + export SELECT_AI_DB_CONNECT_STRING=db2025adb_medium + + select-ai a2a serve \ + --team ORACLE_AI_DATABASE_AGENT \ + --user SELECT_AI_USER \ + --dsn db2025adb_medium + +``--wallet-location`` is used as both the wallet location and the Oracle +configuration directory. Keep wallet files and passwords outside the source +tree. The +`standalone deployment script `__ +can upload a wallet archive +to Secret Manager and expand it into ephemeral Cloud Run storage; see +`Standalone Google Cloud deployment`_ below. + +Dynamic A2A gateway +=================== + +The dynamic mode separates the public A2A protocol endpoint from the +database-bearing runtime. The gateway starts without database credentials. A +worker registers with Consul and waits for the gateway to assign sessions. The +worker's connected sessions use the persistent Oracle-backed task, context, and +conversation state described in :ref:`Persistent Oracle-backed state +`; Consul stores only the temporary routing +metadata. + +Consul provides the gateway's service-discovery and lightweight routing +control plane. Workers register their address and health check with Consul; +the gateway asks Consul for a passing worker when a session is opened. The +gateway then stores the session-to-worker and task-to-session routes in Consul +so later requests go directly to the worker that owns the session. Consul does +not store database credentials, task payloads, or conversation history. + +For local development, install the Consul command-line binary before starting +the local agent. HashiCorp provides package-manager and precompiled-binary +instructions in the `Consul installation guide +`__. +After installation, verify that the binary is on ``PATH``: + +.. code-block:: bash + + consul version + +The ``-dev`` agent below is intentionally for local development only. It uses +an in-memory, single-node Consul server bound to loopback. For the Google +Cloud deployment, do not install or run a local agent: the deployment creates +Consul in GKE from the +`Consul manifest `__. + +Start the three local components in separate terminals: + +.. code-block:: bash + + # Terminal 1: Consul + consul agent -dev -bind=127.0.0.1 -client=127.0.0.1 + + # Terminal 2: worker + CONSUL_HTTP_URL=http://127.0.0.1:8500 \ + WORKER_ID=local-worker \ + WORKER_ADDRESS=127.0.0.1 \ + WORKER_PORT=8081 \ + select-ai a2a worker \ + --host 127.0.0.1 \ + --port 8081 + + # Terminal 3: public gateway + select-ai a2a gateway \ + --host 127.0.0.1 \ + --port 8000 \ + --agent-url http://127.0.0.1:8000 \ + --consul-url http://127.0.0.1:8500 + +The worker uses ``CONSUL_HTTP_URL`` (default ``http://consul:8500``), +``WORKER_ID``, ``WORKER_ADDRESS``, ``WORKER_PORT``, and the optional +``WORKER_ENDPOINT`` environment variables when registering with Consul. The +gateway uses ``AGENT_URL``, ``CONSUL_HTTP_URL``, ``WORKER_SERVICE`` (default +``select-ai-worker``), and ``SESSION_TTL_SECONDS``. The command-line options +override the corresponding environment variables. + +The worker must be able to resolve the submitted DSN. For a TNS alias, set +``TNS_ADMIN`` in the worker terminal before starting it. The gateway session +path accepts a DSN, username, and password; it does not currently accept an +Oracle wallet. + +The gateway's Agent Card advertises ``streaming: false``. It supports blocking +tasks, non-blocking tasks, task retrieval/listing, and cancellation by +forwarding the request to the worker session. Streaming and push-notification +operations are rejected because the gateway does not proxy a live stream. + +A2UI database-connection form +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The first ``message/send`` for a context returns a temporary task containing a +database connection form. The form is an A2UI v0.9 data artifact with the +``application/json+a2ui`` MIME type. It contains these fields: + +.. list-table:: Dynamic connection form + :header-rows: 1 + :widths: 25 75 + :align: left + + * - Field + - Purpose + * - ``dsn`` + - Oracle connect descriptor, simplified connect string, or TNS alias + available to the worker. + * - ``username`` + - Database user for this session. + * - ``password`` + - Database password. The form renders this field as obscured input. + * - ``team_name`` + - Select AI Agent Team to run in this session. + * - ``submit_database_connection`` + - A2UI action that submits the four values to the gateway. + +After the action is submitted, the gateway selects a healthy worker through +Consul and opens a session. The worker starts a child process, calls +``select_ai.async_connect`` with the supplied DSN/user/password, validates the +team, initializes the Oracle task and context stores, and returns a +``database-session`` artifact. The client can then send ordinary database +prompts with the same A2A context ID. + +Credentials are used to open the session and are not written to Consul. Consul +stores only the worker endpoint and expiration for the session, plus task-to- +session routing metadata. The credentials remain in the worker child process +for the lifetime of that session, so use TLS for client-to-gateway traffic and +follow the security policies for any client that renders the form. + +The gateway samples perform this handshake automatically. They use the +repository-local helper module +`samples/a2a/gateway/_common.py `__; +``_common`` +is not a package that users install with ``pip``. Running the samples from the +repository root as shown below makes that helper available automatically. + +The helper functions are: + +``call(method, params)`` + Sends one A2A v0.3 JSON-RPC request to the configured gateway endpoint. + +``connect(prompt)`` + Sends the initial prompt, reads the A2UI connection form, submits the + ``SELECT_AI_DB_CONNECT_STRING``, ``SELECT_AI_USER``, + ``SELECT_AI_PASSWORD``, and ``SELECT_AI_A2A_TEAM`` values, and returns the + A2A context ID for the connected session. + +``send_prompt(prompt, context_id, blocking=None)`` + Sends a database prompt in an existing gateway context. Passing + ``blocking=False`` adds the non-blocking request option. + +``print_task_summary(task)`` + Prints the task state, result artifact name, and text parts without dumping + the connection-form details. + +If you copy a gateway sample into another directory, copy the +`_common.py helper `__ +with it, or replace these helpers with an A2A client implementation of your +own. + +The complete gateway setup is also documented in the +`gateway sample README `__. +The examples below are the +`blocking_task.py sample `__ +and the +`task_poll.py sample `__. + +.. literalinclude:: ../../../samples/a2a/gateway/blocking_task.py + :language: python + :lines: 8- + +Set the database values before running the sample: + +.. code-block:: bash + + export SELECT_AI_DB_CONNECT_STRING='database-dsn' + export SELECT_AI_USER='database-user' + export SELECT_AI_PASSWORD='database-password' + export SELECT_AI_A2A_TEAM='ORACLE_AI_DATABASE_AGENT' + +Run it with: + +.. code-block:: bash + + python samples/a2a/gateway/blocking_task.py + +Representative output is: + +.. code-block:: text + + Task 15c...: completed + Artifact: database-agent-result + The database contains ... + +The gateway polling sample uses the same connection-form handshake, then +passes ``configuration.blocking: false`` and polls ``tasks/get``: + +.. literalinclude:: ../../../samples/a2a/gateway/task_poll.py + :language: python + :lines: 8- + +.. code-block:: text + + Task 91a...: submitted + Task 91a...: working + Task 91a...: completed + Artifact: database-agent-result + The database contains ... + +Session isolation, routing, and cleanup +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Each successful connection action creates one worker session identified by the +A2A context ID. The worker owns an operating-system child process for that +session. The child process owns its Select AI asynchronous connection, A2A +handler, task store, context store, and database conversation access. Requests +for different sessions therefore do not share database credentials or a +connection pool. + +The default session lifetime is 900 seconds (15 minutes). Change it with +``--session-ttl-seconds`` on the worker and gateway, or with the gateway's +``SESSION_TTL_SECONDS`` environment variable. The worker reaper checks for +expired or dead child processes and terminates them. The gateway also checks +the expiry stored in Consul. When a route or child session is no longer +available, the gateway returns an A2UI connection form so the client can +reconnect rather than silently sending a request to a different database. + +Consul is used for two kinds of routing metadata: + +* the worker service registration and health TTL, which let the gateway select + a passing worker; and +* ``select-ai/sessions/`` and ``select-ai/tasks/`` key-value records, which + route a context or task back to the worker that owns it. + +The task and conversation contents are not stored in Consul. They are stored +in Oracle by the selected worker. A worker registers a health check and sends +heartbeats; it deregisters on shutdown. The gateway selects healthy workers +round-robin when opening new sessions. + +Gateway-to-worker mTLS +~~~~~~~~~~~~~~~~~~~~~~ + +Local development uses ordinary HTTP. For an internal deployment, the gateway +can authenticate workers and workers can require a gateway client certificate. +All three gateway files are required together: + +.. code-block:: bash + + select-ai a2a gateway \ + --agent-url https://gateway.example.com \ + --worker-tls-ca-file /run/secrets/worker-ca.pem \ + --worker-tls-cert-file /run/secrets/gateway-client.crt \ + --worker-tls-key-file /run/secrets/gateway-client.key + +Start the worker with the matching server certificate, key, and CA: + +.. code-block:: bash + + select-ai a2a worker \ + --tls-cert-file /run/secrets/worker.crt \ + --tls-key-file /run/secrets/worker.key \ + --tls-ca-file /run/secrets/gateway-client-ca.pem + +The worker must register an HTTPS endpoint through ``WORKER_ENDPOINT`` when +mTLS is enabled. The gateway verifies the worker certificate with the CA and +presents its client certificate. This mTLS option protects only the +gateway-to-worker HTTP hop. It does not add wallet-based mTLS to the gateway's +database connection path. + +Google Cloud deployment +======================= + +The repository contains complete deployment workflows under the +`gcloud directory `__. +The +two scripts deploy different topologies, so choose the directory that matches +the desired runtime model. + +Standalone Google Cloud deployment +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`gcloud/standalone/deploy.sh `__ +deploys one private Cloud Run service for one +database and team: + +.. code-block:: bash + + gcloud/standalone/deploy.sh \ + --project PROJECT_ID \ + --a2a-team ORACLE_AI_DATABASE_AGENT \ + --build + +The script creates or reuses an Artifact Registry repository, runtime service +account, Secret Manager secrets, and Cloud Run service. On the first run it +prompts for the database user, password, and connect descriptor. It injects +them into the service as ``SELECT_AI_USER``, ``SELECT_AI_PASSWORD``, and +``SELECT_AI_DB_CONNECT_STRING``. The service is private; the script grants +``run.routes.invoke`` to the active deployment identity and the Gemini +Enterprise Discovery Engine service agent. + +Use ``--build`` when the current checkout should become a new container image. +The script submits +`gcloud/standalone/cloudbuild.yaml `__ +to Cloud Build, which +builds ``docker/Dockerfile`` from the repository root and pushes the image to +Artifact Registry. Without ``--build``, a later invocation reuses the image +already deployed and only updates Cloud Run configuration or secrets. + +Important standalone options include: + +* ``--service``: Cloud Run service name; use a different service for each + fixed team/database deployment. +* ``--a2a-team``: team installed in Oracle Database. +* ``--pool-max-size``: maximum Oracle connections per Cloud Run instance. +* ``--max-instances``: Cloud Run instance limit. +* ``--wallet-archive``: upload or replace an Autonomous Database wallet ZIP. +* ``--rotate-db-credentials``: prompt for and rotate the database secrets. + +For a wallet deployment, ``--wallet-archive`` stores the ZIP and wallet +password in service-specific Secret Manager secrets. Cloud Run mounts the ZIP +read-only; the launcher expands it into ephemeral ``/tmp`` storage, verifies +the wallet, and sets ``SELECT_AI_WALLET_LOCATION`` before starting the A2A +server. Do not commit wallet files or put them in the container image. + +The standalone script prints the deployed Agent Card JSON at the end. The +service is ready for a client when the printed card points to the final Cloud +Run URL and the client can invoke the private service. + +Dynamic gateway Google Cloud deployment +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`gcloud/gateway/deploy.sh `__ +deploys the gateway topology: Cloud Run for the +public gateway, GKE Autopilot for worker replicas, and Consul for discovery and +routing. + +.. code-block:: bash + + gcloud/gateway/deploy.sh \ + --project PROJECT_ID \ + --region us-central1 \ + --worker-replicas 3 + +The script and +`gcloud/gateway/cloudbuild.yaml `__ +perform these steps: + +1. Enable the required Google Cloud APIs and create or reuse Artifact Registry + and a GKE Autopilot cluster. +2. Configure additive VPC DNS so StatefulSet worker names resolve to current + Pod IPs after a worker is recreated. +3. Deploy the gateway namespace, internal Consul service, and worker service. +4. Build and publish the image with Cloud Build. +5. Deploy the requested worker replicas with ``select-ai a2a worker`` and the + selected session TTL. +6. Deploy Cloud Run with ``select-ai a2a gateway`` and set ``AGENT_URL`` to the + final Cloud Run URL. + +The Cloud Run gateway uses direct VPC egress to reach the internal Consul +load-balancer address and worker endpoints. The GKE worker service is headless +service discovery, not a public load balancer. Consul selects a healthy worker +when a session opens; the route is then pinned to that worker. + +Important gateway options include: + +* ``--cluster`` and ``--gke-dns-domain``: GKE Autopilot cluster and immutable + additive DNS domain. +* ``--network`` and ``--subnet``: VPC path used by Cloud Run to reach the + internal worker infrastructure. +* ``--worker-replicas``: number of worker runtimes available for new sessions. +* ``--session-ttl-seconds``: lifetime of temporary database sessions. +* ``--enable-worker-mtls``: create and use gateway-to-worker certificates. +* ``--rotate-worker-mtls``: replace the existing test certificates and restart + the worker workload. + +The default gateway deployment uses private-VPC HTTP between Cloud Run and +GKE. With ``--enable-worker-mtls``, the script creates short-lived test PKI +material in Secret Manager, creates the Kubernetes TLS secrets, deploys the +worker StatefulSet variant, and mounts the gateway client certificate into +Cloud Run. The worker certificate uses the GKE StatefulSet DNS name, so the +``--gke-dns-domain`` value must remain consistent with the cluster. + +The gateway deployment does not upload an Oracle wallet because the dynamic +worker session path currently accepts only DSN/user/password. If database mTLS +is required, use the standalone deployment or provide a separate database +connection mechanism to the worker implementation. + +The deployment files are intended to be read together: + +.. list-table:: Google Cloud A2A deployment files + :header-rows: 1 + :widths: 32 68 + :align: left + + * - File + - Role + * - `gcloud/standalone/deploy.sh `__ + - Creates and updates one private Cloud Run standalone server. + * - `gcloud/standalone/cloudbuild.yaml `__ + - Builds and publishes the standalone container image. + * - `gcloud/standalone/README.md `__ + - Documents IAM, secrets, wallet archives, and update behavior. + * - `gcloud/gateway/deploy.sh `__ + - Creates or reuses the gateway GKE/Cloud Run topology and supplies build + substitutions. + * - `gcloud/gateway/cloudbuild.yaml `__ + - Builds the image, deploys Consul and workers, and deploys Cloud Run. + * - `gcloud/gateway/gke manifests `__ + - Namespace, Consul, headless worker service, and HTTP or mTLS worker + workloads. + * - `gcloud/gateway/README.md `__ + - Explains the topology, DNS, mTLS test mode, and operational details. + +Troubleshooting and security notes +================================== + +* If the Agent Card advertises the wrong URL, set ``--public-url`` for the + standalone server or ``AGENT_URL`` for the gateway. The URL must be the + client-visible base URL, not an internal container address. +* If the gateway returns the connection form repeatedly, check that the worker + is passing in Consul, that the worker can resolve and connect to Oracle, and + that the session TTL has not expired. +* If a task can no longer be retrieved after a worker restart, submit the A2UI + form again. Consul routes are intentionally tied to the worker session, even + though task data is durable in Oracle. +* Do not put database passwords in source files, container images, Consul KV, + or deployment logs. Use Secret Manager for fixed standalone credentials and + protect the A2UI/gateway path with HTTPS. +* Gateway mTLS and Oracle wallet mTLS are different controls. Gateway mTLS + authenticates the internal HTTP peer; an Oracle wallet authenticates the + database connection and is currently available only to the standalone + server path. + +See :ref:`Command Line Interface ` for the common CLI connection options +and :ref:`Connection ` for Select AI database connection setup. diff --git a/doc/source/user_guide/agent.rst b/doc/source/user_guide/agent.rst index d86e366..309d43d 100644 --- a/doc/source/user_guide/agent.rst +++ b/doc/source/user_guide/agent.rst @@ -182,6 +182,39 @@ output:: .. latex:clearpage:: +Inspect and run a tool +++++++++++++++++++++++ + +Use ``Tool.describe_tool()`` to return JSON metadata for a tool, including its +function arguments. Use ``Tool.run_tool(input)`` to invoke a tool directly with +an input payload. Direct execution is useful for testing a tool independently +of a team workflow. + +.. code-block:: python + + tool = Tool.fetch("MOVIE_SQL_TOOL") + print(tool.describe_tool()) + print(tool.run_tool('{"query": "How many movies are there?"}')) + +The complete example creates a temporary PL/SQL tool, describes it, invokes it, +and removes the temporary database objects: + +.. literalinclude:: ../../../samples/agent/tool_run_describe.py + :language: python + :lines: 14- + +The generated tool name and calculated age vary between runs. Representative +output is: + +output:: + + Tool description: + {"tool_name": "SAMPLE_AGE_TOOL_", ...} + Tool result: + + +.. latex:clearpage:: + ******** ``Task`` ******** @@ -319,11 +352,80 @@ For example: .. autoclass:: select_ai.agent.TeamAttributes :members: +Supervised teams +++++++++++++++++ + +A supervised team uses a dedicated agent to coordinate the other agents in the +team. Set ``supervisor=True`` on that agent's ``AgentAttributes`` and pass the +agent name in ``TeamAttributes.supervisor_agent``. The worker agent and task +assignments remain in the team's ``agents`` list. + +``supervisor_task`` is created by the database when the team is created. Do not +set it when constructing ``TeamAttributes``. Fetch the team after creation to +read the generated value from ``fetched.attributes.supervisor_task``. + +.. code-block:: python + + supervisor = Agent( + agent_name="MOVIE_SUPERVISOR", + attributes=AgentAttributes( + profile_name="oci_ai_profile", + role="You supervise and coordinate the team.", + supervisor=True, + ), + ) + + team = Team( + team_name="MOVIE_AGENT_TEAM", + attributes=TeamAttributes( + agents=[ + {"name": "MOVIE_ANALYST", "task": "ANALYZE_MOVIE_TASK"} + ], + process="sequential", + supervisor_agent=supervisor.agent_name, + ), + ) + +The complete example creates a supervised team, runs a prompt through the +supervisor workflow, and inspects the generated supervisor task and team +metadata. ``Team.run()`` starts the team workflow; the configured supervisor +agent coordinates the worker agent as part of that call. + +.. literalinclude:: ../../../samples/agent/team_supervisor_inspect.py + :language: python + :lines: 14- + +The generated names and returned JSON metadata vary between runs. Representative +output is: + +output:: + + Team response: + Supervisor agent: SAMPLE_SUPERVISOR_ + Supervisor task: + Team description: + Team tools: + .. latex:clearpage:: .. autoclass:: select_ai.agent.Team :members: +Share a team +++++++++++++ + +Grant or revoke access for a database user or role with ``Team`` methods. Run +these methods as the team owner: + +.. code-block:: python + + team = Team.fetch("MOVIE_AGENT_TEAM") + team.grant_access("APP_USER") + team.revoke_access("APP_USER") + +Team fetch and list operations use the connected user's agent-team views and do +not currently accept an ``owner`` argument. + .. latex:clearpage:: Run Team @@ -425,6 +527,21 @@ storage, ``Team.export_team()`` writes the specification to the location and returns ``None``. When importing from object storage, pass the same credential and location instead of ``specification``. +Inspect a team +++++++++++++++ + +``Team.describe_team()`` returns JSON metadata and the aggregated skills for a +team. ``Team.list_tools()`` returns JSON metadata for the tools available to +that team. + +.. code-block:: python + + team = Team.fetch("MOVIE_AGENT_TEAM") + print(team.describe_team()) + print(team.list_tools()) + +The supervised-team sample also demonstrates both inspection methods. + Lifecycle helpers +++++++++++++++++ @@ -446,6 +563,39 @@ operations. .. latex:clearpage:: +Object definitions +****************** + +Use ``get_definition(object_type, object_name)`` to retrieve the canonical +PL/SQL block for recreating an AI ``AGENT``, ``TASK``, ``TOOL``, or ``TEAM``. +The function returns ``None`` when the database does not return a definition. + +.. code-block:: python + + from select_ai.agent import get_definition + + definition = get_definition("TASK", "ANALYZE_MOVIE_TASK") + print(definition) + +The asynchronous equivalent is ``async_get_definition``. See the sync and +async definition samples for a complete create, inspect, and cleanup flow. + +.. literalinclude:: ../../../samples/agent/get_definition.py + :language: python + :lines: 14- + +The task name and exact PL/SQL formatting vary between runs and database +versions. Representative output is: + +output:: + + BEGIN + DBMS_CLOUD_AI_AGENT.CREATE_TASK(...); + END; + / + +.. latex:clearpage:: + ************* Agent history ************* @@ -482,6 +632,22 @@ to retrieve the associated task and tool history. .. autoclass:: select_ai.agent.ToolHistory :members: +The complete sample prints the latest team execution and the task and tool +events associated with that execution: + +.. literalinclude:: ../../../samples/agent/history_list.py + :language: python + :lines: 14- + +History depends on an existing execution for the configured team. Representative +output is: + +output:: + + TeamHistoryEvent(team_exec_id='', team_name='ORACLE_AI_DATABASE_AGENT', state='', ...) + TaskHistoryEvent(team_exec_id='', task_name='', state='', ...) + ToolHistoryEvent(invocation_id=, team_exec_id='', tool_name='', ...) + .. latex:clearpage:: ***************** diff --git a/doc/source/user_guide/async_agent.rst b/doc/source/user_guide/async_agent.rst index 5f48f42..06377d5 100644 --- a/doc/source/user_guide/async_agent.rst +++ b/doc/source/user_guide/async_agent.rst @@ -8,6 +8,7 @@ use ``asyncio`` and ``select_ai.async_connect()`` or The history API follows the same pattern. ``AsyncTeamHistory``, ``AsyncTaskHistory``, and ``AsyncToolHistory`` query only the current user's history views and yield typed events newest first. +See :ref:`async-agent-history` for the complete API reference and sample. .. code-block:: python @@ -150,6 +151,38 @@ output:: .. latex:clearpage:: +Inspect and run a tool +++++++++++++++++++++++ + +Use ``AsyncTool.describe_tool()`` to return JSON metadata for a tool, including +its function arguments. Use ``AsyncTool.run_tool(input)`` to invoke a tool +directly with an input payload. + +.. code-block:: python + + tool = await AsyncTool.fetch("MOVIE_SQL_TOOL") + print(await tool.describe_tool()) + print(await tool.run_tool('{"query": "How many movies are there?"}')) + +The complete example creates a temporary PL/SQL tool, describes it, invokes it, +and removes the temporary database objects: + +.. literalinclude:: ../../../samples/agent/async/tool_run_describe.py + :language: python + :lines: 14- + +The generated tool name and calculated age vary between runs. Representative +output is: + +output:: + + Tool description: + {"tool_name": "SAMPLE_AGE_TOOL_", ...} + Tool result: + + +.. latex:clearpage:: + ************* ``AsyncTask`` @@ -248,6 +281,76 @@ AsyncTeam .. autoclass:: select_ai.agent.AsyncTeam :members: +Share an async team ++++++++++++++++++++ + +Grant or revoke access for a database user or role with ``AsyncTeam`` methods. +Run these methods as the team owner: + +.. code-block:: python + + team = await AsyncTeam.fetch("MOVIE_AGENT_TEAM") + await team.grant_access("APP_USER") + await team.revoke_access("APP_USER") + +Team fetch and list operations use the connected user's agent-team views and do +not currently accept an ``owner`` argument. + +Supervised teams +++++++++++++++++ + +Configure an asynchronous supervised team in the same way as a synchronous +team. Set ``supervisor=True`` on the coordinating agent and pass its name as +``TeamAttributes.supervisor_agent``. Keep the worker agent and task +assignments in the team's ``agents`` list. + +The database creates the supervisor task when the team is created, so +``supervisor_task`` should not be supplied by the application. Fetch the team +after creation to read the generated value from +``fetched.attributes.supervisor_task``. + +.. code-block:: python + + supervisor = AsyncAgent( + agent_name="MOVIE_SUPERVISOR", + attributes=AgentAttributes( + profile_name="oci_ai_profile", + role="You supervise and coordinate the team.", + supervisor=True, + ), + ) + + team = AsyncTeam( + team_name="MOVIE_AGENT_TEAM", + attributes=TeamAttributes( + agents=[ + {"name": "MOVIE_ANALYST", "task": "ANALYZE_MOVIE_TASK"} + ], + process="sequential", + supervisor_agent=supervisor.agent_name, + ), + ) + +The complete asynchronous example creates the team, runs a prompt through the +supervisor workflow, and inspects its generated supervisor task and metadata. +``AsyncTeam.run()`` starts the team workflow; the configured supervisor agent +coordinates the worker agent as part of that call. + +.. literalinclude:: ../../../samples/agent/async/team_supervisor_inspect.py + :language: python + :lines: 14- + +The generated names and returned JSON metadata vary between runs. Representative +output is: + +output:: + + Team response: + Supervisor agent: SAMPLE_SUPERVISOR_ + Supervisor task: + Team description: + Team tools: + .. latex:clearpage:: Run Team @@ -351,6 +454,22 @@ storage, ``AsyncTeam.export_team()`` writes the specification to the location and returns ``None``. When importing from object storage, pass the same credential and location instead of ``specification``. +Inspect a team +++++++++++++++ + +``AsyncTeam.describe_team()`` returns JSON metadata and the aggregated skills +for a team. ``AsyncTeam.list_tools()`` returns JSON metadata for the tools +available to that team. + +.. code-block:: python + + team = await AsyncTeam.fetch("MOVIE_AGENT_TEAM") + print(await team.describe_team()) + print(await team.list_tools()) + +The asynchronous supervised-team sample also demonstrates both inspection +methods. + .. latex:clearpage:: Lifecycle helpers @@ -374,6 +493,106 @@ operations. .. latex:clearpage:: +Object definitions +****************** + +Use ``async_get_definition(object_type, object_name)`` to asynchronously +retrieve the canonical PL/SQL block for recreating an AI ``AGENT``, ``TASK``, +``TOOL``, or ``TEAM``. The function returns ``None`` when the database does not +return a definition. + +.. code-block:: python + + from select_ai.agent import async_get_definition + + definition = await async_get_definition("TASK", "ANALYZE_MOVIE_TASK") + print(definition) + +See the asynchronous definition sample for a complete create, inspect, and +cleanup flow. + +.. literalinclude:: ../../../samples/agent/async/get_definition.py + :language: python + :lines: 14- + +The task name and exact PL/SQL formatting vary between runs and database +versions. Representative output is: + +output:: + + BEGIN + DBMS_CLOUD_AI_AGENT.CREATE_TASK(...); + END; + / + +.. latex:clearpage:: + + +.. _async-agent-history: + +******************** +Async agent history +******************** + +``AsyncTeamHistory``, ``AsyncTaskHistory``, and +``AsyncToolHistory`` provide asynchronous, read-only access to the current +user's Select AI Agent history views. Their ``list()`` methods return async +iterators ordered from newest to oldest. + +Use ``team_exec_id`` from a team event to scope task and tool history to the +same execution. Filters such as ``team_name``, ``task_name``, +``agent_name``, and ``tool_name`` can be used when an execution +identifier is not available. Tool ``input`` and ``output`` values +are decoded to Python objects when they contain valid JSON. + +.. code-block:: python + + from select_ai.agent import ( + AsyncTaskHistory, + AsyncTeamHistory, + AsyncToolHistory, + ) + + async for team_run in AsyncTeamHistory.list( + team_name="ORACLE_AI_DATABASE_AGENT", + limit=1, + ): + print(team_run) + async for task_run in AsyncTaskHistory.list( + team_exec_id=team_run.team_exec_id + ): + print(task_run) + async for tool_run in AsyncToolHistory.list( + team_exec_id=team_run.team_exec_id + ): + print(tool_run) + +The complete sample retrieves a team's latest execution and uses its +``team_exec_id`` to retrieve the associated task and tool history: + +.. autoclass:: select_ai.agent.AsyncTeamHistory + :members: + +.. autoclass:: select_ai.agent.AsyncTaskHistory + :members: + +.. autoclass:: select_ai.agent.AsyncToolHistory + :members: + +.. literalinclude:: ../../../samples/agent/async/agent_history_list.py + :language: python + :lines: 14- + +History depends on an existing execution for the configured team. Representative +output is: + +output:: + + TeamHistoryEvent(team_exec_id='', team_name='ORACLE_AI_DATABASE_AGENT', state='', ...) + TaskHistoryEvent(team_exec_id='', task_name='', state='', ...) + ToolHistoryEvent(invocation_id=, team_exec_id='', tool_name='', ...) + +.. latex:clearpage:: List Teams ++++++++++ diff --git a/doc/source/user_guide/async_profile.rst b/doc/source/user_guide/async_profile.rst index de1c9f8..9642526 100644 --- a/doc/source/user_guide/async_profile.rst +++ b/doc/source/user_guide/async_profile.rst @@ -37,6 +37,7 @@ The usual async profile lifecycle is: list. * Create the profile with ``await select_ai.AsyncProfile(...)``. * Reuse the profile later by name. +* Enable or disable the profile without deleting it. * Update profile attributes when provider settings or object scope changes. * Delete profiles that are no longer needed. @@ -149,6 +150,28 @@ saved database profile and raise an error if the profile does not exist: .. latex:clearpage:: +******************************* +Enable and disable AsyncProfile +******************************* + +Use ``await AsyncProfile.disable()`` to make an existing profile unavailable +without deleting it. Call ``await AsyncProfile.enable()`` to make it available +again. These methods change the database profile status and return ``None``. + +.. literalinclude:: ../../../samples/async/profile_enable_disable.py + :language: python + :lines: 14- + +The sample uses ``SELECT_AI_PROFILE_NAME`` when set, otherwise it operates on +``oci_ai_profile``. It always re-enables the profile during cleanup: + +output:: + + Disabled profile: oci_ai_profile + Enabled profile: oci_ai_profile + +.. latex:clearpage:: + *********************** Update Async Profile *********************** @@ -354,6 +377,83 @@ Streaming is supported by ``generate()``, ``chat()``, ``narrate()``, ``explain_sql()``, ``show_sql()``, and ``show_prompt()``. It is not supported for ``run_sql()``, which returns a ``pandas.DataFrame``. +.. _async-request-profile-attributes: + +*********************************************** +Async request-level profile attribute overrides +*********************************************** + +The ``attributes`` keyword accepts a mapping of profile attributes to apply to +one async request. These values override the corresponding saved profile +attributes for that request only; they do not update the profile in the +database. The mapping must contain JSON-serializable values. + +Request-level attributes are available on the following asynchronous APIs: + +* ``AsyncProfile.generate()``, ``chat()``, and ``narrate()``. +* SQL helpers: ``explain_sql()``, ``run_sql()``, ``show_sql()``, and + ``show_prompt()``. +* ``AsyncSession.chat()``, ``narrate()``, ``explain_sql()``, ``run_sql()``, + ``show_sql()``, and ``show_prompt()``. +* ``AsyncProfile.run_pipeline()``, which applies the same mapping to every + prompt/action request in the pipeline. +* Streaming calls to the text-producing methods, by combining + ``stream=True`` with ``attributes=...``. ``run_sql()`` does not support + streaming. + +The mapping can include attributes such as ``additional_instructions``, +the integer ``seed``, and ``source_language`` or ``target_language``. Use +``ProfileAttributes`` for profile-wide defaults; use this mapping when an +override should apply only to the current request. + +For ``translate()``, pass language values through its named +``source_language`` and ``target_language`` arguments. The ``attributes`` +mapping described here applies to the generate/action APIs. + +.. code-block:: python + + request_attributes = { + "additional_instructions": "Answer in one sentence.", + "seed": 42, + "source_language": "en", + "target_language": "de", + } + + response = await async_profile.chat( + prompt="What is Oracle Cloud Infrastructure?", + attributes=request_attributes, + ) + +For async streaming, await the method call first and then iterate over the +returned async iterator. The same mapping can be used with an async session: + +.. code-block:: python + + chunks = await async_profile.chat( + prompt="Summarize Oracle Cloud Infrastructure.", + stream=True, + attributes=request_attributes, + ) + async for chunk in chunks: + print(chunk, end="") + + async with async_profile.chat_session(conversation) as session: + response = await session.chat( + prompt="Give one more detail.", + attributes=request_attributes, + ) + +The following sample uses ``show_prompt()`` to verify that the request-only +``additional_instructions`` value reached the generated prompt: + +.. literalinclude:: ../../../samples/async/profile_request_attributes.py + :language: python + :lines: 14- + +output:: + + Request attributes applied: True + .. latex:clearpage:: ************************** @@ -407,6 +507,35 @@ output:: Translate *********** +``AsyncProfile.translate()`` accepts optional ``source_language`` and +``target_language`` arguments. Per-call values take precedence over the +profile's ``source_language`` and ``target_language`` defaults. + +If ``source_language`` is omitted for both the call and the profile, source +language detection is delegated to the translation provider. If +``target_language`` is omitted, the profile's default is used; a target +language must be supplied in one of those two places. + +Configure profile-level defaults with ``ProfileAttributes`` when creating or +updating a profile: + +.. code-block:: python + + language_defaults = select_ai.ProfileAttributes( + source_language="en", + target_language="de", + ) + + await async_profile.set_attributes(language_defaults) + print(await async_profile.translate(text="Thank you")) + +When only the target is supplied at the call site, the provider can detect the +source language: + +.. code-block:: python + + print(await async_profile.translate(text="Thank you", target_language="de")) + .. literalinclude:: ../../../samples/async/profile_translate.py :language: python @@ -422,6 +551,20 @@ output:: Async pipeline ********************* +``AsyncProfile.run_pipeline()`` also accepts ``attributes=...``. The mapping +is applied to every prompt/action request in the pipeline, while the saved +profile remains unchanged. + +.. code-block:: python + + results = await async_profile.run_pipeline( + prompt_specifications, + attributes={ + "additional_instructions": "Keep each answer concise.", + "seed": 42, + }, + ) + .. literalinclude:: ../../../samples/async/profile_pipeline.py :language: python :lines: 14- @@ -455,8 +598,22 @@ output:: List profiles asynchronously **************************** -Profile listing returns profiles visible to the connected database user. The -async list API returns an async iterator. +Profile listing returns profiles visible to the connected database user. Pass +``owner`` to list profiles from a specific schema, including a profile shared +with the connected user. Returned objects include ``owner`` and the +owner-qualified ``qualified_name`` property. The async list API returns an +async iterator. + +.. code-block:: python + + profile = await select_ai.AsyncProfile.fetch( + "OCI_AI_PROFILE", + owner="APP_OWNER", + ) + print(profile.qualified_name) + + async for profile in select_ai.AsyncProfile.list(owner="APP_OWNER"): + print(profile.qualified_name) .. literalinclude:: ../../../samples/async/profiles_list.py :language: python diff --git a/doc/source/user_guide/cli.rst b/doc/source/user_guide/cli.rst index 76e2855..223675e 100644 --- a/doc/source/user_guide/cli.rst +++ b/doc/source/user_guide/cli.rst @@ -15,9 +15,10 @@ write Python code. It can help users check whether a profile is configured correctly, inspect generated SQL, try prompts during development, or interact with a curated profile through a simple command. -The CLI works with Select AI profiles. RAG is supported when the selected -profile is already configured with a vector index. Additional CLI options and -workflows will be added in upcoming releases as the CLI evolves. +The CLI works with Select AI profiles and can also run the Select AI A2A +standalone server, dynamic gateway, internal worker, and Agent Card generator. +RAG is supported when the selected profile is already configured with a vector +index. .. only:: html @@ -32,15 +33,23 @@ workflows will be added in upcoming releases as the CLI evolves. :width: 100% The package provides an optional ``select-ai`` command line tool. Install the -CLI extra to use it, including the A2A server commands: +CLI extra to use profile, SQL, chat, and A2A commands. The ``a2a`` extra is an +alias that makes the A2A dependency set explicit: .. code-block:: bash pip install 'select_ai[cli]' + # Equivalent installation for an A2A deployment: + pip install 'select_ai[a2a]' Use ``select-ai --help`` to view the available command groups and ``select-ai --help`` to view options for a specific command. +The complete A2A guide covers the standalone server, dynamic gateway, Agent +Card discovery, A2A protocol compatibility, task execution, persistent +Oracle-backed state, and Google Cloud deployment. See :ref:`A2A Integration +`. + Set the database connection details as environment variables, or pass them as command line options: @@ -53,7 +62,10 @@ command line options: Connection options ================== -All CLI commands accept the same database connection options: +Database-backed commands accept the following connection options. The +standalone ``select-ai a2a serve`` command uses them directly; the dynamic +gateway receives DSN, username, and password through its A2UI connection form, +and ``a2a worker`` receives those values internally when it opens a session. .. list-table:: Connection options :header-rows: 1 @@ -74,8 +86,10 @@ All CLI commands accept the same database connection options: - ``SELECT_AI_WALLET_PASSWORD`` If ``--password`` and ``SELECT_AI_PASSWORD`` are not set, the CLI prompts for -the database password. Wallet options are optional and are only needed for -wallet-based database connections. +the database password. Wallet options are optional and apply to database +commands that support wallet connections, including standalone +``select-ai a2a serve``. The dynamic gateway session path currently accepts +only DSN, username, and password. Interactive chat ================ @@ -181,6 +195,125 @@ Use ``--pattern`` to filter names with a regular expression. ``--credential-name`` when the location URI requires an object storage credential. +A2A commands +============ + +The ``a2a`` command group exposes the cloud-neutral A2A runtimes. Use +``select-ai a2a --help`` or a command-specific ``--help`` option to see the +current defaults: + +.. code-block:: bash + + select-ai a2a --help + select-ai a2a serve --help + select-ai a2a gateway --help + select-ai a2a worker --help + select-ai a2a agent-card --help + +.. list-table:: A2A CLI commands + :header-rows: 1 + :widths: 34 66 + :align: left + + * - Command + - Purpose + * - ``select-ai a2a serve`` + - Start a standalone A2A HTTP server for one configured database AI Agent + Team. It accepts the database connection and optional wallet options. + * - ``select-ai a2a gateway`` + - Start the public dynamic A2A/A2UI gateway. It uses Consul to discover + workers and does not connect to Oracle directly. + * - ``select-ai a2a worker`` + - Start the internal worker that registers with Consul and creates an + isolated database-bearing child process for each submitted connection. + * - ``select-ai a2a agent-card`` + - Print a Gemini Enterprise-compatible A2A v0.3 Agent Card without + starting a server. + +Standalone server +----------------- + +Start one configured database AI Agent Team. The team, user, password, and DSN +can be passed as options or through the ``SELECT_AI_*`` environment variables: + +.. code-block:: bash + + export SELECT_AI_USER=select_ai_user + export SELECT_AI_PASSWORD='database-password' + export SELECT_AI_DB_CONNECT_STRING=db2025adb_medium + + select-ai a2a serve \ + --team ORACLE_AI_DATABASE_AGENT \ + --host 0.0.0.0 \ + --port 8000 \ + --public-url https://a2a.example.com + +.. only:: html + + .. image:: /image/select_ai_a2a_server_demo.gif + :alt: Select AI A2A server CLI demo + :width: 100% + +Important options are ``--team`` (required), ``--host``, ``--port``, +``--public-url``, ``--description``, and ``--pool-max-size``. The +``--wallet-location`` and ``--wallet-password`` options configure an Oracle +wallet for this standalone path. If no password is provided, the command +prompts for it. + +Dynamic gateway and worker +-------------------------- + +The gateway and worker are separate processes. Start a worker for each runtime +that can reach Consul and Oracle, then start the public gateway. The worker has +no database credentials at startup; the gateway opens a temporary session by +passing the A2UI form values to the worker. + +.. code-block:: bash + + CONSUL_HTTP_URL=http://127.0.0.1:8500 \ + WORKER_ID=local-worker \ + WORKER_ADDRESS=127.0.0.1 \ + WORKER_PORT=8081 \ + select-ai a2a worker --host 127.0.0.1 --port 8081 + + select-ai a2a gateway \ + --host 127.0.0.1 \ + --port 8000 \ + --agent-url http://127.0.0.1:8000 \ + --consul-url http://127.0.0.1:8500 + +The worker options are ``--host``, ``--port``, ``--session-ttl-seconds``, and +``--session-start-timeout-seconds``. Its registration can be configured with +the ``CONSUL_HTTP_URL``, ``WORKER_ID``, ``WORKER_ADDRESS``, ``WORKER_PORT``, +and optional ``WORKER_ENDPOINT`` environment variables. The worker's +``--tls-cert-file``, ``--tls-key-file``, and ``--tls-ca-file`` options enable +gateway-to-worker mTLS; provide all three together. + +The gateway options are ``--agent-url`` (required, or ``AGENT_URL``), +``--consul-url`` (or ``CONSUL_HTTP_URL``), ``--worker-service`` (or +``WORKER_SERVICE``), and ``--session-ttl-seconds`` (or +``SESSION_TTL_SECONDS``). The optional +``--worker-tls-ca-file``, ``--worker-tls-cert-file``, and +``--worker-tls-key-file`` options configure the gateway's client side of +gateway-to-worker mTLS. These TLS settings protect the internal HTTP hop and +are separate from Oracle wallet authentication. + +Agent Card generation +--------------------- + +Print the portable A2A v0.3 card for a standalone public service: + +.. code-block:: bash + + select-ai a2a agent-card \ + --team ORACLE_AI_DATABASE_AGENT \ + --public-url https://a2a.example.com + +``--team`` and ``--public-url`` are required; use ``--description`` to replace +the default description. See :ref:`A2A Integration ` for protocol +compatibility, persistent state, gateway session behavior, wallet support, and +Google Cloud deployment details. + Command summary =============== @@ -207,3 +340,11 @@ Command summary - Summarize inline content, a file, or a URI. * - ``select-ai profile translate`` - Translate text with a saved profile. + * - ``select-ai a2a serve`` + - Start the standalone A2A server. + * - ``select-ai a2a gateway`` + - Start the public dynamic A2A gateway. + * - ``select-ai a2a worker`` + - Start the internal dynamic-session worker. + * - ``select-ai a2a agent-card`` + - Print an A2A v0.3 Agent Card. diff --git a/doc/source/user_guide/concurrent_prompt_processing.rst b/doc/source/user_guide/concurrent_prompt_processing.rst index 1f2e165..60b8aac 100644 --- a/doc/source/user_guide/concurrent_prompt_processing.rst +++ b/doc/source/user_guide/concurrent_prompt_processing.rst @@ -143,7 +143,8 @@ tools or services that can forward each answer as soon as it is ready. This recipe uses ``AsyncProfile.run_pipeline()`` to send multiple prompt/action pairs in one database round trip. This is different from Python task concurrency: the application submits a batch and receives the batch -results when the pipeline completes. +results when the pipeline completes. Pass ``attributes=...`` to apply the +same request-level profile overrides to every item in the pipeline. .. literalinclude:: ../../../recipes/concurrent_prompt_processing/async_pipeline.py :language: python diff --git a/doc/source/user_guide/conversation.rst b/doc/source/user_guide/conversation.rst index a9db8f7..dd427b0 100644 --- a/doc/source/user_guide/conversation.rst +++ b/doc/source/user_guide/conversation.rst @@ -28,8 +28,18 @@ The usual lifecycle is: ``Conversation Object model`` ***************************** .. _conversationfig: -.. figure:: /image/conversation.png - :alt: Select AI Conversation + +.. only:: html + + .. figure:: /image/conversation_object_model.svg + :alt: Select AI Conversation object model + :width: 100% + +.. only:: latex + + .. figure:: /image/conversation_object_model.png + :alt: Select AI Conversation object model + :width: 100% .. latex:clearpage:: @@ -103,6 +113,14 @@ The synchronous API is used with ``select_ai.connect()`` or length. * - ``list()`` - Iterate over conversations visible to the current user. + * - ``list_prompts()`` + - Iterate over prompts and responses stored in this conversation. + * - ``delete_prompt(conversation_prompt_id, force=False)`` + - Delete one stored prompt. Use ``force=True`` to ignore a missing prompt. + * - ``add_tag(tag_key, tag_value)`` + - Add a tag or update the value for an existing tag key. + * - ``remove_tag(tag_key, force=False)`` + - Remove a tag. Use ``force=True`` to ignore a missing tag. * - ``delete(force=False)`` - Drop the conversation. Use ``force=True`` to ignore missing-conversation errors. @@ -157,6 +175,50 @@ output:: .. latex:clearpage:: +Prompt history and tags ++++++++++++++++++++++++ + +``Conversation.list_prompts()`` returns an iterator of +``ConversationPrompt`` objects in creation order. Each object includes the +prompt identifier, prompt text, response text, conversation metadata, and +timestamps. Use the identifier with ``Conversation.delete_prompt()`` to remove +a stored prompt from the conversation. + +Conversation tags are key-value pairs. ``Conversation.add_tag()`` creates a tag +or updates the value for an existing key. ``Conversation.remove_tag()`` removes +a tag; pass ``force=True`` when a missing tag should not raise an error. + +.. autoclass:: select_ai.ConversationPrompt + :members: + +.. code-block:: python + + conversation.add_tag("PROJECT", "SELECT_AI") + prompts = list(conversation.list_prompts()) + for prompt in prompts: + print(prompt.conversation_prompt_id, prompt.prompt) + conversation.delete_prompt(prompts[-1].conversation_prompt_id) + conversation.remove_tag("PROJECT") + +The complete sample creates a conversation, stores one prompt through a chat +session, lists the resulting ``ConversationPrompt``, deletes it, and exercises +tag creation, update, and removal: + +.. literalinclude:: ../../../samples/conversation_prompts_tags.py + :language: python + :lines: 14- + +The conversation ID, prompt ID, and model response vary between runs. +Representative output is: + +output:: + + conversation history works. + Prompt : Reply with exactly: conversation history works. + Prompts after deletion: [] + +.. latex:clearpage:: + List conversations ++++++++++++++++++ @@ -223,6 +285,14 @@ The async API mirrors the synchronous API and is used with - ``async for conversation in AsyncConversation.list()`` * - ``Conversation.delete(...)`` - ``await AsyncConversation.delete(...)`` + * - ``Conversation.list_prompts()`` + - ``async for prompt in AsyncConversation.list_prompts()`` + * - ``Conversation.delete_prompt(...)`` + - ``await AsyncConversation.delete_prompt(...)`` + * - ``Conversation.add_tag(...)`` + - ``await AsyncConversation.add_tag(...)`` + * - ``Conversation.remove_tag(...)`` + - ``await AsyncConversation.remove_tag(...)`` * - ``with profile.chat_session(...)`` - ``async with async_profile.chat_session(...)`` @@ -255,6 +325,42 @@ output:: .. latex:clearpage:: +Async prompt history and tags ++++++++++++++++++++++++++++++ + +``AsyncConversation.list_prompts()`` is an async generator that yields the same +``ConversationPrompt`` objects as the synchronous API. Await +``delete_prompt()``, ``add_tag()``, and ``remove_tag()`` to manage stored +prompts and conversation tags. As with the synchronous API, use ``force=True`` +when deleting a missing prompt or removing a missing tag should succeed. + +.. code-block:: python + + await conversation.add_tag("PROJECT", "SELECT_AI") + prompts = [prompt async for prompt in conversation.list_prompts()] + for prompt in prompts: + print(prompt.conversation_prompt_id, prompt.prompt) + await conversation.delete_prompt(prompts[-1].conversation_prompt_id) + await conversation.remove_tag("PROJECT") + +The asynchronous sample performs the same prompt-history and tag-management +flow using ``AsyncConversation``: + +.. literalinclude:: ../../../samples/async/conversation_prompts_tags.py + :language: python + :lines: 13- + +The conversation ID, prompt ID, and model response vary between runs. +Representative output is: + +output:: + + conversation history works. + Prompt : Reply with exactly: conversation history works. + Prompts after deletion: [] + +.. latex:clearpage:: + Async list conversations ++++++++++++++++++++++++ diff --git a/doc/source/user_guide/credential.rst b/doc/source/user_guide/credential.rst index 19ad639..f8c97cf 100644 --- a/doc/source/user_guide/credential.rst +++ b/doc/source/user_guide/credential.rst @@ -25,6 +25,7 @@ formats. :header-rows: 1 :widths: 30 70 :align: left + :class: longtable * - AI provider - Credential format @@ -108,8 +109,13 @@ Pass ``replace=True`` when you want to recreate an existing credential with the same name. Without ``replace=True``, creating a credential that already exists raises a database error. -Sync API -++++++++ +Pass ``public_synonym=True`` to also create a public synonym with the same name +as the credential. This requires the ``CREATE PUBLIC SYNONYM`` system +privilege. The credential samples create the synonym and remove it during the +delete step. + +Create Sync API ++++++++++++++++ .. literalinclude:: ../../../samples/create_ai_credential.py :language: python @@ -121,8 +127,8 @@ output:: .. latex:clearpage:: -Async API -+++++++++ +Create Async API +++++++++++++++++ .. literalinclude:: ../../../samples/async/create_ai_credential.py :language: python @@ -140,10 +146,11 @@ Delete credential Use ``select_ai.delete_credential(...)`` to drop a credential that is no longer needed. Pass ``force=True`` when cleanup should succeed even if the credential -does not exist. +does not exist. Pass ``public_synonym=True`` to drop the credential's public +synonym as well; this requires the ``DROP PUBLIC SYNONYM`` system privilege. -Sync API -++++++++ +Delete Sync API ++++++++++++++++ .. literalinclude:: ../../../samples/delete_ai_credential.py :language: python @@ -153,10 +160,8 @@ output:: Deleted credential: my_oci_ai_profile_key -.. latex:clearpage:: - -Async API -+++++++++ +Delete Async API +++++++++++++++++ .. literalinclude:: ../../../samples/async/delete_ai_credential.py :language: python @@ -165,3 +170,39 @@ Async API output:: Deleted credential: my_oci_ai_profile_key + +.. latex:clearpage:: + +************************** +Credential access +************************** + +Use ``select_ai.grant_credential_access(...)`` and +``select_ai.revoke_credential_access(...)`` to grant or revoke credential +access for a database user or role. The asynchronous equivalents are +``async_grant_credential_access(...)`` and +``async_revoke_credential_access(...)``. + +.. code-block:: python + + select_ai.grant_credential_access( + "MY_PROVIDER_CREDENTIAL", + "APP_USER", + ) + select_ai.revoke_credential_access( + "MY_PROVIDER_CREDENTIAL", + "APP_USER", + ) + +Use ``await`` with the asynchronous equivalents: + +.. code-block:: python + + await select_ai.async_grant_credential_access( + "MY_PROVIDER_CREDENTIAL", + "APP_USER", + ) + await select_ai.async_revoke_credential_access( + "MY_PROVIDER_CREDENTIAL", + "APP_USER", + ) diff --git a/doc/source/user_guide/introduction.rst b/doc/source/user_guide/introduction.rst index ab0774d..b6ce86f 100644 --- a/doc/source/user_guide/introduction.rst +++ b/doc/source/user_guide/introduction.rst @@ -59,6 +59,10 @@ Most workflows use the same building blocks: * - Credential - Store provider and service secrets securely in Oracle Database. - :ref:`Credential ` + * - Sharing and ownership + - Share profiles, vector indexes, teams, and credentials with users or + roles and address objects owned by another schema. + - :ref:`Sharing and ownership ` * - Profile - Combine provider, credential, database object scope, and generation options into a reusable Select AI profile. diff --git a/doc/source/user_guide/profile.rst b/doc/source/user_guide/profile.rst index 9f1749d..8521790 100644 --- a/doc/source/user_guide/profile.rst +++ b/doc/source/user_guide/profile.rst @@ -26,6 +26,7 @@ The usual profile lifecycle is: list. * Create the profile with ``select_ai.Profile(...)``. * Reuse the profile later by name. +* Enable or disable the profile without deleting it. * Update profile attributes when provider settings or object scope changes. * Delete profiles that are no longer needed. @@ -65,8 +66,18 @@ Profile Object Model ******************** .. _profilefig: -.. figure:: /image/profile_provider.png - :alt: Select AI Profile and Providers + +.. only:: html + + .. figure:: /image/profile_object_model.svg + :alt: Select AI Profile object model + :width: 100% + +.. only:: latex + + .. figure:: /image/profile_object_model.png + :alt: Select AI Profile object model + :width: 100% .. latex:clearpage:: @@ -152,6 +163,28 @@ database profile and raise an error if the profile does not exist: .. latex:clearpage:: +************************** +Enable and disable Profile +************************** + +Use ``Profile.disable()`` to make an existing profile unavailable without +deleting it. Call ``Profile.enable()`` to make it available again. These +methods change the database profile status and return ``None``. + +.. literalinclude:: ../../../samples/profile_enable_disable.py + :language: python + :lines: 14- + +The sample uses ``SELECT_AI_PROFILE_NAME`` when set, otherwise it operates on +``oci_ai_profile``. It always re-enables the profile during cleanup: + +output:: + + Disabled profile: oci_ai_profile + Enabled profile: oci_ai_profile + +.. latex:clearpage:: + ************************** Update Profile ************************** @@ -305,6 +338,79 @@ Streaming is supported by ``generate()``, ``chat()``, ``narrate()``, ``explain_sql()``, ``show_sql()``, and ``show_prompt()``. It is not supported for ``run_sql()``, which returns a ``pandas.DataFrame``. +.. _request-profile-attributes: + +***************************************** +Request-level profile attribute overrides +***************************************** + +The ``attributes`` keyword accepts a mapping of profile attributes to apply to +one request. These values override the corresponding saved profile attributes +for that request only; they do not update the profile in the database. The +mapping must contain JSON-serializable values. + +Request-level attributes are available on the following synchronous APIs: + +* ``Profile.generate()``, ``chat()``, and ``narrate()``. +* SQL helpers: ``explain_sql()``, ``run_sql()``, ``show_sql()``, and + ``show_prompt()``. +* ``Session.chat()``, ``narrate()``, ``explain_sql()``, ``run_sql()``, + ``show_sql()``, and ``show_prompt()``. +* Streaming calls to the text-producing methods, by combining + ``stream=True`` with ``attributes=...``. ``run_sql()`` does not support + streaming. + +The mapping can include attributes such as ``additional_instructions``, +the integer ``seed``, and ``source_language`` or ``target_language``. Use +``ProfileAttributes`` for profile-wide defaults; use this mapping when an +override should apply only to the current request. + +For ``translate()``, pass language values through its named +``source_language`` and ``target_language`` arguments. The ``attributes`` +mapping described here applies to the generate/action APIs. + +.. code-block:: python + + request_attributes = { + "additional_instructions": "Answer in one sentence.", + "seed": 42, + "source_language": "en", + "target_language": "de", + } + + response = profile.chat( + prompt="What is Oracle Cloud Infrastructure?", + attributes=request_attributes, + ) + +The same mapping can be used with streaming and session requests: + +.. code-block:: python + + for chunk in profile.chat( + prompt="Summarize Oracle Cloud Infrastructure.", + stream=True, + attributes=request_attributes, + ): + print(chunk, end="") + + with profile.chat_session(conversation) as session: + response = session.chat( + prompt="Give one more detail.", + attributes=request_attributes, + ) + +The following sample uses ``show_prompt()`` to verify that the request-only +``additional_instructions`` value reached the generated prompt: + +.. literalinclude:: ../../../samples/profile_request_attributes.py + :language: python + :lines: 14- + +output:: + + Request attributes applied: True + .. latex:clearpage:: ************************** @@ -358,6 +464,35 @@ output:: Translate *********** +``Profile.translate()`` accepts optional ``source_language`` and +``target_language`` arguments. Per-call values take precedence over the +profile's ``source_language`` and ``target_language`` defaults. + +If ``source_language`` is omitted for both the call and the profile, source +language detection is delegated to the translation provider. If +``target_language`` is omitted, the profile's default is used; a target +language must be supplied in one of those two places. + +Configure profile-level defaults with ``ProfileAttributes`` when creating or +updating a profile: + +.. code-block:: python + + language_defaults = select_ai.ProfileAttributes( + source_language="en", + target_language="de", + ) + + profile.set_attributes(language_defaults) + print(profile.translate(text="Thank you")) + +When only the target is supplied at the call site, the provider can detect the +source language: + +.. code-block:: python + + print(profile.translate(text="Thank you", target_language="de")) + .. literalinclude:: ../../../samples/profile_translate.py :language: python :lines: 14- @@ -373,8 +508,21 @@ List profiles ************************** Profile listing returns profiles visible to the connected database user. -Instantiate ``Profile`` with one of the returned names to reuse the saved -profile. +Pass ``owner`` to list profiles from a specific schema, including a profile +shared with the connected user. Each returned object includes ``owner`` and +the owner-qualified ``qualified_name`` property. Instantiate ``Profile`` with +one of the returned names to reuse the saved profile. + +.. code-block:: python + + profile = select_ai.Profile.fetch( + "OCI_AI_PROFILE", + owner="APP_OWNER", + ) + print(profile.qualified_name) + + for profile in select_ai.Profile.list(owner="APP_OWNER"): + print(profile.qualified_name) .. literalinclude:: ../../../samples/profiles_list.py :language: python diff --git a/doc/source/user_guide/profile_attributes.rst b/doc/source/user_guide/profile_attributes.rst index fba5783..047132a 100644 --- a/doc/source/user_guide/profile_attributes.rst +++ b/doc/source/user_guide/profile_attributes.rst @@ -79,9 +79,10 @@ Attribute groups for requests that use the profile. * - ``source_language``, ``target_language`` - Set default languages for ``Profile.translate()`` and - ``AsyncProfile.translate()``. If no source language is configured or - supplied per call, the provider detects it. A target language must be - supplied either per call or in the profile. + ``AsyncProfile.translate()``. Per-call values override these defaults. + If no source language is configured or supplied per call, the provider + detects it. A target language must be supplied either per call or in the + profile. * - ``vector_index_name``, ``enable_sources``, ``enable_source_offsets``, ``enable_custom_source_uri`` - Configures retrieval-augmented generation and source reporting for diff --git a/doc/source/user_guide/provider.rst b/doc/source/user_guide/provider.rst index f75f007..200b71c 100644 --- a/doc/source/user_guide/provider.rst +++ b/doc/source/user_guide/provider.rst @@ -131,6 +131,115 @@ Custom provider endpoint: prompt="How many customers do I have?", ) +Cloud provider profile samples +------------------------------ + +The repository includes complete profile-creation samples for AWS Bedrock, +Azure OpenAI, and Google Gemini. Each sample: + +* grants the Select AI database user HTTP access to the provider endpoint; +* creates or replaces a provider credential; and +* creates or replaces a profile, then sends a test chat request. + +Run the samples from the repository root after setting the common database +variables: + +* ``SELECT_AI_ADMIN_USER`` and ``SELECT_AI_ADMIN_PASSWORD``; +* ``SELECT_AI_USER`` and ``SELECT_AI_PASSWORD``; +* ``SELECT_AI_DB_CONNECT_STRING``; and +* the provider-specific API key or access-key variables described below. + +Do not store production secrets in source files or commit them to the +repository. Adjust the sample region, model, resource, deployment, and object +names for the provider account and database used by your application. + +AWS Bedrock ++++++++++++ + +The `AWS Bedrock profile sample `__ +uses ``AWSProvider`` with the following provider settings: + +* ``region``: AWS Bedrock region; +* ``model``: text-generation model identifier; and +* ``embedding_model``: embedding model identifier. + +Set ``AWS_ACCESS_KEY_ID`` and ``AWS_SECRET_ACCESS_KEY`` before running it: + +.. code-block:: bash + + python samples/profile_create_aws.py + +.. literalinclude:: ../../../samples/profile_create_aws.py + :language: python + :lines: 8- + +The sample creates ``aws_bedrock_meta_prf`` and prints the saved attributes +and a test response: + +output:: + + Created profile: aws_bedrock_meta_prf + {'credential_name': 'AWS_CRED', 'provider': AWSProvider(...), ...} + Chat response: AWS chat succeeded. + +Azure OpenAI +++++++++++++ + +The `Azure OpenAI profile sample `__ +uses ``AzureProvider`` with the following provider settings: + +* ``azure_resource_name``: Azure OpenAI resource name; +* ``azure_deployment_name``: text-generation deployment name; and +* ``azure_embedding_deployment_name``: embedding deployment name. + +Set ``AZURE_API_KEY`` before running it: + +.. code-block:: bash + + python samples/profile_create_azure.py + +.. literalinclude:: ../../../samples/profile_create_azure.py + :language: python + :lines: 8- + +The sample creates ``azureai_prf``, fetches it to verify the saved profile, +and prints a test response: + +output:: + + Profile(profile_name=azureai_prf, ...) + Created profile: azureai_prf + {'credential_name': 'AZUREAI_CRED', 'provider': AzureProvider(...), ...} + Chat response: Azure chat succeeded. + +Google Gemini (GCP) ++++++++++++++++++++ + +The `Google Gemini profile sample `__ +uses ``GoogleProvider`` with the following provider settings: + +* ``model``: Gemini text-generation model identifier; and +* ``embedding_model``: Gemini embedding model identifier. + +Set ``GOOGLE_API_KEY`` before running it: + +.. code-block:: bash + + python samples/profile_create_gcp.py + +.. literalinclude:: ../../../samples/profile_create_gcp.py + :language: python + :lines: 8- + +The sample creates ``google_gemini_3_6_flash`` and prints the saved attributes +and a test response: + +output:: + + Created profile: google_gemini_3_6_flash + {'credential_name': 'GOOGLE_CRED', 'provider': GoogleProvider(...), ...} + Chat response: GCP chat succeeded. + .. latex:clearpage:: ``Provider`` diff --git a/doc/source/user_guide/sharing.rst b/doc/source/user_guide/sharing.rst new file mode 100644 index 0000000..e05af91 --- /dev/null +++ b/doc/source/user_guide/sharing.rst @@ -0,0 +1,258 @@ +.. _sharing: + +********************** +Sharing and ownership +********************** + +Select AI objects can be shared with a database user or role. Sharing must be +performed by the object owner, and the grantee must have the package and +database privileges required to use the object. + +The sharing APIs are available in both synchronous and asynchronous forms: + +.. list-table:: Sharing API summary + :header-rows: 1 + :widths: 25 37 38 + :align: left + + * - Object + - Synchronous API + - Asynchronous API + * - Profile + - ``Profile.grant_access()`` and ``revoke_access()`` + - ``AsyncProfile.grant_access()`` and ``revoke_access()`` + * - Vector index + - ``VectorIndex.grant_access()`` and ``revoke_access()`` + - ``AsyncVectorIndex.grant_access()`` and ``revoke_access()`` + * - Agent team + - ``Team.grant_access()`` and ``revoke_access()`` + - ``AsyncTeam.grant_access()`` and ``revoke_access()`` + * - Credential + - ``grant_credential_access()`` and ``revoke_credential_access()`` + - ``async_grant_credential_access()`` and + ``async_revoke_credential_access()`` + +Each access method accepts a database user or role name. Access methods return +``None`` after the database grant or revoke operation completes. + +Profiles +======== + +``Profile.fetch()`` and ``Profile.list()`` accept an optional ``owner`` +argument. If ``owner`` is omitted, the current schema is used. The fetched +object records its owner in ``profile.owner`` and exposes an owner-qualified +name through ``profile.qualified_name``. + +.. code-block:: python + + profile = select_ai.Profile.fetch( + "OCI_AI_PROFILE", + owner="APP_OWNER", + ) + print(profile.owner) + print(profile.qualified_name) + + profiles = select_ai.Profile.list( + profile_name_pattern="^OCI_AI_PROFILE$", + owner="APP_OWNER", + ) + for profile in profiles: + print(profile.qualified_name) + +Grant and revoke access on the owner-side object: + +.. code-block:: python + + profile.grant_access("APP_USER") + profile.revoke_access("APP_USER") + +The asynchronous APIs use the same ``owner`` argument and return an async +iterator from ``AsyncProfile.list()``: + +.. code-block:: python + + profile = await select_ai.AsyncProfile.fetch( + "OCI_AI_PROFILE", + owner="APP_OWNER", + ) + profiles = [ + item + async for item in select_ai.AsyncProfile.list( + "^OCI_AI_PROFILE$", + owner="APP_OWNER", + ) + ] + print(profile.qualified_name) + print([item.qualified_name for item in profiles]) + + await profile.grant_access("APP_USER") + await profile.revoke_access("APP_USER") + +Vector indexes +============== + +``VectorIndex.fetch()`` and ``VectorIndex.list()`` also accept an optional +``owner`` argument. A vector index exposes ``owner`` and the owner-qualified +``qualified_name`` property in the same way as a profile. + +.. code-block:: python + + index = select_ai.VectorIndex.fetch( + "PRODUCT_DOCS", + owner="APP_OWNER", + ) + indexes = select_ai.VectorIndex.list( + index_name_pattern="^PRODUCT_DOCS$", + owner="APP_OWNER", + ) + print(index.qualified_name) + print([item.qualified_name for item in indexes]) + + index.grant_access("APP_USER") + index.revoke_access("APP_USER") + +The async form mirrors the synchronous API: + +.. code-block:: python + + index = await select_ai.AsyncVectorIndex.fetch( + "PRODUCT_DOCS", + owner="APP_OWNER", + ) + indexes = [ + item + async for item in select_ai.AsyncVectorIndex.list( + "^PRODUCT_DOCS$", + owner="APP_OWNER", + ) + ] + await index.grant_access("APP_USER") + await index.revoke_access("APP_USER") + +Agent teams +=========== + +Agent teams support synchronous and asynchronous access grants and revokes: + +.. code-block:: python + + from select_ai.agent import Team + + team = Team.fetch("MOVIE_AGENT_TEAM") + team.grant_access("APP_USER") + team.revoke_access("APP_USER") + +.. code-block:: python + + from select_ai.agent import AsyncTeam + + team = await AsyncTeam.fetch("MOVIE_AGENT_TEAM") + await team.grant_access("APP_USER") + await team.revoke_access("APP_USER") + +The current team APIs do not accept an ``owner`` argument. Team fetch and list +operations use the connected user's agent-team views; owner-qualified names +are currently provided for profiles and vector indexes. + +Credentials +=========== + +Credential access is exposed as module-level functions because credentials are +owned in the current schema: + +.. code-block:: python + + select_ai.grant_credential_access( + "MY_PROVIDER_CREDENTIAL", + "APP_USER", + ) + select_ai.revoke_credential_access( + "MY_PROVIDER_CREDENTIAL", + "APP_USER", + ) + +Use the asynchronous equivalents with ``await``: + +.. code-block:: python + + await select_ai.async_grant_credential_access( + "MY_PROVIDER_CREDENTIAL", + "APP_USER", + ) + await select_ai.async_revoke_credential_access( + "MY_PROVIDER_CREDENTIAL", + "APP_USER", + ) + +Credential creation and deletion optionally manage a public synonym with the +same name as the credential. Creating or dropping a public synonym requires the +corresponding ``CREATE PUBLIC SYNONYM`` or ``DROP PUBLIC SYNONYM`` privilege: + +.. code-block:: python + + select_ai.create_credential( + credential=credential, + replace=True, + public_synonym=True, + ) + select_ai.delete_credential( + "MY_PROVIDER_CREDENTIAL", + force=True, + public_synonym=True, + ) + +The async functions ``async_create_credential()`` and +``async_delete_credential()`` accept the same ``public_synonym`` option. + +Complete sharing sample +======================== + +The following samples fetch owner-qualified profiles and vector indexes and +exercise grant/revoke operations for profiles, vector indexes, teams, and +credentials. Set ``SELECT_AI_SHARE_GRANTEE`` to a database user or role. The +objects named by the other optional environment variables must already exist, +and the scripts should run as their owner. + +.. literalinclude:: ../../../samples/sharing.py + :language: python + :lines: 14- + +Representative synchronous output is: + +output:: + + Profile: APP_OWNER.OCI_AI_PROFILE + Profiles: ['APP_OWNER.OCI_AI_PROFILE'] + Granted profile access to: APP_USER + Revoked profile access from: APP_USER + Vector index: APP_OWNER.PRODUCT_DOCS + Vector indexes: ['APP_OWNER.PRODUCT_DOCS'] + Granted vector index access to: APP_USER + Revoked vector index access from: APP_USER + Granted team access to: APP_USER + Revoked team access from: APP_USER + Granted credential access to: APP_USER + Revoked credential access from: APP_USER + +The asynchronous sample uses the corresponding async methods: + +.. literalinclude:: ../../../samples/async/sharing.py + :language: python + :lines: 14- + +Representative asynchronous output has the same form: + +output:: + + Profile: APP_OWNER.ASYNC_OCI_AI_PROFILE + Profiles: ['APP_OWNER.ASYNC_OCI_AI_PROFILE'] + Granted profile access to: APP_USER + Revoked profile access from: APP_USER + Vector index: APP_OWNER.PRODUCT_DOCS + Vector indexes: ['APP_OWNER.PRODUCT_DOCS'] + Granted vector index access to: APP_USER + Revoked vector index access from: APP_USER + Granted team access to: APP_USER + Revoked team access from: APP_USER + Granted credential access to: APP_USER + Revoked credential access from: APP_USER diff --git a/doc/source/user_guide/synthetic_data.rst b/doc/source/user_guide/synthetic_data.rst index 726b13f..d6b6caa 100644 --- a/doc/source/user_guide/synthetic_data.rst +++ b/doc/source/user_guide/synthetic_data.rst @@ -146,7 +146,7 @@ output:: .. latex:clearpage:: Single Table Async API -+++++++++++++++++++++ ++++++++++++++++++++++++ .. literalinclude:: ../../../samples/async/profile_gen_single_table_synthetic_data.py :language: python diff --git a/doc/source/user_guide/vector_index.rst b/doc/source/user_guide/vector_index.rst index f90e4d4..689fd60 100644 --- a/doc/source/user_guide/vector_index.rst +++ b/doc/source/user_guide/vector_index.rst @@ -40,8 +40,18 @@ The usual lifecycle is: **************************** .. _vectorindexfig: -.. figure:: /image/vector_index.png - :alt: Select AI Vector Index + +.. only:: html + + .. figure:: /image/vector_index_object_model.svg + :alt: Select AI Vector Index object model + :width: 100% + +.. only:: latex + + .. figure:: /image/vector_index_object_model.png + :alt: Select AI Vector Index object model + :width: 100% .. latex:clearpage:: @@ -155,12 +165,13 @@ Important lifecycle methods: ``replace=True`` and the index already exists, the existing index is dropped and recreated. Use ``wait_for_completion=True`` when the next step depends on the initial load being complete. - * - ``fetch(index_name)`` + * - ``fetch(index_name, owner=None)`` - Build a ``VectorIndex`` proxy from database metadata, including attributes and the linked profile when it still exists. - * - ``list(index_name_pattern=".*")`` - - Iterate over vector indexes visible to the current user. The pattern is - evaluated with Oracle ``REGEXP_LIKE``. + * - ``list(index_name_pattern=".*", owner=None)`` + - Iterate over vector indexes visible to the current user or owned by the + specified schema. The pattern is evaluated with Oracle + ``REGEXP_LIKE``. * - ``set_attribute()`` and ``set_attributes()`` - Update one or more index attributes. * - ``get_next_refresh_timestamp()`` @@ -214,9 +225,19 @@ Fetch vector index +++++++++++++++++++++++++++ You can fetch the vector index attributes and associated AI profile using -the class method ``VectorIndex.fetch(index_name)``. Fetch is useful when the -index was created earlier or by another process and you want to inspect or -update it without recreating the original Python object. +the class method ``VectorIndex.fetch(index_name, owner=None)``. Pass ``owner`` +for an index shared from another schema. The returned object exposes the +resolved ``owner`` and the owner-qualified ``qualified_name``. Fetch is useful +when the index was created earlier or by another process and you want to inspect +or update it without recreating the original Python object. + +.. code-block:: python + + vector_index = select_ai.VectorIndex.fetch( + "PRODUCT_DOCS", + owner="APP_OWNER", + ) + print(vector_index.qualified_name) .. literalinclude:: ../../../samples/vector_index_fetch.py :language: python diff --git a/gcloud/README.md b/gcloud/README.md index 830cebc..bf005a8 100644 --- a/gcloud/README.md +++ b/gcloud/README.md @@ -17,6 +17,15 @@ hosted behind the same A2A endpoint. Consul preserves session and task affinity when requests reach different gateway instances. Oracle Database capacity and the configured session TTL remain the limiting factors. +![Select AI A2A deployment architecture](../doc/source/image/a2a_architecture.svg) + +The A2A commands are cloud-neutral: `select-ai a2a serve`, +`select-ai a2a gateway`, and `select-ai a2a worker` can run as processes or +containers on any cloud platform, a Kubernetes cluster, or self-managed +infrastructure with the required Oracle and Consul connectivity. The scripts +in this directory are optional Google Cloud automation for the Cloud Run/GKE +topologies shown below. + ## What the A2A client connects to ### Standalone server @@ -24,12 +33,6 @@ the configured session TTL remain the limiting factors. The standalone deployment is one Cloud Run A2A service for one configured Oracle database and one Select AI team. -```text -A2A client ── A2A JSON-RPC ──► Cloud Run A2A server ──► Oracle Database - fixed credentials - fixed team -``` - The service receives its database credentials from Secret Manager. The A2A client can discover the Agent Card and immediately send a database prompt. The server supports blocking tasks, task polling, and streaming responses. @@ -47,15 +50,6 @@ Use [standalone deployment](standalone/README.md) for the deployment details. The gateway deployment provides one public A2A endpoint for users who choose the database and Select AI team at runtime. -```text -A2A client ── A2A JSON-RPC ──► Cloud Run gateway - │ A2UI connection form - ▼ - GKE worker session ──► Oracle Database - ▲ - │ Consul session/task routing -``` - The client first sends a message and receives an A2UI connection form. After the client submits the DSN, username, password, and team name, the gateway opens a temporary worker session. Subsequent A2A messages use that session and diff --git a/samples/README.md b/samples/README.md index 8e0c860..c083e41 100644 --- a/samples/README.md +++ b/samples/README.md @@ -21,7 +21,148 @@ export TNS_ADMIN= Some of the new samples use this optional environment variable: - `SELECT_AI_PROFILE_NAME` — existing profile for the conversation and - supervised-team samples. + supervised-team, profile lifecycle, translation, and request-attribute + samples. +- `SELECT_AI_SHARE_GRANTEE` — database user or role used by the sharing + sample. +- `SELECT_AI_OWNER`, `SELECT_AI_VECTOR_INDEX_NAME`, `SELECT_AI_TEAM_NAME`, and + `SELECT_AI_CREDENTIAL_NAME` — optional names used by the sharing sample. + +## Cloud provider profiles + +Create a profile and run a test chat against one of the supported cloud AI +providers: + +```bash +export AWS_ACCESS_KEY_ID= +export AWS_SECRET_ACCESS_KEY= +python samples/profile_create_aws.py + +export AZURE_API_KEY= +python samples/profile_create_azure.py + +export GOOGLE_API_KEY= +python samples/profile_create_gcp.py +``` + +The scripts create or replace the provider credential and profile, grant the +database user's HTTP access to the provider endpoint, and print a test chat +response. See the +[provider documentation](../doc/source/user_guide/provider.rst#cloud-provider-profile-samples) +for the provider settings and representative output. + +## Supervised agent teams + +Create a team with a dedicated supervisor agent, run a prompt through the +supervisor workflow, and inspect the database-generated supervisor task: + +```bash +python samples/agent/team_supervisor_inspect.py +python samples/agent/async/team_supervisor_inspect.py +``` + +The samples set `AgentAttributes(supervisor=True)` on the coordinating agent +and pass that agent's name as `TeamAttributes.supervisor_agent`. The database +populates `supervisor_task` when the team is created. + +## Agent definitions and tool inspection + +Retrieve a canonical database definition: + +```bash +python samples/agent/get_definition.py +python samples/agent/async/get_definition.py +``` + +Inspect and directly invoke a PL/SQL tool: + +```bash +python samples/agent/tool_run_describe.py +python samples/agent/async/tool_run_describe.py +``` + +The supervised-team samples above also demonstrate +`Team.describe_team()`/`AsyncTeam.describe_team()` and +`Team.list_tools()`/`AsyncTeam.list_tools()`. + +## Agent execution history + +Inspect the latest team execution and its related task and tool history: + +```bash +python samples/agent/history_list.py +python samples/agent/async/agent_history_list.py +``` + +The async sample uses `AsyncTeamHistory`, `AsyncTaskHistory`, and +`AsyncToolHistory` with async iteration. + +## Profile lifecycle controls + +Disable and re-enable an existing profile without deleting it: + +```bash +python samples/profile_enable_disable.py +python samples/async/profile_enable_disable.py +``` + +The scripts use `SELECT_AI_PROFILE_NAME` when set; otherwise they use the +sample profile name `oci_ai_profile`. + +## Translation language defaults + +Translate text while letting the provider detect the source language: + +```bash +python samples/profile_translate.py +python samples/async/profile_translate.py +``` + +These samples use `SELECT_AI_PROFILE_NAME` when set and pass only the target +language at the call site. The default profile names are `oci_ai_profile` for +the synchronous sample and `async_oci_ai_profile` for the asynchronous sample. +Profile-level language defaults can be configured with `ProfileAttributes` when +the target is also omitted. + +## Request-level profile attribute overrides + +Override profile attributes for one request without changing the saved profile: + +```bash +python samples/profile_request_attributes.py +python samples/async/profile_request_attributes.py +``` + +The samples demonstrate `additional_instructions`, integer `seed`, and source +and target language settings passed through the `attributes` mapping. + +## Sharing and ownership + +Inspect owner-qualified profiles and vector indexes and grant/revoke access to +profiles, vector indexes, teams, and credentials: + +```bash +python samples/sharing.py +python samples/async/sharing.py +``` + +Set `SELECT_AI_SHARE_GRANTEE` before running the scripts. The objects must +already exist, and the scripts should run as their owner. The credential +creation and deletion samples also create and remove a public synonym. + +## Conversation prompt history and tags + +Create a conversation, list its stored prompts, delete a prompt, and manage +conversation tags: + +```bash +python samples/conversation_prompts_tags.py +python samples/async/conversation_prompts_tags.py +``` + +The scripts use `SELECT_AI_PROFILE_NAME` when set; otherwise they use the +sample profile name `oci_ai_profile`. See the conversation user guide for +representative output. ## A2A non-blocking task polling @@ -42,6 +183,21 @@ The sample sends the A2A v0.3 `message/send` request with `configuration.blocking: false`, prints the returned task ID, and polls `tasks/get`. Edit `ENDPOINT` or `PROMPT` at the top of the script if needed. +Representative output is: + +```text +Task 42b...: submitted +Task 42b...: working +Task 42b...: completed +{ + "id": "42b...", + "status": {"state": "completed", "timestamp": "..."}, + "artifacts": [{"name": "database-agent-result", "parts": ["..."]}] +} +``` + +Task IDs, timestamps, and database answers vary between runs. + To compare it with the default blocking behavior, run: ```bash @@ -52,6 +208,17 @@ This sample intentionally omits `configuration.blocking`. The server waits for the database work to finish and returns the completed Task in the initial `message/send` response; no polling is needed. +Representative output is: + +```text +Task 7e1...: completed +{ + "id": "7e1...", + "status": {"state": "completed", "timestamp": "..."}, + "artifacts": [{"name": "database-agent-result", "parts": ["..."]}] +} +``` + ## A2A dynamic gateway The dynamic gateway samples submit the A2UI database connection form, open a @@ -69,6 +236,10 @@ python samples/a2a/gateway/task_poll.py See that README for local Consul, worker, and gateway startup instructions. +The full A2A architecture, protocol details, session lifecycle, and Google +Cloud deployment explanation are in the +[A2A user guide](../doc/source/user_guide/a2a.rst). + `SELECT_AI_DB_CONNECT_STRING` can be in any one of the following formats diff --git a/samples/a2a/gateway/README.md b/samples/a2a/gateway/README.md index c3349da..d5056f4 100644 --- a/samples/a2a/gateway/README.md +++ b/samples/a2a/gateway/README.md @@ -17,6 +17,17 @@ send `A2A-Version: 1.0` and use `SendMessage`, `GetTask`, `ListTasks`, and `CancelTask`; its non-blocking option is `configuration.returnImmediately`. The gateway accepts both versions, but streaming is disabled in both. +The sample scripts import `call`, `connect`, `send_prompt`, and +`print_task_summary` from the adjacent +[`_common.py`](https://github.com/oracle/python-select-ai/blob/main/samples/a2a/gateway/_common.py) +file. This is a +repository-local sample helper, not an additional Python dependency. It sends +the JSON-RPC requests, performs the A2UI connection-form handshake using the +environment variables below, and formats the final task result. If you copy a +script elsewhere, copy +[`_common.py`](https://github.com/oracle/python-select-ai/blob/main/samples/a2a/gateway/_common.py) +with it or replace those helpers with your own A2A client code. + ## Local setup Install the A2A extra if necessary: diff --git a/samples/agent/async/team_supervisor_inspect.py b/samples/agent/async/team_supervisor_inspect.py index 90acc9c..f2d4ba6 100644 --- a/samples/agent/async/team_supervisor_inspect.py +++ b/samples/agent/async/team_supervisor_inspect.py @@ -8,8 +8,9 @@ # ----------------------------------------------------------------------------- # agent/async/team_supervisor_inspect.py # -# Async version of the supervised team creation and inspection sample. -# Requires SELECT_AI_PROFILE_NAME to name an existing AI profile. +# Async version of the supervised team creation, execution, and inspection +# sample. +# Uses SELECT_AI_PROFILE_NAME when set; otherwise uses oci_ai_profile. # ----------------------------------------------------------------------------- import asyncio @@ -25,11 +26,15 @@ TaskAttributes, TeamAttributes, ) +from select_ai.conversation import ( + AsyncConversation, + ConversationAttributes, +) user = os.getenv("SELECT_AI_USER") password = os.getenv("SELECT_AI_PASSWORD") dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") -profile_name = os.getenv("SELECT_AI_PROFILE_NAME", "LLAMA_4_MAVERICK") +profile_name = os.getenv("SELECT_AI_PROFILE_NAME", "oci_ai_profile") async def main(): @@ -70,22 +75,47 @@ async def main(): ), ) - await task.create(replace=True) - await worker.create(replace=True) - await supervisor.create(replace=True) - await team.create(replace=True) + conversation = AsyncConversation( + attributes=ConversationAttributes( + title="Supervised team sample", + description="Conversation for the supervised team sample", + ) + ) + created_objects = [] + conversation_created = False try: + await task.create(replace=True) + created_objects.append(task) + await worker.create(replace=True) + created_objects.append(worker) + await supervisor.create(replace=True) + created_objects.append(supervisor) + await team.create(replace=True) + created_objects.append(team) + + await conversation.create() + conversation_created = True + response = await team.run( + prompt="What is the capital of France? Answer in one sentence.", + params={"conversation_id": conversation.conversation_id}, + ) + if response is None or ( + isinstance(response, str) and response.startswith("Task failed:") + ): + raise RuntimeError(f"Supervised team run failed: {response}") + print("Team response:", response) + fetched = await AsyncTeam.fetch(team.team_name) print("Supervisor agent:", fetched.attributes.supervisor_agent) print("Supervisor task:", fetched.attributes.supervisor_task) print("Team description:", await team.describe_team()) print("Team tools:", await team.list_tools()) finally: - await team.delete(force=True) - await supervisor.delete(force=True) - await worker.delete(force=True) - await task.delete(force=True) + if conversation_created: + await conversation.delete(force=True) + for obj in reversed(created_objects): + await obj.delete(force=True) asyncio.run(main()) diff --git a/samples/agent/team_supervisor_inspect.py b/samples/agent/team_supervisor_inspect.py index 5c5cc98..def79d7 100644 --- a/samples/agent/team_supervisor_inspect.py +++ b/samples/agent/team_supervisor_inspect.py @@ -8,8 +8,9 @@ # ----------------------------------------------------------------------------- # agent/team_supervisor_inspect.py # -# Create a supervised team, then inspect its metadata and available tools. -# Requires SELECT_AI_PROFILE_NAME to name an existing AI profile. +# Create a supervised team, run it, then inspect its metadata and available +# tools. +# Uses SELECT_AI_PROFILE_NAME when set; otherwise uses oci_ai_profile. # ----------------------------------------------------------------------------- import os @@ -24,11 +25,12 @@ Team, TeamAttributes, ) +from select_ai.conversation import Conversation, ConversationAttributes user = os.getenv("SELECT_AI_USER") password = os.getenv("SELECT_AI_PASSWORD") dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") -profile_name = os.getenv("SELECT_AI_PROFILE_NAME", "LLAMA_4_MAVERICK") +profile_name = os.getenv("SELECT_AI_PROFILE_NAME", "oci_ai_profile") suffix = uuid.uuid4().hex.upper() select_ai.connect(user=user, password=password, dsn=dsn) @@ -67,19 +69,44 @@ ), ) -task.create(replace=True) -worker.create(replace=True) -supervisor.create(replace=True) -team.create(replace=True) +conversation = Conversation( + attributes=ConversationAttributes( + title="Supervised team sample", + description="Conversation for the supervised team sample", + ) +) +created_objects = [] +conversation_created = False try: + task.create(replace=True) + created_objects.append(task) + worker.create(replace=True) + created_objects.append(worker) + supervisor.create(replace=True) + created_objects.append(supervisor) + team.create(replace=True) + created_objects.append(team) + + conversation.create() + conversation_created = True + response = team.run( + prompt="What is the capital of France? Answer in one sentence.", + params={"conversation_id": conversation.conversation_id}, + ) + if response is None or ( + isinstance(response, str) and response.startswith("Task failed:") + ): + raise RuntimeError(f"Supervised team run failed: {response}") + print("Team response:", response) + fetched = Team.fetch(team.team_name) print("Supervisor agent:", fetched.attributes.supervisor_agent) print("Supervisor task:", fetched.attributes.supervisor_task) print("Team description:", team.describe_team()) print("Team tools:", team.list_tools()) finally: - team.delete(force=True) - supervisor.delete(force=True) - worker.delete(force=True) - task.delete(force=True) + if conversation_created: + conversation.delete(force=True) + for obj in reversed(created_objects): + obj.delete(force=True) diff --git a/samples/async/create_ai_credential.py b/samples/async/create_ai_credential.py index 45af376..3aed3ed 100644 --- a/samples/async/create_ai_credential.py +++ b/samples/async/create_ai_credential.py @@ -36,7 +36,9 @@ async def main(): "fingerprint": default_config["fingerprint"], } await select_ai.async_create_credential( - credential=credential, replace=True + credential=credential, + replace=True, + public_synonym=True, ) print("Created credential: ", credential["credential_name"]) diff --git a/samples/async/delete_ai_credential.py b/samples/async/delete_ai_credential.py index 94e043e..06816cb 100644 --- a/samples/async/delete_ai_credential.py +++ b/samples/async/delete_ai_credential.py @@ -24,7 +24,9 @@ async def main(): await select_ai.async_connect(user=user, password=password, dsn=dsn) await select_ai.async_delete_credential( - credential_name="my_oci_ai_profile_key", force=True + credential_name="my_oci_ai_profile_key", + force=True, + public_synonym=True, ) print("Deleted credential: my_oci_ai_profile_key") diff --git a/samples/async/profile_enable_disable.py b/samples/async/profile_enable_disable.py new file mode 100644 index 0000000..30c1fc8 --- /dev/null +++ b/samples/async/profile_enable_disable.py @@ -0,0 +1,37 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------- +# async/profile_enable_disable.py +# +# Asynchronously disable and re-enable an existing Select AI profile. +# ----------------------------------------------------------------------------- + +import asyncio +import os + +import select_ai + +user = os.getenv("SELECT_AI_USER") +password = os.getenv("SELECT_AI_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +profile_name = os.getenv("SELECT_AI_PROFILE_NAME", "oci_ai_profile") + + +async def main(): + await select_ai.async_connect(user=user, password=password, dsn=dsn) + profile = await select_ai.AsyncProfile(profile_name=profile_name) + + await profile.disable() + try: + print("Disabled profile:", profile.profile_name) + finally: + await profile.enable() + print("Enabled profile:", profile.profile_name) + + +asyncio.run(main()) diff --git a/samples/async/profile_request_attributes.py b/samples/async/profile_request_attributes.py new file mode 100644 index 0000000..6a9f959 --- /dev/null +++ b/samples/async/profile_request_attributes.py @@ -0,0 +1,45 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------- +# async/profile_request_attributes.py +# +# Override profile attributes for one async request without changing the saved +# profile. +# ----------------------------------------------------------------------------- + +import asyncio +import os + +import select_ai + +user = os.getenv("SELECT_AI_USER") +password = os.getenv("SELECT_AI_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +profile_name = os.getenv("SELECT_AI_PROFILE_NAME", "async_oci_ai_profile") + + +async def main(): + await select_ai.async_connect(user=user, password=password, dsn=dsn) + async_profile = await select_ai.AsyncProfile(profile_name=profile_name) + + instruction = "Include the exact marker PYSAI_REQUEST_ATTRIBUTES_SAMPLE." + request_attributes = { + "additional_instructions": instruction, + "seed": 42, + "source_language": "en", + "target_language": "de", + } + + prompt = await async_profile.show_prompt( + prompt="Tell me about Oracle Cloud Infrastructure.", + attributes=request_attributes, + ) + print("Request attributes applied:", instruction in prompt) + + +asyncio.run(main()) diff --git a/samples/async/profile_translate.py b/samples/async/profile_translate.py index a098fef..c1ca9e0 100644 --- a/samples/async/profile_translate.py +++ b/samples/async/profile_translate.py @@ -8,7 +8,7 @@ # ----------------------------------------------------------------------------- # async/profile_translate.py # -# Translate text from language to another +# Translate text while allowing the provider to detect its source language. # ----------------------------------------------------------------------------- import asyncio @@ -19,16 +19,16 @@ user = os.getenv("SELECT_AI_USER") password = os.getenv("SELECT_AI_PASSWORD") dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +profile_name = os.getenv("SELECT_AI_PROFILE_NAME", "async_oci_ai_profile") async def main(): await select_ai.async_connect(user=user, password=password, dsn=dsn) async_profile = await select_ai.AsyncProfile( - profile_name="async_oci_ai_profile", + profile_name=profile_name, ) response = await async_profile.translate( text="Thank you", - source_language="en", target_language="de", ) print(response) diff --git a/samples/async/sharing.py b/samples/async/sharing.py new file mode 100644 index 0000000..d378d3b --- /dev/null +++ b/samples/async/sharing.py @@ -0,0 +1,90 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------- +# async/sharing.py +# +# Inspect owner-qualified objects and grant/revoke access to shared objects +# using asynchronous APIs. The objects must already exist and the script must +# run as their owner. +# ----------------------------------------------------------------------------- + +import asyncio +import os + +import select_ai +from select_ai.agent import AsyncTeam + +user = os.getenv("SELECT_AI_USER") +password = os.getenv("SELECT_AI_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +owner = os.getenv("SELECT_AI_OWNER") or None +grantee = os.getenv("SELECT_AI_SHARE_GRANTEE") +profile_name = os.getenv("SELECT_AI_PROFILE_NAME", "async_oci_ai_profile") +vector_index_name = os.getenv( + "SELECT_AI_VECTOR_INDEX_NAME", "test_vector_index" +) +team_name = os.getenv("SELECT_AI_TEAM_NAME", "MOVIE_AGENT_TEAM") +credential_name = os.getenv( + "SELECT_AI_CREDENTIAL_NAME", "my_oci_ai_profile_key" +) + +if not grantee: + raise RuntimeError( + "Set SELECT_AI_SHARE_GRANTEE to a database user or role" + ) + + +async def main(): + await select_ai.async_connect(user=user, password=password, dsn=dsn) + + profile = await select_ai.AsyncProfile.fetch(profile_name, owner=owner) + profiles = [ + item + async for item in select_ai.AsyncProfile.list( + f"^{profile.profile_name}$", + owner=profile.owner, + ) + ] + print("Profile:", profile.qualified_name) + print("Profiles:", [item.qualified_name for item in profiles]) + await profile.grant_access(grantee) + print("Granted profile access to:", grantee) + await profile.revoke_access(grantee) + print("Revoked profile access from:", grantee) + + vector_index = await select_ai.AsyncVectorIndex.fetch( + vector_index_name, + owner=owner, + ) + indexes = [ + item + async for item in select_ai.AsyncVectorIndex.list( + f"^{vector_index.index_name}$", + owner=vector_index.owner, + ) + ] + print("Vector index:", vector_index.qualified_name) + print("Vector indexes:", [item.qualified_name for item in indexes]) + await vector_index.grant_access(grantee) + print("Granted vector index access to:", grantee) + await vector_index.revoke_access(grantee) + print("Revoked vector index access from:", grantee) + + team = await AsyncTeam.fetch(team_name) + await team.grant_access(grantee) + print("Granted team access to:", grantee) + await team.revoke_access(grantee) + print("Revoked team access from:", grantee) + + await select_ai.async_grant_credential_access(credential_name, grantee) + print("Granted credential access to:", grantee) + await select_ai.async_revoke_credential_access(credential_name, grantee) + print("Revoked credential access from:", grantee) + + +asyncio.run(main()) diff --git a/samples/conversation_prompts_tags.py b/samples/conversation_prompts_tags.py index a63bb26..9af7e23 100644 --- a/samples/conversation_prompts_tags.py +++ b/samples/conversation_prompts_tags.py @@ -19,6 +19,7 @@ user = os.getenv("SELECT_AI_USER") password = os.getenv("SELECT_AI_PASSWORD") dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +profile_name = os.getenv("SELECT_AI_PROFILE_NAME", "oci_ai_profile") select_ai.connect(user=user, password=password, dsn=dsn) @@ -26,7 +27,7 @@ attributes=select_ai.ConversationAttributes(title="Prompt and tag sample") ) conversation.create() -profile = select_ai.Profile(profile_name="oci_ai_profile") +profile = select_ai.Profile(profile_name=profile_name) try: conversation.add_tag("PROJECT", "SELECT_AI") diff --git a/samples/create_ai_credential.py b/samples/create_ai_credential.py index d942300..865d48b 100644 --- a/samples/create_ai_credential.py +++ b/samples/create_ai_credential.py @@ -34,5 +34,9 @@ "private_key": key_contents, "fingerprint": default_config["fingerprint"], } -select_ai.create_credential(credential=credential, replace=True) +select_ai.create_credential( + credential=credential, + replace=True, + public_synonym=True, +) print("Created credential: ", credential["credential_name"]) diff --git a/samples/delete_ai_credential.py b/samples/delete_ai_credential.py index 1d54cbb..75e4bb8 100644 --- a/samples/delete_ai_credential.py +++ b/samples/delete_ai_credential.py @@ -20,6 +20,8 @@ select_ai.connect(user=user, password=password, dsn=dsn) select_ai.delete_credential( - credential_name="my_oci_ai_profile_key", force=True + credential_name="my_oci_ai_profile_key", + force=True, + public_synonym=True, ) print("Deleted credential: my_oci_ai_profile_key") diff --git a/samples/profile_enable_disable.py b/samples/profile_enable_disable.py new file mode 100644 index 0000000..de40b00 --- /dev/null +++ b/samples/profile_enable_disable.py @@ -0,0 +1,31 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------- +# profile_enable_disable.py +# +# Disable and re-enable an existing Select AI profile. +# ----------------------------------------------------------------------------- + +import os + +import select_ai + +user = os.getenv("SELECT_AI_USER") +password = os.getenv("SELECT_AI_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +profile_name = os.getenv("SELECT_AI_PROFILE_NAME", "oci_ai_profile") + +select_ai.connect(user=user, password=password, dsn=dsn) +profile = select_ai.Profile(profile_name=profile_name) + +profile.disable() +try: + print("Disabled profile:", profile.profile_name) +finally: + profile.enable() + print("Enabled profile:", profile.profile_name) diff --git a/samples/profile_request_attributes.py b/samples/profile_request_attributes.py new file mode 100644 index 0000000..95b04a1 --- /dev/null +++ b/samples/profile_request_attributes.py @@ -0,0 +1,39 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------- +# profile_request_attributes.py +# +# Override profile attributes for one request without changing the saved +# profile. +# ----------------------------------------------------------------------------- + +import os + +import select_ai + +user = os.getenv("SELECT_AI_USER") +password = os.getenv("SELECT_AI_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +profile_name = os.getenv("SELECT_AI_PROFILE_NAME", "oci_ai_profile") + +select_ai.connect(user=user, password=password, dsn=dsn) +profile = select_ai.Profile(profile_name=profile_name) + +instruction = "Include the exact marker PYSAI_REQUEST_ATTRIBUTES_SAMPLE." +request_attributes = { + "additional_instructions": instruction, + "seed": 42, + "source_language": "en", + "target_language": "de", +} + +prompt = profile.show_prompt( + prompt="Tell me about Oracle Cloud Infrastructure.", + attributes=request_attributes, +) +print("Request attributes applied:", instruction in prompt) diff --git a/samples/profile_translate.py b/samples/profile_translate.py index 8c86ef2..b66a776 100644 --- a/samples/profile_translate.py +++ b/samples/profile_translate.py @@ -6,9 +6,9 @@ # ----------------------------------------------------------------------------- # ----------------------------------------------------------------------------- -# profile_summarize_uri.py +# profile_translate.py # -# Summarize content at a certain URI +# Translate text while allowing the provider to detect its source language. # ----------------------------------------------------------------------------- import os @@ -18,10 +18,9 @@ user = os.getenv("SELECT_AI_USER") password = os.getenv("SELECT_AI_PASSWORD") dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +profile_name = os.getenv("SELECT_AI_PROFILE_NAME", "oci_ai_profile") select_ai.connect(user=user, password=password, dsn=dsn) -profile = select_ai.Profile(profile_name="oci_ai_profile") -response = profile.translate( - text="Thank you", source_language="en", target_language="de" -) +profile = select_ai.Profile(profile_name=profile_name) +response = profile.translate(text="Thank you", target_language="de") print(response) diff --git a/samples/sharing.py b/samples/sharing.py new file mode 100644 index 0000000..db32a5b --- /dev/null +++ b/samples/sharing.py @@ -0,0 +1,81 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------- +# sharing.py +# +# Inspect owner-qualified objects and grant/revoke access to shared objects. +# The objects must already exist and the script must run as their owner. +# ----------------------------------------------------------------------------- + +import os + +import select_ai +from select_ai.agent import Team + +user = os.getenv("SELECT_AI_USER") +password = os.getenv("SELECT_AI_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +owner = os.getenv("SELECT_AI_OWNER") or None +grantee = os.getenv("SELECT_AI_SHARE_GRANTEE") +profile_name = os.getenv("SELECT_AI_PROFILE_NAME", "oci_ai_profile") +vector_index_name = os.getenv( + "SELECT_AI_VECTOR_INDEX_NAME", "test_vector_index" +) +team_name = os.getenv("SELECT_AI_TEAM_NAME", "MOVIE_AGENT_TEAM") +credential_name = os.getenv( + "SELECT_AI_CREDENTIAL_NAME", "my_oci_ai_profile_key" +) + +if not grantee: + raise RuntimeError( + "Set SELECT_AI_SHARE_GRANTEE to a database user or role" + ) + +select_ai.connect(user=user, password=password, dsn=dsn) + +profile = select_ai.Profile.fetch(profile_name, owner=owner) +profiles = list( + select_ai.Profile.list( + f"^{profile.profile_name}$", + owner=profile.owner, + ) +) +print("Profile:", profile.qualified_name) +print("Profiles:", [item.qualified_name for item in profiles]) +profile.grant_access(grantee) +print("Granted profile access to:", grantee) +profile.revoke_access(grantee) +print("Revoked profile access from:", grantee) + +vector_index = select_ai.VectorIndex.fetch( + vector_index_name, + owner=owner, +) +indexes = list( + select_ai.VectorIndex.list( + f"^{vector_index.index_name}$", + owner=vector_index.owner, + ) +) +print("Vector index:", vector_index.qualified_name) +print("Vector indexes:", [item.qualified_name for item in indexes]) +vector_index.grant_access(grantee) +print("Granted vector index access to:", grantee) +vector_index.revoke_access(grantee) +print("Revoked vector index access from:", grantee) + +team = Team.fetch(team_name) +team.grant_access(grantee) +print("Granted team access to:", grantee) +team.revoke_access(grantee) +print("Revoked team access from:", grantee) + +select_ai.grant_credential_access(credential_name, grantee) +print("Granted credential access to:", grantee) +select_ai.revoke_credential_access(credential_name, grantee) +print("Revoked credential access from:", grantee) From 7869fea4e67674d69bfa7e0786e24f83de0da371 Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Mon, 14 Sep 2026 23:09:51 -0700 Subject: [PATCH 25/31] Use fully qualified db object names --- src/select_ai/agent/core.py | 28 +++++++------- src/select_ai/agent/definition.py | 4 +- src/select_ai/agent/history.py | 21 ++++++----- src/select_ai/agent/sql.py | 30 +++++++-------- src/select_ai/agent/task.py | 28 +++++++------- src/select_ai/agent/team.py | 60 ++++++++++++++--------------- src/select_ai/agent/tool.py | 36 +++++++++--------- src/select_ai/async_profile.py | 38 ++++++++++--------- src/select_ai/conversation.py | 24 ++++++------ src/select_ai/credential.py | 63 +++++++++++++++++-------------- src/select_ai/privilege.py | 28 +++++++++----- src/select_ai/profile.py | 38 +++++++++++-------- src/select_ai/sql.py | 48 +++++++++++------------ src/select_ai/vector_index.py | 36 +++++++++--------- 14 files changed, 255 insertions(+), 227 deletions(-) diff --git a/src/select_ai/agent/core.py b/src/select_ai/agent/core.py index a4c8ce3..1fe0fef 100644 --- a/src/select_ai/agent/core.py +++ b/src/select_ai/agent/core.py @@ -146,7 +146,7 @@ def create( with cursor() as cr: try: cr.callproc( - "DBMS_CLOUD_AI_AGENT.CREATE_AGENT", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.CREATE_AGENT", keyword_parameters=parameters, ) except oracledb.Error as err: @@ -154,7 +154,7 @@ def create( if err_obj.code in (20050, 20052) and replace: self.delete(force=True) cr.callproc( - "DBMS_CLOUD_AI_AGENT.CREATE_AGENT", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.CREATE_AGENT", keyword_parameters=parameters, ) else: @@ -169,7 +169,7 @@ def delete(self, force: Optional[bool] = False): """ with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI_AGENT.DROP_AGENT", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.DROP_AGENT", keyword_parameters={ "agent_name": self.agent_name, "force": force, @@ -194,7 +194,7 @@ def disable(self): """ with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI_AGENT.DISABLE_AGENT", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.DISABLE_AGENT", keyword_parameters={ "agent_name": self.agent_name, }, @@ -206,7 +206,7 @@ def enable(self): """ with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI_AGENT.ENABLE_AGENT", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.ENABLE_AGENT", keyword_parameters={ "agent_name": self.agent_name, }, @@ -273,7 +273,7 @@ def set_attributes(self, attributes: AgentAttributes) -> None: } with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTES", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTES", keyword_parameters=parameters, ) self.attributes = self._get_attributes(agent_name=self.agent_name) @@ -290,7 +290,7 @@ def set_attribute(self, attribute_name: str, attribute_value: Any) -> None: } with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTE", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTE", keyword_parameters=parameters, ) self.attributes = self._get_attributes(agent_name=self.agent_name) @@ -369,7 +369,7 @@ async def create( async with async_cursor() as cr: try: await cr.callproc( - "DBMS_CLOUD_AI_AGENT.CREATE_AGENT", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.CREATE_AGENT", keyword_parameters=parameters, ) except oracledb.Error as err: @@ -377,7 +377,7 @@ async def create( if err_obj.code in (20050, 20052) and replace: await self.delete(force=True) await cr.callproc( - "DBMS_CLOUD_AI_AGENT.CREATE_AGENT", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.CREATE_AGENT", keyword_parameters=parameters, ) else: @@ -392,7 +392,7 @@ async def delete(self, force: Optional[bool] = False): """ async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI_AGENT.DROP_AGENT", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.DROP_AGENT", keyword_parameters={ "agent_name": self.agent_name, "force": force, @@ -419,7 +419,7 @@ async def disable(self): """ async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI_AGENT.DISABLE_AGENT", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.DISABLE_AGENT", keyword_parameters={ "agent_name": self.agent_name, }, @@ -431,7 +431,7 @@ async def enable(self): """ async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI_AGENT.ENABLE_AGENT", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.ENABLE_AGENT", keyword_parameters={ "agent_name": self.agent_name, }, @@ -500,7 +500,7 @@ async def set_attributes(self, attributes: AgentAttributes) -> None: } async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTES", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTES", keyword_parameters=parameters, ) self.attributes = await self._get_attributes( @@ -521,7 +521,7 @@ async def set_attribute( } async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTE", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTE", keyword_parameters=parameters, ) self.attributes = await self._get_attributes( diff --git a/src/select_ai/agent/definition.py b/src/select_ai/agent/definition.py index a8fb59a..2d69858 100644 --- a/src/select_ai/agent/definition.py +++ b/src/select_ai/agent/definition.py @@ -23,7 +23,7 @@ def get_definition(object_type: str, object_name: str) -> Optional[str]: """ with cursor() as cr: data = cr.callfunc( - "DBMS_CLOUD_AI_AGENT.GET_DEFINITION", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.GET_DEFINITION", oracledb.DB_TYPE_CLOB, keyword_parameters={ "object_type": object_type, @@ -42,7 +42,7 @@ async def async_get_definition( """ async with async_cursor() as cr: data = await cr.callfunc( - "DBMS_CLOUD_AI_AGENT.GET_DEFINITION", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.GET_DEFINITION", oracledb.DB_TYPE_CLOB, keyword_parameters={ "object_type": object_type, diff --git a/src/select_ai/agent/history.py b/src/select_ai/agent/history.py index 2cda87b..f7222a5 100644 --- a/src/select_ai/agent/history.py +++ b/src/select_ai/agent/history.py @@ -33,7 +33,7 @@ @dataclass class TeamHistoryEvent(SelectAIDataClass): - """One row from ``USER_AI_AGENT_TEAM_HISTORY``.""" + """One row from ``C##CLOUD$SERVICE.USER_AI_AGENT_TEAM_HISTORY``.""" team_exec_id: str team_name: str @@ -46,7 +46,7 @@ class TeamHistoryEvent(SelectAIDataClass): @dataclass class TaskHistoryEvent(SelectAIDataClass): - """One row from ``USER_AI_AGENT_TASK_HISTORY``.""" + """One row from ``C##CLOUD$SERVICE.USER_AI_AGENT_TASK_HISTORY``.""" team_exec_id: str team_name: str @@ -63,7 +63,7 @@ class TaskHistoryEvent(SelectAIDataClass): @dataclass class ToolHistoryEvent(SelectAIDataClass): - """One row from ``USER_AI_AGENT_TOOL_HISTORY``.""" + """One row from ``C##CLOUD$SERVICE.USER_AI_AGENT_TOOL_HISTORY``.""" invocation_id: int team_exec_id: str @@ -151,7 +151,7 @@ async def _async_events( class TeamHistory: - """Read runs from ``USER_AI_AGENT_TEAM_HISTORY``.""" + """Read runs from ``C##CLOUD$SERVICE.USER_AI_AGENT_TEAM_HISTORY``.""" @classmethod def list( @@ -170,7 +170,7 @@ def list( class TaskHistory: - """Read task runs from ``USER_AI_AGENT_TASK_HISTORY``.""" + """Read task runs from ``C##CLOUD$SERVICE.USER_AI_AGENT_TASK_HISTORY``.""" @classmethod def list( @@ -196,7 +196,7 @@ def list( class ToolHistory: - """Read tool calls from ``USER_AI_AGENT_TOOL_HISTORY``.""" + """Read tool calls from ``C##CLOUD$SERVICE.USER_AI_AGENT_TOOL_HISTORY``.""" @classmethod def list( @@ -222,7 +222,8 @@ def list( class AsyncTeamHistory: - """Asynchronously read runs from ``USER_AI_AGENT_TEAM_HISTORY``.""" + """Asynchronously read runs from + ``C##CLOUD$SERVICE.USER_AI_AGENT_TEAM_HISTORY``.""" @classmethod async def list( @@ -242,7 +243,8 @@ async def list( class AsyncTaskHistory: - """Asynchronously read task runs from ``USER_AI_AGENT_TASK_HISTORY``.""" + """Asynchronously read task runs from + ``C##CLOUD$SERVICE.USER_AI_AGENT_TASK_HISTORY``.""" @classmethod async def list( @@ -269,7 +271,8 @@ async def list( class AsyncToolHistory: - """Asynchronously read tool calls from ``USER_AI_AGENT_TOOL_HISTORY``.""" + """Asynchronously read tool calls from + ``C##CLOUD$SERVICE.USER_AI_AGENT_TOOL_HISTORY``.""" @classmethod async def list( diff --git a/src/select_ai/agent/sql.py b/src/select_ai/agent/sql.py index 126f7c4..7a85707 100644 --- a/src/select_ai/agent/sql.py +++ b/src/select_ai/agent/sql.py @@ -8,76 +8,76 @@ GET_USER_AI_AGENT = """ SELECT a.agent_name, a.description -from USER_AI_AGENTS a +from C##CLOUD$SERVICE.USER_AI_AGENTS a where a.agent_name = :agent_name """ GET_USER_AI_AGENT_ATTRIBUTES = """ SELECT attribute_name, attribute_value -FROM USER_AI_AGENT_ATTRIBUTES +FROM C##CLOUD$SERVICE.USER_AI_AGENT_ATTRIBUTES WHERE agent_name = :agent_name """ LIST_USER_AI_AGENTS = """ SELECT a.agent_name, a.description -from USER_AI_AGENTS a +from C##CLOUD$SERVICE.USER_AI_AGENTS a where REGEXP_LIKE(a.agent_name, :agent_name_pattern, 'i') """ GET_USER_AI_AGENT_TASK = """ SELECT t.task_name, t.description -FROM USER_AI_AGENT_TASKS t +FROM C##CLOUD$SERVICE.USER_AI_AGENT_TASKS t WHERE t.task_name= :task_name """ GET_USER_AI_AGENT_TASK_ATTRIBUTES = """ SELECT attribute_name, attribute_value -FROM USER_AI_AGENT_TASK_ATTRIBUTES +FROM C##CLOUD$SERVICE.USER_AI_AGENT_TASK_ATTRIBUTES WHERE task_name= :task_name """ LIST_USER_AI_AGENT_TASKS = """ SELECT t.task_name, t.description -FROM USER_AI_AGENT_TASKS t +FROM C##CLOUD$SERVICE.USER_AI_AGENT_TASKS t WHERE REGEXP_LIKE(t.task_name, :task_name_pattern, 'i') """ GET_USER_AI_AGENT_TOOL = """ SELECT t.tool_name, t.description -FROM USER_AI_AGENT_TOOLS t +FROM C##CLOUD$SERVICE.USER_AI_AGENT_TOOLS t WHERE t.tool_name = :tool_name """ GET_USER_AI_AGENT_TOOL_ATTRIBUTES = """ SELECT attribute_name, attribute_value -FROM USER_AI_AGENT_TOOL_ATTRIBUTES +FROM C##CLOUD$SERVICE.USER_AI_AGENT_TOOL_ATTRIBUTES WHERE tool_name = :tool_name """ LIST_USER_AI_AGENT_TOOLS = """ SELECT t.tool_name, t.description -FROM USER_AI_AGENT_TOOLS t +FROM C##CLOUD$SERVICE.USER_AI_AGENT_TOOLS t WHERE REGEXP_LIKE(t.tool_name, :tool_name_pattern, 'i') """ GET_USER_AI_AGENT_TEAM = """ SELECT t.agent_team_name as team_name, t.description -FROM USER_AI_AGENT_TEAMS t +FROM C##CLOUD$SERVICE.USER_AI_AGENT_TEAMS t WHERE t.agent_team_name = :team_name """ GET_USER_AI_AGENT_TEAM_ATTRIBUTES = """ SELECT attribute_name, attribute_value -FROM USER_AI_AGENT_TEAM_ATTRIBUTES +FROM C##CLOUD$SERVICE.USER_AI_AGENT_TEAM_ATTRIBUTES WHERE agent_team_name = :team_name """ LIST_USER_AI_AGENT_TEAMS = """ SELECT t.AGENT_TEAM_NAME as team_name, description -FROM USER_AI_AGENT_TEAMS t +FROM C##CLOUD$SERVICE.USER_AI_AGENT_TEAMS t WHERE REGEXP_LIKE(t.AGENT_TEAM_NAME, :team_name_pattern, 'i') """ @@ -85,7 +85,7 @@ LIST_USER_AI_AGENT_TEAM_HISTORY = """ SELECT team_exec_id, team_name, state, start_date, end_date, conversation_id, params -FROM user_ai_agent_team_history +FROM C##CLOUD$SERVICE.USER_AI_AGENT_TEAM_HISTORY WHERE (:team_name IS NULL OR team_name = :team_name) AND (:team_exec_id IS NULL OR team_exec_id = :team_exec_id) ORDER BY start_date DESC NULLS LAST @@ -95,7 +95,7 @@ LIST_USER_AI_AGENT_TASK_HISTORY = """ SELECT team_exec_id, team_name, task_order, agent_name, task_name, conversation_params, input, result, state, start_date, end_date -FROM user_ai_agent_task_history +FROM C##CLOUD$SERVICE.USER_AI_AGENT_TASK_HISTORY WHERE (:team_name IS NULL OR team_name = :team_name) AND (:task_name IS NULL OR task_name = :task_name) AND (:agent_name IS NULL OR agent_name = :agent_name) @@ -107,7 +107,7 @@ LIST_USER_AI_AGENT_TOOL_HISTORY = """ SELECT invocation_id, team_exec_id, task_order, tool_name, agent_name, task_name, start_date, end_date, input, output, tool_output -FROM user_ai_agent_tool_history +FROM C##CLOUD$SERVICE.USER_AI_AGENT_TOOL_HISTORY WHERE (:tool_name IS NULL OR tool_name = :tool_name) AND (:task_name IS NULL OR task_name = :task_name) AND (:agent_name IS NULL OR agent_name = :agent_name) diff --git a/src/select_ai/agent/task.py b/src/select_ai/agent/task.py index 598c337..79b616c 100644 --- a/src/select_ai/agent/task.py +++ b/src/select_ai/agent/task.py @@ -156,7 +156,7 @@ def create( with cursor() as cr: try: cr.callproc( - "DBMS_CLOUD_AI_AGENT.CREATE_TASK", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.CREATE_TASK", keyword_parameters=parameters, ) except oracledb.Error as err: @@ -164,7 +164,7 @@ def create( if err_obj.code in (20051, 20052) and replace: self.delete(force=True) cr.callproc( - "DBMS_CLOUD_AI_AGENT.CREATE_TASK", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.CREATE_TASK", keyword_parameters=parameters, ) else: @@ -178,7 +178,7 @@ def delete(self, force: bool = False): """ with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI_AGENT.DROP_TASK", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.DROP_TASK", keyword_parameters={ "task_name": self.task_name, "force": force, @@ -202,7 +202,7 @@ def disable(self): """ with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI_AGENT.DISABLE_TASK", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.DISABLE_TASK", keyword_parameters={ "task_name": self.task_name, }, @@ -214,7 +214,7 @@ def enable(self): """ with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI_AGENT.ENABLE_TASK", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.ENABLE_TASK", keyword_parameters={ "task_name": self.task_name, }, @@ -289,7 +289,7 @@ def set_attributes(self, attributes: TaskAttributes): } with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTES", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTES", keyword_parameters=parameters, ) @@ -309,7 +309,7 @@ def set_attribute(self, attribute_name: str, attribute_value: Any): } with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTE", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTE", keyword_parameters=parameters, ) @@ -391,7 +391,7 @@ async def create( async with async_cursor() as cr: try: await cr.callproc( - "DBMS_CLOUD_AI_AGENT.CREATE_TASK", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.CREATE_TASK", keyword_parameters=parameters, ) except oracledb.Error as err: @@ -399,7 +399,7 @@ async def create( if err_obj.code in (20051, 20052) and replace: await self.delete(force=True) await cr.callproc( - "DBMS_CLOUD_AI_AGENT.CREATE_TASK", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.CREATE_TASK", keyword_parameters=parameters, ) else: @@ -413,7 +413,7 @@ async def delete(self, force: bool = False): """ async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI_AGENT.DROP_TASK", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.DROP_TASK", keyword_parameters={ "task_name": self.task_name, "force": force, @@ -437,7 +437,7 @@ async def disable(self): """ async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI_AGENT.DISABLE_TASK", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.DISABLE_TASK", keyword_parameters={ "task_name": self.task_name, }, @@ -449,7 +449,7 @@ async def enable(self): """ async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI_AGENT.ENABLE_TASK", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.ENABLE_TASK", keyword_parameters={ "task_name": self.task_name, }, @@ -527,7 +527,7 @@ async def set_attributes(self, attributes: TaskAttributes): } async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTES", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTES", keyword_parameters=parameters, ) @@ -547,6 +547,6 @@ async def set_attribute(self, attribute_name: str, attribute_value: Any): } async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTE", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTE", keyword_parameters=parameters, ) diff --git a/src/select_ai/agent/team.py b/src/select_ai/agent/team.py index e767ba6..7a698e6 100644 --- a/src/select_ai/agent/team.py +++ b/src/select_ai/agent/team.py @@ -180,7 +180,7 @@ def create( with cursor() as cr: try: cr.callproc( - "DBMS_CLOUD_AI_AGENT.CREATE_TEAM", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.CREATE_TEAM", keyword_parameters=parameters, ) except oracledb.Error as err: @@ -188,7 +188,7 @@ def create( if err_obj.code in (20053, 20052) and replace: self.delete(force=True) cr.callproc( - "DBMS_CLOUD_AI_AGENT.CREATE_TEAM", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.CREATE_TEAM", keyword_parameters=parameters, ) else: @@ -202,7 +202,7 @@ def delete(self, force: Optional[bool] = False): """ with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI_AGENT.DROP_TEAM", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.DROP_TEAM", keyword_parameters={ "team_name": self.team_name, "force": force, @@ -226,7 +226,7 @@ def disable(self): """ with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI_AGENT.DISABLE_TEAM", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.DISABLE_TEAM", keyword_parameters={ "team_name": self.team_name, }, @@ -238,7 +238,7 @@ def enable(self): """ with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI_AGENT.ENABLE_TEAM", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.ENABLE_TEAM", keyword_parameters={ "team_name": self.team_name, }, @@ -249,7 +249,7 @@ def grant_access(self, user_or_role_name: str) -> None: user_or_role_name = validate_user_or_role_name(user_or_role_name) with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI_AGENT.GRANT_TEAM_ACCESS", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.GRANT_TEAM_ACCESS", keyword_parameters={ "team_name": self.team_name, "user_or_role_name": user_or_role_name, @@ -261,7 +261,7 @@ def revoke_access(self, user_or_role_name: str) -> None: user_or_role_name = validate_user_or_role_name(user_or_role_name) with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI_AGENT.REVOKE_TEAM_ACCESS", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.REVOKE_TEAM_ACCESS", keyword_parameters={ "team_name": self.team_name, "user_or_role_name": user_or_role_name, @@ -355,7 +355,7 @@ def run(self, prompt: str = None, params: Mapping = None): with cursor() as cr: data = cr.callfunc( - "DBMS_CLOUD_AI_AGENT.RUN_TEAM", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.RUN_TEAM", oracledb.DB_TYPE_CLOB, keyword_parameters=parameters, ) @@ -369,7 +369,7 @@ def describe_team(self) -> Optional[str]: """Return this team's JSON metadata and aggregated skills.""" with cursor() as cr: data = cr.callfunc( - "DBMS_CLOUD_AI_AGENT.DESCRIBE_TEAM", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.DESCRIBE_TEAM", oracledb.DB_TYPE_CLOB, keyword_parameters={"team_name": self.team_name}, ) @@ -379,7 +379,7 @@ def list_tools(self) -> Optional[str]: """Return JSON metadata for the tools available to this team.""" with cursor() as cr: data = cr.callfunc( - "DBMS_CLOUD_AI_AGENT.LIST_TOOLS", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.LIST_TOOLS", oracledb.DB_TYPE_CLOB, keyword_parameters={"team_name": self.team_name}, ) @@ -435,13 +435,13 @@ def export_team( ) parameters["location"] = location cr.callproc( - "DBMS_CLOUD_AI_AGENT.EXPORT_TEAM", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.EXPORT_TEAM", keyword_parameters=parameters, ) return None data = cr.callfunc( - "DBMS_CLOUD_AI_AGENT.EXPORT_TEAM", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.EXPORT_TEAM", oracledb.DB_TYPE_CLOB, keyword_parameters=parameters, ) @@ -516,7 +516,7 @@ def import_team( with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI_AGENT.IMPORT_TEAM", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.IMPORT_TEAM", keyword_parameters=parameters, ) @@ -567,7 +567,7 @@ def set_attributes(self, attributes: TeamAttributes) -> None: } with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTES", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTES", keyword_parameters=parameters, ) @@ -584,7 +584,7 @@ def set_attribute(self, attribute_name: str, attribute_value: Any) -> None: } with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTE", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTE", keyword_parameters=parameters, ) @@ -665,7 +665,7 @@ async def create( async with async_cursor() as cr: try: await cr.callproc( - "DBMS_CLOUD_AI_AGENT.CREATE_TEAM", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.CREATE_TEAM", keyword_parameters=parameters, ) except oracledb.Error as err: @@ -673,7 +673,7 @@ async def create( if err_obj.code in (20053, 20052) and replace: await self.delete(force=True) await cr.callproc( - "DBMS_CLOUD_AI_AGENT.CREATE_TEAM", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.CREATE_TEAM", keyword_parameters=parameters, ) else: @@ -687,7 +687,7 @@ async def delete(self, force: Optional[bool] = False): """ async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI_AGENT.DROP_TEAM", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.DROP_TEAM", keyword_parameters={ "team_name": self.team_name, "force": force, @@ -711,7 +711,7 @@ async def disable(self): """ async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI_AGENT.DISABLE_TEAM", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.DISABLE_TEAM", keyword_parameters={ "team_name": self.team_name, }, @@ -723,7 +723,7 @@ async def enable(self): """ async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI_AGENT.ENABLE_TEAM", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.ENABLE_TEAM", keyword_parameters={ "team_name": self.team_name, }, @@ -734,7 +734,7 @@ async def grant_access(self, user_or_role_name: str) -> None: user_or_role_name = validate_user_or_role_name(user_or_role_name) async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI_AGENT.GRANT_TEAM_ACCESS", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.GRANT_TEAM_ACCESS", keyword_parameters={ "team_name": self.team_name, "user_or_role_name": user_or_role_name, @@ -746,7 +746,7 @@ async def revoke_access(self, user_or_role_name: str) -> None: user_or_role_name = validate_user_or_role_name(user_or_role_name) async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI_AGENT.REVOKE_TEAM_ACCESS", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.REVOKE_TEAM_ACCESS", keyword_parameters={ "team_name": self.team_name, "user_or_role_name": user_or_role_name, @@ -843,7 +843,7 @@ async def run(self, prompt: str = None, params: Mapping = None): async with async_cursor() as cr: data = await cr.callfunc( - "DBMS_CLOUD_AI_AGENT.RUN_TEAM", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.RUN_TEAM", oracledb.DB_TYPE_CLOB, keyword_parameters=parameters, ) @@ -857,7 +857,7 @@ async def describe_team(self) -> Optional[str]: """Asynchronously return this team's JSON metadata and skills.""" async with async_cursor() as cr: data = await cr.callfunc( - "DBMS_CLOUD_AI_AGENT.DESCRIBE_TEAM", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.DESCRIBE_TEAM", oracledb.DB_TYPE_CLOB, keyword_parameters={"team_name": self.team_name}, ) @@ -867,7 +867,7 @@ async def list_tools(self) -> Optional[str]: """Asynchronously return JSON metadata for this team's tools.""" async with async_cursor() as cr: data = await cr.callfunc( - "DBMS_CLOUD_AI_AGENT.LIST_TOOLS", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.LIST_TOOLS", oracledb.DB_TYPE_CLOB, keyword_parameters={"team_name": self.team_name}, ) @@ -923,13 +923,13 @@ async def export_team( ) parameters["location"] = location await cr.callproc( - "DBMS_CLOUD_AI_AGENT.EXPORT_TEAM", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.EXPORT_TEAM", keyword_parameters=parameters, ) return None data = await cr.callfunc( - "DBMS_CLOUD_AI_AGENT.EXPORT_TEAM", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.EXPORT_TEAM", oracledb.DB_TYPE_CLOB, keyword_parameters=parameters, ) @@ -1004,7 +1004,7 @@ async def import_team( async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI_AGENT.IMPORT_TEAM", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.IMPORT_TEAM", keyword_parameters=parameters, ) @@ -1055,7 +1055,7 @@ async def set_attributes(self, attributes: TeamAttributes) -> None: } async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTES", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTES", keyword_parameters=parameters, ) @@ -1074,6 +1074,6 @@ async def set_attribute( } async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTE", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTE", keyword_parameters=parameters, ) diff --git a/src/select_ai/agent/tool.py b/src/select_ai/agent/tool.py index a9d58fd..37f9f8e 100644 --- a/src/select_ai/agent/tool.py +++ b/src/select_ai/agent/tool.py @@ -329,7 +329,7 @@ def create( with cursor() as cr: try: cr.callproc( - "DBMS_CLOUD_AI_AGENT.CREATE_TOOL", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.CREATE_TOOL", keyword_parameters=parameters, ) except oracledb.Error as err: @@ -337,7 +337,7 @@ def create( if err_obj.code in (20050, 20052) and replace: self.delete(force=True) cr.callproc( - "DBMS_CLOUD_AI_AGENT.CREATE_TOOL", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.CREATE_TOOL", keyword_parameters=parameters, ) else: @@ -610,7 +610,7 @@ def delete(self, force: bool = False): """ with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI_AGENT.DROP_TOOL", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.DROP_TOOL", keyword_parameters={ "tool_name": self.tool_name, "force": force, @@ -634,7 +634,7 @@ def disable(self): """ with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI_AGENT.DISABLE_TOOL", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.DISABLE_TOOL", keyword_parameters={ "tool_name": self.tool_name, }, @@ -646,7 +646,7 @@ def enable(self): """ with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI_AGENT.ENABLE_TOOL", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.ENABLE_TOOL", keyword_parameters={ "tool_name": self.tool_name, }, @@ -661,7 +661,7 @@ def run_tool(self, input: str) -> Optional[str]: """ with cursor() as cr: data = cr.callfunc( - "DBMS_CLOUD_AI_AGENT.RUN_TOOL", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.RUN_TOOL", oracledb.DB_TYPE_CLOB, keyword_parameters={ "tool_name": self.tool_name, @@ -674,7 +674,7 @@ def describe_tool(self) -> Optional[str]: """Return this tool's JSON metadata and function arguments.""" with cursor() as cr: data = cr.callfunc( - "DBMS_CLOUD_AI_AGENT.DESCRIBE_TOOL", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.DESCRIBE_TOOL", oracledb.DB_TYPE_CLOB, keyword_parameters={"tool_name": self.tool_name}, ) @@ -733,7 +733,7 @@ def set_attributes(self, attributes: ToolAttributes) -> None: } with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTES", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTES", keyword_parameters=parameters, ) @@ -750,7 +750,7 @@ def set_attribute(self, attribute_name: str, attribute_value: Any) -> None: } with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTE", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTE", keyword_parameters=parameters, ) @@ -824,7 +824,7 @@ async def create( async with async_cursor() as cr: try: await cr.callproc( - "DBMS_CLOUD_AI_AGENT.CREATE_TOOL", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.CREATE_TOOL", keyword_parameters=parameters, ) except oracledb.Error as err: @@ -832,7 +832,7 @@ async def create( if err_obj.code in (20050, 20052) and replace: await self.delete(force=True) await cr.callproc( - "DBMS_CLOUD_AI_AGENT.CREATE_TOOL", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.CREATE_TOOL", keyword_parameters=parameters, ) else: @@ -1105,7 +1105,7 @@ async def delete(self, force: bool = False): """ async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI_AGENT.DROP_TOOL", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.DROP_TOOL", keyword_parameters={ "tool_name": self.tool_name, "force": force, @@ -1129,7 +1129,7 @@ async def disable(self): """ async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI_AGENT.DISABLE_TOOL", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.DISABLE_TOOL", keyword_parameters={ "tool_name": self.tool_name, }, @@ -1141,7 +1141,7 @@ async def enable(self): """ async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI_AGENT.ENABLE_TOOL", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.ENABLE_TOOL", keyword_parameters={ "tool_name": self.tool_name, }, @@ -1151,7 +1151,7 @@ async def run_tool(self, input: str) -> Optional[str]: """Asynchronously run this tool directly and return its result.""" async with async_cursor() as cr: data = await cr.callfunc( - "DBMS_CLOUD_AI_AGENT.RUN_TOOL", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.RUN_TOOL", oracledb.DB_TYPE_CLOB, keyword_parameters={ "tool_name": self.tool_name, @@ -1164,7 +1164,7 @@ async def describe_tool(self) -> Optional[str]: """Asynchronously return this tool's JSON metadata.""" async with async_cursor() as cr: data = await cr.callfunc( - "DBMS_CLOUD_AI_AGENT.DESCRIBE_TOOL", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.DESCRIBE_TOOL", oracledb.DB_TYPE_CLOB, keyword_parameters={"tool_name": self.tool_name}, ) @@ -1232,7 +1232,7 @@ async def set_attributes(self, attributes: ToolAttributes) -> None: } async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTES", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTES", keyword_parameters=parameters, ) @@ -1251,6 +1251,6 @@ async def set_attribute( } async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTE", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT.SET_ATTRIBUTE", keyword_parameters=parameters, ) diff --git a/src/select_ai/async_profile.py b/src/select_ai/async_profile.py index c9ccd33..bb82e18 100644 --- a/src/select_ai/async_profile.py +++ b/src/select_ai/async_profile.py @@ -109,7 +109,8 @@ async def _init_profile(self): async def _get_profile_description( profile_name: str, owner: Optional[str] = None ) -> Tuple[Union[str, None], str]: - """Get a profile description and owner from ALL_CLOUD_AI_PROFILES. + """Get a profile description and owner from + C##CLOUD$SERVICE.ALL_CLOUD_AI_PROFILES. :param str profile_name: Name of profile :param str owner: Owner of a shared profile. Defaults to current schema. @@ -187,7 +188,8 @@ async def _set_attribute( } async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI.SET_ATTRIBUTE", keyword_parameters=parameters + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.SET_ATTRIBUTE", + keyword_parameters=parameters, ) async def set_attribute( @@ -230,7 +232,8 @@ async def set_attributes(self, attributes: ProfileAttributes): } async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI.SET_ATTRIBUTES", keyword_parameters=parameters + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.SET_ATTRIBUTES", + keyword_parameters=parameters, ) self.attributes = await self.get_attributes() @@ -252,7 +255,7 @@ async def create(self, replace: Optional[int] = False) -> None: async with async_cursor() as cr: try: await cr.callproc( - "DBMS_CLOUD_AI.CREATE_PROFILE", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.CREATE_PROFILE", keyword_parameters=parameters, ) except oracledb.DatabaseError as e: @@ -261,7 +264,7 @@ async def create(self, replace: Optional[int] = False) -> None: if error.code == 20046 and replace: await self.delete(force=True) await cr.callproc( - "DBMS_CLOUD_AI.CREATE_PROFILE", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.CREATE_PROFILE", keyword_parameters=parameters, ) else: @@ -274,7 +277,7 @@ async def _delete(profile_name: str, force: bool = False): """ async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI.DROP_PROFILE", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.DROP_PROFILE", keyword_parameters={ "profile_name": profile_name, "force": force, @@ -298,7 +301,7 @@ async def enable(self) -> None: """ async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI.ENABLE_PROFILE", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.ENABLE_PROFILE", keyword_parameters={"profile_name": self.profile_name}, ) @@ -310,7 +313,7 @@ async def disable(self) -> None: """ async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI.DISABLE_PROFILE", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.DISABLE_PROFILE", keyword_parameters={"profile_name": self.profile_name}, ) @@ -319,7 +322,7 @@ async def grant_access(self, user_or_role_name: str) -> None: user_or_role_name = validate_user_or_role_name(user_or_role_name) async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI.GRANT_PROFILE_ACCESS", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.GRANT_PROFILE_ACCESS", keyword_parameters={ "profile_name": self.profile_name, "user_or_role_name": user_or_role_name, @@ -331,7 +334,7 @@ async def revoke_access(self, user_or_role_name: str) -> None: user_or_role_name = validate_user_or_role_name(user_or_role_name) async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI.REVOKE_PROFILE_ACCESS", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.REVOKE_PROFILE_ACCESS", keyword_parameters={ "profile_name": self.profile_name, "user_or_role_name": user_or_role_name, @@ -390,7 +393,8 @@ async def _save_feedback( params["profile_name"] = self.profile_name async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI.FEEDBACK", keyword_parameters=params + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.FEEDBACK", + keyword_parameters=params, ) async def add_positive_feedback( @@ -508,7 +512,7 @@ async def _generate_with_cursor( ) data = await cr.callfunc( - "DBMS_CLOUD_AI.GENERATE", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.GENERATE", oracledb.DB_TYPE_CLOB, keyword_parameters=parameters, ) @@ -581,7 +585,7 @@ async def _generate_stream_with_cursor( prompt, action, params, attributes ) data = await cr.callfunc( - "DBMS_CLOUD_AI.GENERATE", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.GENERATE", oracledb.DB_TYPE_CLOB, keyword_parameters=parameters, ) @@ -849,7 +853,7 @@ async def summarize( parameters["profile_name"] = self.profile_name async with async_cursor() as cr: data = await cr.callfunc( - "DBMS_CLOUD_AI.SUMMARIZE", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.SUMMARIZE", oracledb.DB_TYPE_CLOB, keyword_parameters=parameters, ) @@ -879,7 +883,7 @@ async def generate_synthetic_data( keyword_parameters["profile_name"] = self.profile_name async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI.GENERATE_SYNTHETIC_DATA", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.GENERATE_SYNTHETIC_DATA", keyword_parameters=keyword_parameters, ) @@ -917,7 +921,7 @@ async def run_pipeline( if serialized_attributes is not None: parameters["attributes"] = serialized_attributes pipeline.add_callfunc( - "DBMS_CLOUD_AI.GENERATE", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.GENERATE", return_type=oracledb.DB_TYPE_CLOB, keyword_parameters=parameters, ) @@ -960,7 +964,7 @@ async def translate( } async with async_cursor() as cr: data = await cr.callfunc( - "DBMS_CLOUD_AI.TRANSLATE", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.TRANSLATE", oracledb.DB_TYPE_CLOB, keyword_parameters=parameters, ) diff --git a/src/select_ai/conversation.py b/src/select_ai/conversation.py index ae3cb62..59d1c2f 100644 --- a/src/select_ai/conversation.py +++ b/src/select_ai/conversation.py @@ -124,7 +124,7 @@ def create(self) -> str: """ with cursor() as cr: self.conversation_id = cr.callfunc( - "DBMS_CLOUD_AI.CREATE_CONVERSATION", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.CREATE_CONVERSATION", oracledb.DB_TYPE_VARCHAR, keyword_parameters={"attributes": self.attributes.json()}, ) @@ -134,7 +134,7 @@ def delete(self, force: bool = False): """Drops the conversation""" with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI.DROP_CONVERSATION", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.DROP_CONVERSATION", keyword_parameters={ "conversation_id": self.conversation_id, "force": force, @@ -151,7 +151,7 @@ def delete_prompt( """ with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI.DELETE_CONVERSATION_PROMPT", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.DELETE_CONVERSATION_PROMPT", keyword_parameters={ "conversation_prompt_id": conversation_prompt_id, "force": force, @@ -166,7 +166,7 @@ def add_tag(self, tag_key: str, tag_value: str) -> None: """ with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI.ADD_CONVERSATION_TAG", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.ADD_CONVERSATION_TAG", keyword_parameters={ "conversation_id": self.conversation_id, "tag_key": tag_key, @@ -182,7 +182,7 @@ def remove_tag(self, tag_key: str, force: bool = False) -> None: """ with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI.REMOVE_CONVERSATION_TAG", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.REMOVE_CONVERSATION_TAG", keyword_parameters={ "conversation_id": self.conversation_id, "tag_key": tag_key, @@ -233,7 +233,7 @@ def set_attributes(self, attributes: ConversationAttributes): """Updates the attributes of the conversation in the database""" with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI.UPDATE_CONVERSATION", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.UPDATE_CONVERSATION", keyword_parameters={ "conversation_id": self.conversation_id, "attributes": attributes.json(), @@ -314,7 +314,7 @@ async def create(self) -> str: """ async with async_cursor() as cr: self.conversation_id = await cr.callfunc( - "DBMS_CLOUD_AI.CREATE_CONVERSATION", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.CREATE_CONVERSATION", oracledb.DB_TYPE_VARCHAR, keyword_parameters={"attributes": self.attributes.json()}, ) @@ -324,7 +324,7 @@ async def delete(self, force: bool = False): """Delete the conversation""" async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI.DROP_CONVERSATION", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.DROP_CONVERSATION", keyword_parameters={ "conversation_id": self.conversation_id, "force": force, @@ -341,7 +341,7 @@ async def delete_prompt( """ async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI.DELETE_CONVERSATION_PROMPT", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.DELETE_CONVERSATION_PROMPT", keyword_parameters={ "conversation_prompt_id": conversation_prompt_id, "force": force, @@ -356,7 +356,7 @@ async def add_tag(self, tag_key: str, tag_value: str) -> None: """ async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI.ADD_CONVERSATION_TAG", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.ADD_CONVERSATION_TAG", keyword_parameters={ "conversation_id": self.conversation_id, "tag_key": tag_key, @@ -372,7 +372,7 @@ async def remove_tag(self, tag_key: str, force: bool = False) -> None: """ async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI.REMOVE_CONVERSATION_TAG", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.REMOVE_CONVERSATION_TAG", keyword_parameters={ "conversation_id": self.conversation_id, "tag_key": tag_key, @@ -418,7 +418,7 @@ async def set_attributes(self, attributes: ConversationAttributes): """Updates the attributes of the conversation""" with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI.UPDATE_CONVERSATION", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.UPDATE_CONVERSATION", keyword_parameters={ "conversation_id": self.conversation_id, "attributes": attributes.json(), diff --git a/src/select_ai/credential.py b/src/select_ai/credential.py index 5096f64..ae96be9 100644 --- a/src/select_ai/credential.py +++ b/src/select_ai/credential.py @@ -29,11 +29,11 @@ v_credential_name VARCHAR2(261); v_owner VARCHAR2(261); BEGIN - v_credential_name := DBMS_ASSERT.ENQUOTE_NAME( - DBMS_ASSERT.SIMPLE_SQL_NAME(UPPER(:credential_name)), + v_credential_name := SYS.DBMS_ASSERT.ENQUOTE_NAME( + SYS.DBMS_ASSERT.SIMPLE_SQL_NAME(UPPER(:credential_name)), FALSE ); - v_owner := DBMS_ASSERT.ENQUOTE_NAME( + v_owner := SYS.DBMS_ASSERT.ENQUOTE_NAME( SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA'), FALSE ); @@ -47,8 +47,8 @@ DECLARE v_credential_name VARCHAR2(261); BEGIN - v_credential_name := DBMS_ASSERT.ENQUOTE_NAME( - DBMS_ASSERT.SIMPLE_SQL_NAME(UPPER(:credential_name)), + v_credential_name := SYS.DBMS_ASSERT.ENQUOTE_NAME( + SYS.DBMS_ASSERT.SIMPLE_SQL_NAME(UPPER(:credential_name)), FALSE ); EXECUTE IMMEDIATE 'DROP PUBLIC SYNONYM ' || v_credential_name; @@ -61,16 +61,16 @@ v_owner VARCHAR2(261); v_user_or_role_name VARCHAR2(261); BEGIN - v_credential_name := DBMS_ASSERT.ENQUOTE_NAME( - DBMS_ASSERT.SIMPLE_SQL_NAME(UPPER(:credential_name)), + v_credential_name := SYS.DBMS_ASSERT.ENQUOTE_NAME( + SYS.DBMS_ASSERT.SIMPLE_SQL_NAME(UPPER(:credential_name)), FALSE ); - v_owner := DBMS_ASSERT.ENQUOTE_NAME( + v_owner := SYS.DBMS_ASSERT.ENQUOTE_NAME( SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA'), FALSE ); - v_user_or_role_name := DBMS_ASSERT.ENQUOTE_NAME( - DBMS_ASSERT.SIMPLE_SQL_NAME(UPPER(:user_or_role_name)), + v_user_or_role_name := SYS.DBMS_ASSERT.ENQUOTE_NAME( + SYS.DBMS_ASSERT.SIMPLE_SQL_NAME(UPPER(:user_or_role_name)), FALSE ); EXECUTE IMMEDIATE @@ -85,16 +85,16 @@ v_owner VARCHAR2(261); v_user_or_role_name VARCHAR2(261); BEGIN - v_credential_name := DBMS_ASSERT.ENQUOTE_NAME( - DBMS_ASSERT.SIMPLE_SQL_NAME(UPPER(:credential_name)), + v_credential_name := SYS.DBMS_ASSERT.ENQUOTE_NAME( + SYS.DBMS_ASSERT.SIMPLE_SQL_NAME(UPPER(:credential_name)), FALSE ); - v_owner := DBMS_ASSERT.ENQUOTE_NAME( + v_owner := SYS.DBMS_ASSERT.ENQUOTE_NAME( SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA'), FALSE ); - v_user_or_role_name := DBMS_ASSERT.ENQUOTE_NAME( - DBMS_ASSERT.SIMPLE_SQL_NAME(UPPER(:user_or_role_name)), + v_user_or_role_name := SYS.DBMS_ASSERT.ENQUOTE_NAME( + SYS.DBMS_ASSERT.SIMPLE_SQL_NAME(UPPER(:user_or_role_name)), FALSE ); EXECUTE IMMEDIATE @@ -207,14 +207,16 @@ async def async_create_credential( ): """Asynchronously create a credential. - Creates a credential object using DBMS_CLOUD.CREATE_CREDENTIAL. If replace + Creates a credential object using + C##CLOUD$SERVICE.DBMS_CLOUD.CREATE_CREDENTIAL. If replace is True, credential will be replaced if it already exists. If public_synonym is True, a public synonym with the credential name is created for the credential in the current schema. Creating the synonym requires the CREATE PUBLIC SYNONYM system privilege. :param Mapping credential: Credential attributes accepted by - DBMS_CLOUD.CREATE_CREDENTIAL, including credential_name. + C##CLOUD$SERVICE.DBMS_CLOUD.CREATE_CREDENTIAL, including + credential_name. :param bool replace: Replace an existing credential with the same name. :param bool public_synonym: Create a public synonym for the credential. :return: None @@ -224,20 +226,21 @@ async def async_create_credential( async with async_cursor() as cr: try: await cr.callproc( - "DBMS_CLOUD.CREATE_CREDENTIAL", keyword_parameters=credential + "C##CLOUD$SERVICE.DBMS_CLOUD.CREATE_CREDENTIAL", + keyword_parameters=credential, ) except oracledb.DatabaseError as e: (error,) = e.args # If already exists and replace is True then drop and recreate if error.code == 20022 and replace: await cr.callproc( - "DBMS_CLOUD.DROP_CREDENTIAL", + "C##CLOUD$SERVICE.DBMS_CLOUD.DROP_CREDENTIAL", keyword_parameters={ "credential_name": credential["credential_name"] }, ) await cr.callproc( - "DBMS_CLOUD.CREATE_CREDENTIAL", + "C##CLOUD$SERVICE.DBMS_CLOUD.CREATE_CREDENTIAL", keyword_parameters=credential, ) else: @@ -257,7 +260,8 @@ async def async_delete_credential( ): """Asynchronously delete a credential. - Deletes a credential object using DBMS_CLOUD.DROP_CREDENTIAL. If + Deletes a credential object using + C##CLOUD$SERVICE.DBMS_CLOUD.DROP_CREDENTIAL. If public_synonym is True, also drops its public synonym. Dropping the synonym requires the DROP PUBLIC SYNONYM system privilege. @@ -276,7 +280,7 @@ async def async_delete_credential( try: await cr.callproc( - "DBMS_CLOUD.DROP_CREDENTIAL", + "C##CLOUD$SERVICE.DBMS_CLOUD.DROP_CREDENTIAL", keyword_parameters={"credential_name": credential_name}, ) except oracledb.DatabaseError as e: @@ -294,14 +298,16 @@ def create_credential( ): """Create a credential. - Creates a credential object using DBMS_CLOUD.CREATE_CREDENTIAL. If replace + Creates a credential object using + C##CLOUD$SERVICE.DBMS_CLOUD.CREATE_CREDENTIAL. If replace is True, credential will be replaced if it "already exists". If public_synonym is True, a public synonym with the credential name is created for the credential in the current schema. Creating the synonym requires the CREATE PUBLIC SYNONYM system privilege. :param Mapping credential: Credential attributes accepted by - DBMS_CLOUD.CREATE_CREDENTIAL, including credential_name. + C##CLOUD$SERVICE.DBMS_CLOUD.CREATE_CREDENTIAL, including + credential_name. :param bool replace: Replace an existing credential with the same name. :param bool public_synonym: Create a public synonym for the credential. :return: None @@ -311,20 +317,21 @@ def create_credential( with cursor() as cr: try: cr.callproc( - "DBMS_CLOUD.CREATE_CREDENTIAL", keyword_parameters=credential + "C##CLOUD$SERVICE.DBMS_CLOUD.CREATE_CREDENTIAL", + keyword_parameters=credential, ) except oracledb.DatabaseError as e: (error,) = e.args # If already exists and replace is True then drop and recreate if error.code == 20022 and replace: cr.callproc( - "DBMS_CLOUD.DROP_CREDENTIAL", + "C##CLOUD$SERVICE.DBMS_CLOUD.DROP_CREDENTIAL", keyword_parameters={ "credential_name": credential["credential_name"] }, ) cr.callproc( - "DBMS_CLOUD.CREATE_CREDENTIAL", + "C##CLOUD$SERVICE.DBMS_CLOUD.CREATE_CREDENTIAL", keyword_parameters=credential, ) else: @@ -361,7 +368,7 @@ def delete_credential( try: cr.callproc( - "DBMS_CLOUD.DROP_CREDENTIAL", + "C##CLOUD$SERVICE.DBMS_CLOUD.DROP_CREDENTIAL", keyword_parameters={"credential_name": credential_name}, ) except oracledb.DatabaseError as e: diff --git a/src/select_ai/privilege.py b/src/select_ai/privilege.py index 8a445b3..2cfb2d7 100644 --- a/src/select_ai/privilege.py +++ b/src/select_ai/privilege.py @@ -37,7 +37,7 @@ def _append_host_ace_statement(privileges: List[str]) -> str: privilege_list = ", ".join(f":{name}" for name in privilege_bind_names) return f""" BEGIN - DBMS_NETWORK_ACL_ADMIN.APPEND_HOST_ACE( + SYS.DBMS_NETWORK_ACL_ADMIN.APPEND_HOST_ACE( host => :host, lower_port => :lower_port, upper_port => :upper_port, @@ -58,7 +58,7 @@ def _remove_host_ace_statement(privileges: List[str]) -> str: privilege_list = ", ".join(f":{name}" for name in privilege_bind_names) return f""" BEGIN - DBMS_NETWORK_ACL_ADMIN.REMOVE_HOST_ACE( + SYS.DBMS_NETWORK_ACL_ADMIN.REMOVE_HOST_ACE( host => :host, lower_port => :lower_port, upper_port => :upper_port, @@ -92,8 +92,10 @@ def _append_host_ace_parameters( async def async_grant_privileges(users: Union[str, List[str]]): """ - This method grants execute privilege on the packages DBMS_CLOUD, - DBMS_CLOUD_AI, DBMS_CLOUD_AI_AGENT and DBMS_CLOUD_PIPELINE. + This method grants execute privilege on the packages + C##CLOUD$SERVICE.DBMS_CLOUD, C##CLOUD$SERVICE.DBMS_CLOUD_AI, + C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT and + C##CLOUD$SERVICE.DBMS_CLOUD_PIPELINE. """ if isinstance(users, str): @@ -107,8 +109,10 @@ async def async_grant_privileges(users: Union[str, List[str]]): async def async_revoke_privileges(users: Union[str, List[str]]): """ - This method revokes execute privilege on the packages DBMS_CLOUD, - DBMS_CLOUD_AI, DBMS_CLOUD_AI_AGENT and DBMS_CLOUD_PIPELINE. + This method revokes execute privilege on the packages + C##CLOUD$SERVICE.DBMS_CLOUD, C##CLOUD$SERVICE.DBMS_CLOUD_AI, + C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT and + C##CLOUD$SERVICE.DBMS_CLOUD_PIPELINE. """ if isinstance(users, str): @@ -216,8 +220,10 @@ async def async_revoke_network_access( def grant_privileges(users: Union[str, List[str]]): """ - This method grants execute privilege on the packages DBMS_CLOUD, - DBMS_CLOUD_AI, DBMS_CLOUD_AI_AGENT and DBMS_CLOUD_PIPELINE + This method grants execute privilege on the packages + C##CLOUD$SERVICE.DBMS_CLOUD, C##CLOUD$SERVICE.DBMS_CLOUD_AI, + C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT and + C##CLOUD$SERVICE.DBMS_CLOUD_PIPELINE """ if isinstance(users, str): users = [users] @@ -229,8 +235,10 @@ def grant_privileges(users: Union[str, List[str]]): def revoke_privileges(users: Union[str, List[str]]): """ - This method revokes execute privilege on the packages DBMS_CLOUD, - DBMS_CLOUD_AI, DBMS_CLOUD_AI_AGENT and DBMS_CLOUD_PIPELINE. + This method revokes execute privilege on the packages + C##CLOUD$SERVICE.DBMS_CLOUD, C##CLOUD$SERVICE.DBMS_CLOUD_AI, + C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT and + C##CLOUD$SERVICE.DBMS_CLOUD_PIPELINE. """ if isinstance(users, str): users = [users] diff --git a/src/select_ai/profile.py b/src/select_ai/profile.py index 833c55f..33f45fc 100644 --- a/src/select_ai/profile.py +++ b/src/select_ai/profile.py @@ -90,7 +90,8 @@ def _init_profile(self) -> None: def _get_profile_description( profile_name: str, owner: Optional[str] = None ) -> Tuple[Union[str, None], str]: - """Get a profile description and owner from ALL_CLOUD_AI_PROFILES. + """Get a profile description and owner from + C##CLOUD$SERVICE.ALL_CLOUD_AI_PROFILES. :param str profile_name: Name of the profile. :param str owner: Owner of a shared profile. Defaults to current schema. @@ -165,7 +166,8 @@ def _set_attribute( } with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI.SET_ATTRIBUTE", keyword_parameters=parameters + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.SET_ATTRIBUTE", + keyword_parameters=parameters, ) def set_attribute( @@ -208,7 +210,8 @@ def set_attributes(self, attributes: ProfileAttributes): } with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI.SET_ATTRIBUTES", keyword_parameters=parameters + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.SET_ATTRIBUTES", + keyword_parameters=parameters, ) self.attributes = self.get_attributes() @@ -231,7 +234,7 @@ def create(self, replace: Optional[int] = False) -> None: with cursor() as cr: try: cr.callproc( - "DBMS_CLOUD_AI.CREATE_PROFILE", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.CREATE_PROFILE", keyword_parameters=parameters, ) except oracledb.DatabaseError as e: @@ -240,7 +243,7 @@ def create(self, replace: Optional[int] = False) -> None: if error.code == 20046 and replace: self.delete(force=True) cr.callproc( - "DBMS_CLOUD_AI.CREATE_PROFILE", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.CREATE_PROFILE", keyword_parameters=parameters, ) else: @@ -250,7 +253,7 @@ def create(self, replace: Optional[int] = False) -> None: def _delete(profile_name: str, force: bool = False): with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI.DROP_PROFILE", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.DROP_PROFILE", keyword_parameters={ "profile_name": profile_name, "force": force, @@ -274,7 +277,7 @@ def enable(self) -> None: """ with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI.ENABLE_PROFILE", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.ENABLE_PROFILE", keyword_parameters={"profile_name": self.profile_name}, ) @@ -286,7 +289,7 @@ def disable(self) -> None: """ with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI.DISABLE_PROFILE", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.DISABLE_PROFILE", keyword_parameters={"profile_name": self.profile_name}, ) @@ -300,7 +303,7 @@ def grant_access(self, user_or_role_name: str) -> None: user_or_role_name = validate_user_or_role_name(user_or_role_name) with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI.GRANT_PROFILE_ACCESS", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.GRANT_PROFILE_ACCESS", keyword_parameters={ "profile_name": self.profile_name, "user_or_role_name": user_or_role_name, @@ -317,7 +320,7 @@ def revoke_access(self, user_or_role_name: str) -> None: user_or_role_name = validate_user_or_role_name(user_or_role_name) with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI.REVOKE_PROFILE_ACCESS", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.REVOKE_PROFILE_ACCESS", keyword_parameters={ "profile_name": self.profile_name, "user_or_role_name": user_or_role_name, @@ -375,7 +378,10 @@ def _save_feedback( ) params["profile_name"] = self.profile_name with cursor() as cr: - cr.callproc("DBMS_CLOUD_AI.FEEDBACK", keyword_parameters=params) + cr.callproc( + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.FEEDBACK", + keyword_parameters=params, + ) def add_positive_feedback( self, @@ -490,7 +496,7 @@ def _generate_with_cursor( prompt, action, params, attributes ) data = cr.callfunc( - "DBMS_CLOUD_AI.GENERATE", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.GENERATE", oracledb.DB_TYPE_CLOB, keyword_parameters=parameters, ) @@ -561,7 +567,7 @@ def _generate_stream_with_cursor( prompt, action, params, attributes ) data = cr.callfunc( - "DBMS_CLOUD_AI.GENERATE", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.GENERATE", oracledb.DB_TYPE_CLOB, keyword_parameters=parameters, ) @@ -827,7 +833,7 @@ def summarize( parameters["profile_name"] = self.profile_name with cursor() as cr: data = cr.callfunc( - "DBMS_CLOUD_AI.SUMMARIZE", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.SUMMARIZE", oracledb.DB_TYPE_CLOB, keyword_parameters=parameters, ) @@ -859,7 +865,7 @@ def generate_synthetic_data( keyword_parameters["profile_name"] = self.profile_name with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI.GENERATE_SYNTHETIC_DATA", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.GENERATE_SYNTHETIC_DATA", keyword_parameters=keyword_parameters, ) @@ -888,7 +894,7 @@ def translate( } with cursor() as cr: data = cr.callfunc( - "DBMS_CLOUD_AI.TRANSLATE", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.TRANSLATE", oracledb.DB_TYPE_CLOB, keyword_parameters=parameters, ) diff --git a/src/select_ai/sql.py b/src/select_ai/sql.py index 3790938..a888b5a 100644 --- a/src/select_ai/sql.py +++ b/src/select_ai/sql.py @@ -11,15 +11,15 @@ v_packages array_t; v_user VARCHAR2(261); BEGIN - v_user := DBMS_ASSERT.ENQUOTE_NAME( - DBMS_ASSERT.SCHEMA_NAME(:user), + v_user := SYS.DBMS_ASSERT.ENQUOTE_NAME( + SYS.DBMS_ASSERT.SCHEMA_NAME(:user), FALSE ); v_packages := array_t( - 'DBMS_CLOUD', - 'DBMS_CLOUD_AI', - 'DBMS_CLOUD_AI_AGENT', - 'DBMS_CLOUD_PIPELINE' + 'C##CLOUD$SERVICE.DBMS_CLOUD', + 'C##CLOUD$SERVICE.DBMS_CLOUD_AI', + 'C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT', + 'C##CLOUD$SERVICE.DBMS_CLOUD_PIPELINE' ); FOR i in 1..v_packages.count LOOP EXECUTE IMMEDIATE @@ -34,15 +34,15 @@ v_packages array_t; v_user VARCHAR2(261); BEGIN - v_user := DBMS_ASSERT.ENQUOTE_NAME( - DBMS_ASSERT.SCHEMA_NAME(:user), + v_user := SYS.DBMS_ASSERT.ENQUOTE_NAME( + SYS.DBMS_ASSERT.SCHEMA_NAME(:user), FALSE ); v_packages := array_t( - 'DBMS_CLOUD', - 'DBMS_CLOUD_AI', - 'DBMS_CLOUD_AI_AGENT', - 'DBMS_CLOUD_PIPELINE' + 'C##CLOUD$SERVICE.DBMS_CLOUD', + 'C##CLOUD$SERVICE.DBMS_CLOUD_AI', + 'C##CLOUD$SERVICE.DBMS_CLOUD_AI_AGENT', + 'C##CLOUD$SERVICE.DBMS_CLOUD_PIPELINE' ); FOR i in 1..v_packages.count LOOP EXECUTE IMMEDIATE @@ -53,7 +53,7 @@ ENABLE_AI_PROFILE_DOMAIN_FOR_USER = """ BEGIN - DBMS_NETWORK_ACL_ADMIN.APPEND_HOST_ACE( + SYS.DBMS_NETWORK_ACL_ADMIN.APPEND_HOST_ACE( host => :host, ace => xs$ace_type(privilege_list => xs$name_list('http'), principal_name => :user, @@ -64,7 +64,7 @@ DISABLE_AI_PROFILE_DOMAIN_FOR_USER = """ BEGIN - DBMS_NETWORK_ACL_ADMIN.REMOVE_HOST_ACE( + SYS.DBMS_NETWORK_ACL_ADMIN.REMOVE_HOST_ACE( host => :host, ace => xs$ace_type(privilege_list => xs$name_list('http'), principal_name => :user, @@ -75,14 +75,14 @@ GET_ALL_AI_PROFILE_ATTRIBUTES = """ SELECT attribute_name, attribute_value -FROM ALL_CLOUD_AI_PROFILE_ATTRIBUTES +FROM C##CLOUD$SERVICE.ALL_CLOUD_AI_PROFILE_ATTRIBUTES WHERE profile_name = :profile_name AND owner = COALESCE(:owner, SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA')) """ GET_ALL_AI_PROFILE = """ SELECT profile_name, description, owner -FROM ALL_CLOUD_AI_PROFILES +FROM C##CLOUD$SERVICE.ALL_CLOUD_AI_PROFILES WHERE profile_name = :profile_name AND owner = COALESCE(:owner, SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA')) """ @@ -90,28 +90,28 @@ LIST_ALL_AI_PROFILES = """ SELECT profile_name, description, owner -FROM ALL_CLOUD_AI_PROFILES +FROM C##CLOUD$SERVICE.ALL_CLOUD_AI_PROFILES WHERE REGEXP_LIKE(profile_name, :profile_name_pattern, 'i') AND owner = COALESCE(:owner, SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA')) """ LIST_ALL_VECTOR_INDEXES = """ SELECT v.index_name, v.description, v.owner -FROM ALL_CLOUD_VECTOR_INDEXES v +FROM C##CLOUD$SERVICE.ALL_CLOUD_VECTOR_INDEXES v WHERE REGEXP_LIKE(v.index_name, :index_name_pattern, 'i') AND v.owner = COALESCE(:owner, SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA')) """ GET_ALL_VECTOR_INDEX = """ SELECT index_name, description, owner -FROM ALL_CLOUD_VECTOR_INDEXES +FROM C##CLOUD$SERVICE.ALL_CLOUD_VECTOR_INDEXES WHERE index_name = :index_name AND owner = COALESCE(:owner, SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA')) """ GET_ALL_VECTOR_INDEX_ATTRIBUTES = """ SELECT attribute_name, attribute_value -FROM ALL_CLOUD_VECTOR_INDEX_ATTRIBUTES +FROM C##CLOUD$SERVICE.ALL_CLOUD_VECTOR_INDEX_ATTRIBUTES WHERE index_name = :index_name AND owner = COALESCE(:owner, SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA')) """ @@ -121,14 +121,14 @@ conversation_title, description, retention_days -from USER_CLOUD_AI_CONVERSATIONS +from C##CLOUD$SERVICE.USER_CLOUD_AI_CONVERSATIONS """ GET_USER_CONVERSATION_ATTRIBUTES = """ SELECT conversation_title, description, retention_days -from USER_CLOUD_AI_CONVERSATIONS +from C##CLOUD$SERVICE.USER_CLOUD_AI_CONVERSATIONS WHERE conversation_id = :conversation_id """ @@ -147,7 +147,7 @@ client_ip, sid, serial# -FROM USER_CLOUD_AI_CONVERSATION_PROMPTS +FROM C##CLOUD$SERVICE.USER_CLOUD_AI_CONVERSATION_PROMPTS WHERE conversation_id = :conversation_id ORDER BY created """ @@ -155,6 +155,6 @@ GET_VECTOR_PIPELINE_LAST_EXECUTION = """ SELECT CAST(last_execution AT TIME ZONE 'UTC' AS TIMESTAMP) -FROM USER_CLOUD_PIPELINES +FROM C##CLOUD$SERVICE.USER_CLOUD_PIPELINES WHERE pipeline_name = :pipeline_name """ diff --git a/src/select_ai/vector_index.py b/src/select_ai/vector_index.py index 51791ef..a18384f 100644 --- a/src/select_ai/vector_index.py +++ b/src/select_ai/vector_index.py @@ -251,7 +251,7 @@ def create( with cursor() as cr: try: cr.callproc( - "DBMS_CLOUD_AI.CREATE_VECTOR_INDEX", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.CREATE_VECTOR_INDEX", keyword_parameters=parameters, ) except oracledb.DatabaseError as e: @@ -260,7 +260,7 @@ def create( if error.code == 20048 and replace: self.delete(force=True) cr.callproc( - "DBMS_CLOUD_AI.CREATE_VECTOR_INDEX", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.CREATE_VECTOR_INDEX", keyword_parameters=parameters, ) else: @@ -302,7 +302,7 @@ def delete( """ with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI.DROP_VECTOR_INDEX", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.DROP_VECTOR_INDEX", keyword_parameters={ "index_name": self.index_name, "include_data": include_data, @@ -323,7 +323,7 @@ def enable(self): with cursor() as cr: try: cr.callproc( - "DBMS_CLOUD_AI.ENABLE_VECTOR_INDEX", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.ENABLE_VECTOR_INDEX", keyword_parameters={"index_name": self.index_name}, ) except oracledb.Error as e: @@ -347,7 +347,7 @@ def disable(self): with cursor() as cr: try: cr.callproc( - "DBMS_CLOUD_AI.DISABLE_VECTOR_INDEX", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.DISABLE_VECTOR_INDEX", keyword_parameters={"index_name": self.index_name}, ) except oracledb.Error as e: @@ -363,7 +363,7 @@ def grant_access(self, user_or_role_name: str) -> None: user_or_role_name = validate_user_or_role_name(user_or_role_name) with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI.GRANT_VECTOR_INDEX_ACCESS", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.GRANT_VECTOR_INDEX_ACCESS", keyword_parameters={ "index_name": self.index_name, "user_or_role_name": user_or_role_name, @@ -375,7 +375,7 @@ def revoke_access(self, user_or_role_name: str) -> None: user_or_role_name = validate_user_or_role_name(user_or_role_name) with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI.REVOKE_VECTOR_INDEX_ACCESS", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.REVOKE_VECTOR_INDEX_ACCESS", keyword_parameters={ "index_name": self.index_name, "user_or_role_name": user_or_role_name, @@ -432,7 +432,7 @@ def set_attribute( } with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI.UPDATE_VECTOR_INDEX", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.UPDATE_VECTOR_INDEX", keyword_parameters=parameters, ) @@ -456,7 +456,7 @@ def set_attributes( } with cursor() as cr: cr.callproc( - "DBMS_CLOUD_AI.UPDATE_VECTOR_INDEX", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.UPDATE_VECTOR_INDEX", keyword_parameters=parameters, ) self.attributes = self.get_attributes() @@ -635,7 +635,7 @@ async def create( async with async_cursor() as cr: try: await cr.callproc( - "DBMS_CLOUD_AI.CREATE_VECTOR_INDEX", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.CREATE_VECTOR_INDEX", keyword_parameters=parameters, ) except oracledb.DatabaseError as e: @@ -644,7 +644,7 @@ async def create( if error.code == 20048 and replace: await self.delete(force=True) await cr.callproc( - "DBMS_CLOUD_AI.CREATE_VECTOR_INDEX", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.CREATE_VECTOR_INDEX", keyword_parameters=parameters, ) else: @@ -670,7 +670,7 @@ async def delete( """ async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI.DROP_VECTOR_INDEX", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.DROP_VECTOR_INDEX", keyword_parameters={ "index_name": self.index_name, "include_data": include_data, @@ -709,7 +709,7 @@ async def enable(self) -> None: async with async_cursor() as cr: try: await cr.callproc( - "DBMS_CLOUD_AI.ENABLE_VECTOR_INDEX", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.ENABLE_VECTOR_INDEX", keyword_parameters={"index_name": self.index_name}, ) except oracledb.DatabaseError as e: @@ -733,7 +733,7 @@ async def disable(self) -> None: async with async_cursor() as cr: try: await cr.callproc( - "DBMS_CLOUD_AI.DISABLE_VECTOR_INDEX", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.DISABLE_VECTOR_INDEX", keyword_parameters={"index_name": self.index_name}, ) except oracledb.Error as e: @@ -748,7 +748,7 @@ async def grant_access(self, user_or_role_name: str) -> None: user_or_role_name = validate_user_or_role_name(user_or_role_name) async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI.GRANT_VECTOR_INDEX_ACCESS", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.GRANT_VECTOR_INDEX_ACCESS", keyword_parameters={ "index_name": self.index_name, "user_or_role_name": user_or_role_name, @@ -760,7 +760,7 @@ async def revoke_access(self, user_or_role_name: str) -> None: user_or_role_name = validate_user_or_role_name(user_or_role_name) async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI.REVOKE_VECTOR_INDEX_ACCESS", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.REVOKE_VECTOR_INDEX_ACCESS", keyword_parameters={ "index_name": self.index_name, "user_or_role_name": user_or_role_name, @@ -817,7 +817,7 @@ async def set_attribute( setattr(self.attributes, attribute_name, attribute_value) async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI.UPDATE_VECTOR_INDEX", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.UPDATE_VECTOR_INDEX", keyword_parameters=parameters, ) @@ -838,7 +838,7 @@ async def set_attributes(self, attributes: VectorIndexAttributes) -> None: } async with async_cursor() as cr: await cr.callproc( - "DBMS_CLOUD_AI.UPDATE_VECTOR_INDEX", + "C##CLOUD$SERVICE.DBMS_CLOUD_AI.UPDATE_VECTOR_INDEX", keyword_parameters=parameters, ) self.attributes = await self.get_attributes() From 13afb58ccfe8b82ce26b4557bb2bb9ab76aa0836 Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Tue, 15 Sep 2026 15:20:07 -0700 Subject: [PATCH 26/31] Simplify A2A deployment and session configuration --- doc/source/image/a2a_architecture.svg | 6 +- doc/source/user_guide/a2a.rst | 106 +++-- doc/source/user_guide/cli.rst | 53 +-- docker/a2a-entrypoint.sh | 1 + gcloud/README.md | 3 +- gcloud/gateway/README.md | 5 +- gcloud/gateway/cloudbuild.yaml | 6 +- gcloud/gateway/gke/worker-mtls.yaml | 12 +- gcloud/gateway/gke/worker.yaml | 8 +- samples/a2a/gateway/README.md | 16 +- src/select_ai/agent/a2a/__init__.py | 17 +- src/select_ai/agent/a2a/context_store.py | 18 +- src/select_ai/agent/a2a/forms.py | 210 ++++++--- src/select_ai/agent/a2a/gateway.py | 123 +++++- src/select_ai/agent/a2a/models.py | 94 +++- src/select_ai/agent/a2a/session_process.py | 447 +++++++++++++++++++ src/select_ai/agent/a2a/task_store.py | 22 +- src/select_ai/agent/a2a/worker.py | 489 +++------------------ src/select_ai/agent/a2a/worker_client.py | 4 +- src/select_ai/cli/a2a.py | 289 +++++++----- tests/a2a/test_agent_card.py | 4 +- tests/a2a/test_worker_runtime.py | 69 ++- 22 files changed, 1258 insertions(+), 744 deletions(-) create mode 100644 src/select_ai/agent/a2a/session_process.py diff --git a/doc/source/image/a2a_architecture.svg b/doc/source/image/a2a_architecture.svg index c988b00..a9b9dfc 100644 --- a/doc/source/image/a2a_architecture.svg +++ b/doc/source/image/a2a_architecture.svg @@ -56,8 +56,8 @@ Oracle Database - SELECT_AI_A2A_TASKS - SELECT_AI_A2A_CONTEXTS + DBMS_AI_A2A_TASKS[$] + DBMS_AI_A2A_CONTEXTS[$] conversation history @@ -72,7 +72,7 @@ A2UI-capable client - select-ai a2a gateway + a2a serve --deployment clustered public Agent Card and A2UI A2A JSON-RPC, no streaming session/task proxy diff --git a/doc/source/user_guide/a2a.rst b/doc/source/user_guide/a2a.rst index f6a8eb5..a4d3bf7 100644 --- a/doc/source/user_guide/a2a.rst +++ b/doc/source/user_guide/a2a.rst @@ -14,10 +14,10 @@ There are two deployment modes: * **Standalone A2A server**: one server owns a configured Oracle connection pool and one configured Select AI team. This mode supports streaming and is suitable when the service owner controls the database identity. -* **Dynamic A2A gateway**: a public gateway asks the client for a database DSN, - username, password, and team name through an A2UI form. It opens a temporary - isolated worker session for that selection. This mode supports task polling, - but does not advertise or implement streaming. +* **Clustered A2A server**: the public server asks the client for any database + DSN, username, password, and team values not fixed at deployment time. It + opens a temporary isolated worker session for that selection. This mode + supports task polling, but does not advertise or implement streaming. .. only:: html @@ -47,7 +47,8 @@ that can reach the required Oracle Database and, for dynamic deployments, the Consul service: * ``select-ai a2a serve`` runs the standalone server. -* ``select-ai a2a gateway`` runs the public dynamic gateway. +* ``select-ai a2a serve --deployment clustered`` runs the public dynamic + server. * ``select-ai a2a worker`` runs a database-bearing dynamic worker. The commands can be packaged into the platform's preferred container or @@ -77,7 +78,7 @@ The public routes are the same in both modes: - Selected by the client for each session. * - Public process - ``select-ai a2a serve`` - - ``select-ai a2a gateway`` + - ``select-ai a2a serve --deployment clustered`` * - Session process - The server's shared asynchronous connection pool. - A worker child process and connection pool per active session. @@ -285,19 +286,23 @@ Persistent Oracle-backed state Both A2A deployment modes use Oracle implementations of the A2A task and context stores. In standalone mode, the server initializes these stores when -the application starts. In dynamic gateway mode, ``select-ai a2a gateway`` -does not connect to Oracle itself; the ``select-ai a2a worker`` command starts +the application starts. In clustered mode, +``select-ai a2a serve --deployment clustered`` does not connect to Oracle +itself; the ``select-ai a2a worker`` command starts the internal worker, and each connected worker session initializes the stores after it opens its supplied database connection. On first initialization, the stores create these tables if they do not already exist: -``SELECT_AI_A2A_TASKS`` +``DBMS_AI_A2A_TASKS$`` and ``DBMS_AI_A2A_TASKS`` Stores the task ID, context ID, serialized task JSON, owner, and update timestamp. It supports task retrieval, filtering, listing, pagination, and - deletion. + deletion. The object with the ``$`` suffix is the writable internal table; + the object without it is a read-only view of the same columns. -``SELECT_AI_A2A_CONTEXTS`` - Maps an A2A context and owner to an Oracle conversation ID. +``DBMS_AI_A2A_CONTEXTS$`` and ``DBMS_AI_A2A_CONTEXTS`` + Maps an A2A context and owner to an Oracle conversation ID. The object with + the ``$`` suffix is the writable internal table; the object without it is a + read-only view of the same columns. When a request starts a new context, Select AI creates an ``AsyncConversation`` and passes its ID to ``AsyncTeam.run``. Later messages in @@ -389,27 +394,33 @@ Start the three local components in separate terminals: consul agent -dev -bind=127.0.0.1 -client=127.0.0.1 # Terminal 2: worker - CONSUL_HTTP_URL=http://127.0.0.1:8500 \ - WORKER_ID=local-worker \ - WORKER_ADDRESS=127.0.0.1 \ - WORKER_PORT=8081 \ select-ai a2a worker \ --host 127.0.0.1 \ - --port 8081 + --port 8081 \ + --consul-url http://127.0.0.1:8500 \ + --worker-id local-worker \ + --worker-endpoint http://127.0.0.1:8081 # Terminal 3: public gateway - select-ai a2a gateway \ + select-ai a2a serve \ + --deployment clustered \ --host 127.0.0.1 \ --port 8000 \ - --agent-url http://127.0.0.1:8000 \ + --public-url http://127.0.0.1:8000 \ --consul-url http://127.0.0.1:8500 -The worker uses ``CONSUL_HTTP_URL`` (default ``http://consul:8500``), -``WORKER_ID``, ``WORKER_ADDRESS``, ``WORKER_PORT``, and the optional -``WORKER_ENDPOINT`` environment variables when registering with Consul. The -gateway uses ``AGENT_URL``, ``CONSUL_HTTP_URL``, ``WORKER_SERVICE`` (default -``select-ai-worker``), and ``SESSION_TTL_SECONDS``. The command-line options -override the corresponding environment variables. +The worker uses ``--consul-url``, ``--worker-id``, and the optional +``--worker-endpoint`` when registering with Consul. Their environment +fallbacks are ``CONSUL_HTTP_URL``, ``WORKER_ID``, and ``WORKER_ENDPOINT``; +explicit command-line values take precedence. ``--port`` is both the listening +port and the registered port. Cluster manifests can set ``WORKER_ADDRESS`` to +the pod IP when no advertised endpoint is supplied. Clustered serve uses +``PUBLIC_URL``, ``CONSUL_HTTP_URL``, ``WORKER_SERVICE`` (default +``select-ai-a2a-worker``), and ``SESSION_TTL_SECONDS``. + +Pass any fixed connection values with ``--dsn``, ``--user``, ``--password``, +and ``--team``. The generated form contains only the remaining values. Use +``--a2ui-form`` to supply a validated custom form for those missing values. The worker must be able to resolve the submitted DSN. For a TNS alias, set ``TNS_ADMIN`` in the worker terminal before starting it. The gateway session @@ -424,9 +435,10 @@ operations are rejected because the gateway does not proxy a live stream. A2UI database-connection form ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The first ``message/send`` for a context returns a temporary task containing a -database connection form. The form is an A2UI v0.9 data artifact with the -``application/json+a2ui`` MIME type. It contains these fields: +When connection properties are missing, the first ``message/send`` for a +context returns a temporary task containing a database connection form. The +form is an A2UI v0.9 data artifact with the ``application/json+a2ui`` MIME +type. It contains only the values not configured on the server: .. list-table:: Dynamic connection form :header-rows: 1 @@ -445,7 +457,22 @@ database connection form. The form is an A2UI v0.9 data artifact with the * - ``team_name`` - Select AI Agent Team to run in this session. * - ``submit_database_connection`` - - A2UI action that submits the four values to the gateway. + - A2UI action that submits exactly the displayed values. + +For example, a clustered server started with ``--dsn`` and ``--team`` renders +only username and password. Server-configured values are immutable and cannot +be overridden by a form submission. When all four values are configured, the +server skips the form and opens the worker session automatically. + +Use ``--a2ui-form PATH`` to replace the generated form. The file must be a +top-level JSON array of A2UI v0.9 operations using the advertised catalog and +one consistent template ``surfaceId``. It must contain exactly one +``submit_database_connection`` action whose context submits exactly the +missing canonical properties: ``dsn``, ``username``, ``password``, and/or +``team_name``. A collected password must use an obscured ``TextField`` and the +template must not contain a password default. The server validates the file at +startup and replaces its template surface ID with a fresh ID every time the +form is displayed. After the action is submitted, the gateway selects a healthy worker through Consul and opens a session. The worker starts a child process, calls @@ -562,7 +589,8 @@ Consul is used for two kinds of routing metadata: * the worker service registration and health TTL, which let the gateway select a passing worker; and -* ``select-ai/sessions/`` and ``select-ai/tasks/`` key-value records, which +* ``select-ai-a2a/sessions/`` and ``select-ai-a2a/tasks/`` key-value records, + which route a context or task back to the worker that owns it. The task and conversation contents are not stored in Consul. They are stored @@ -579,8 +607,9 @@ All three gateway files are required together: .. code-block:: bash - select-ai a2a gateway \ - --agent-url https://gateway.example.com \ + select-ai a2a serve \ + --deployment clustered \ + --public-url https://gateway.example.com \ --worker-tls-ca-file /run/secrets/worker-ca.pem \ --worker-tls-cert-file /run/secrets/gateway-client.crt \ --worker-tls-key-file /run/secrets/gateway-client.key @@ -590,11 +619,12 @@ Start the worker with the matching server certificate, key, and CA: .. code-block:: bash select-ai a2a worker \ + --worker-endpoint https://worker.example.com:8443 \ --tls-cert-file /run/secrets/worker.crt \ --tls-key-file /run/secrets/worker.key \ --tls-ca-file /run/secrets/gateway-client-ca.pem -The worker must register an HTTPS endpoint through ``WORKER_ENDPOINT`` when +The worker must register an HTTPS endpoint through ``--worker-endpoint`` when mTLS is enabled. The gateway verifies the worker certificate with the CA and presents its client certificate. This mTLS option protects only the gateway-to-worker HTTP hop. It does not add wallet-based mTLS to the gateway's @@ -686,8 +716,8 @@ perform these steps: 4. Build and publish the image with Cloud Build. 5. Deploy the requested worker replicas with ``select-ai a2a worker`` and the selected session TTL. -6. Deploy Cloud Run with ``select-ai a2a gateway`` and set ``AGENT_URL`` to the - final Cloud Run URL. +6. Deploy Cloud Run with ``select-ai a2a serve --deployment clustered`` and + set ``PUBLIC_URL`` to the final Cloud Run URL. The Cloud Run gateway uses direct VPC egress to reach the internal Consul load-balancer address and worker endpoints. The GKE worker service is headless @@ -747,9 +777,9 @@ The deployment files are intended to be read together: Troubleshooting and security notes ================================== -* If the Agent Card advertises the wrong URL, set ``--public-url`` for the - standalone server or ``AGENT_URL`` for the gateway. The URL must be the - client-visible base URL, not an internal container address. +* If the Agent Card advertises the wrong URL, set ``--public-url`` or + ``PUBLIC_URL``. The URL must be the client-visible base URL, not an internal + container address. * If the gateway returns the connection form repeatedly, check that the worker is passing in Consul, that the worker can resolve and connect to Oracle, and that the session TTL has not expired. diff --git a/doc/source/user_guide/cli.rst b/doc/source/user_guide/cli.rst index 223675e..3adcc0b 100644 --- a/doc/source/user_guide/cli.rst +++ b/doc/source/user_guide/cli.rst @@ -206,7 +206,6 @@ current defaults: select-ai a2a --help select-ai a2a serve --help - select-ai a2a gateway --help select-ai a2a worker --help select-ai a2a agent-card --help @@ -218,11 +217,9 @@ current defaults: * - Command - Purpose * - ``select-ai a2a serve`` - - Start a standalone A2A HTTP server for one configured database AI Agent - Team. It accepts the database connection and optional wallet options. - * - ``select-ai a2a gateway`` - - Start the public dynamic A2A/A2UI gateway. It uses Consul to discover - workers and does not connect to Oracle directly. + - Start the public A2A HTTP server in standalone or clustered deployment + mode. Standalone accepts database connection and wallet options; + clustered uses Consul to discover workers. * - ``select-ai a2a worker`` - Start the internal worker that registers with Consul and creates an isolated database-bearing child process for each submitted connection. @@ -270,29 +267,37 @@ passing the A2UI form values to the worker. .. code-block:: bash - CONSUL_HTTP_URL=http://127.0.0.1:8500 \ - WORKER_ID=local-worker \ - WORKER_ADDRESS=127.0.0.1 \ - WORKER_PORT=8081 \ - select-ai a2a worker --host 127.0.0.1 --port 8081 + select-ai a2a worker \ + --host 127.0.0.1 \ + --port 8081 \ + --consul-url http://127.0.0.1:8500 \ + --worker-id local-worker \ + --worker-endpoint http://127.0.0.1:8081 - select-ai a2a gateway \ + select-ai a2a serve \ + --deployment clustered \ --host 127.0.0.1 \ --port 8000 \ - --agent-url http://127.0.0.1:8000 \ + --public-url http://127.0.0.1:8000 \ --consul-url http://127.0.0.1:8500 -The worker options are ``--host``, ``--port``, ``--session-ttl-seconds``, and -``--session-start-timeout-seconds``. Its registration can be configured with -the ``CONSUL_HTTP_URL``, ``WORKER_ID``, ``WORKER_ADDRESS``, ``WORKER_PORT``, -and optional ``WORKER_ENDPOINT`` environment variables. The worker's -``--tls-cert-file``, ``--tls-key-file``, and ``--tls-ca-file`` options enable -gateway-to-worker mTLS; provide all three together. - -The gateway options are ``--agent-url`` (required, or ``AGENT_URL``), +The worker options are ``--host``, ``--port``, ``--worker-id``, +``--consul-url``, ``--worker-endpoint``, ``--session-ttl-seconds``, and +``--session-start-timeout-seconds``. ``WORKER_ID``, ``CONSUL_HTTP_URL``, and +``WORKER_ENDPOINT`` are environment fallbacks; explicit command-line values +take precedence. ``--port`` is also the port registered with Consul. Cluster +manifests can set ``WORKER_ADDRESS`` to the pod IP when no endpoint is +supplied. The worker's ``--tls-cert-file``, ``--tls-key-file``, and +``--tls-ca-file`` options enable gateway-to-worker mTLS; provide all three +together. + +The clustered serve options are ``--public-url`` (or ``PUBLIC_URL``), ``--consul-url`` (or ``CONSUL_HTTP_URL``), ``--worker-service`` (or ``WORKER_SERVICE``), and ``--session-ttl-seconds`` (or -``SESSION_TTL_SECONDS``). The optional +``SESSION_TTL_SECONDS``). ``--dsn``, ``--user``, ``--password``, and ``--team`` +fix any supplied connection values; the generated A2UI form contains only the +missing values. ``--a2ui-form`` loads a custom A2UI JSON form that must submit +exactly those missing values. The optional ``--worker-tls-ca-file``, ``--worker-tls-cert-file``, and ``--worker-tls-key-file`` options configure the gateway's client side of gateway-to-worker mTLS. These TLS settings protect the internal HTTP hop and @@ -341,9 +346,7 @@ Command summary * - ``select-ai profile translate`` - Translate text with a saved profile. * - ``select-ai a2a serve`` - - Start the standalone A2A server. - * - ``select-ai a2a gateway`` - - Start the public dynamic A2A gateway. + - Start the public A2A server in standalone or clustered mode. * - ``select-ai a2a worker`` - Start the internal dynamic-session worker. * - ``select-ai a2a agent-card`` diff --git a/docker/a2a-entrypoint.sh b/docker/a2a-entrypoint.sh index 258a9b1..807fdfc 100644 --- a/docker/a2a-entrypoint.sh +++ b/docker/a2a-entrypoint.sh @@ -33,6 +33,7 @@ fi : "${SELECT_AI_POOL_MAX_SIZE:=10}" exec select-ai a2a serve \ + --deployment standalone \ --team "$SELECT_AI_A2A_TEAM" \ --host 0.0.0.0 \ --port "${PORT:-8080}" \ diff --git a/gcloud/README.md b/gcloud/README.md index bf005a8..dca668e 100644 --- a/gcloud/README.md +++ b/gcloud/README.md @@ -20,7 +20,8 @@ the configured session TTL remain the limiting factors. ![Select AI A2A deployment architecture](../doc/source/image/a2a_architecture.svg) The A2A commands are cloud-neutral: `select-ai a2a serve`, -`select-ai a2a gateway`, and `select-ai a2a worker` can run as processes or +`select-ai a2a serve --deployment clustered`, and `select-ai a2a worker` can +run as processes or containers on any cloud platform, a Kubernetes cluster, or self-managed infrastructure with the required Oracle and Consul connectivity. The scripts in this directory are optional Google Cloud automation for the Cloud Run/GKE diff --git a/gcloud/gateway/README.md b/gcloud/gateway/README.md index 5e11273..a8814c7 100644 --- a/gcloud/gateway/README.md +++ b/gcloud/gateway/README.md @@ -100,8 +100,9 @@ when a worker Pod is recreated. Cloud Build then: 2. deploys the GKE namespace and internal Consul service; 3. deploys the requested number of GKE worker replicas using `select-ai a2a worker`; -4. deploys the same image to Cloud Run using `select-ai a2a gateway`; -5. sets the final Cloud Run URL in `AGENT_URL` for the Agent Card. +4. deploys the same image to Cloud Run using + `select-ai a2a serve --deployment clustered`; +5. sets the final Cloud Run URL in `PUBLIC_URL` for the Agent Card. Common options: diff --git a/gcloud/gateway/cloudbuild.yaml b/gcloud/gateway/cloudbuild.yaml index 89bb0af..42a19b4 100644 --- a/gcloud/gateway/cloudbuild.yaml +++ b/gcloud/gateway/cloudbuild.yaml @@ -186,14 +186,14 @@ steps: --min-instances=1 \ --port=8080 \ --command=select-ai \ - --args=a2a,gateway \ - --set-env-vars="AGENT_URL=https://pending.invalid,CONSUL_HTTP_URL=http://$consul_ip:8500,WORKER_SERVICE=select-ai-worker,SESSION_TTL_SECONDS=${_SESSION_TTL_SECONDS}$mtls_env" \ + --args=a2a,serve,--deployment,clustered,--host,0.0.0.0 \ + --set-env-vars="PUBLIC_URL=https://pending.invalid,CONSUL_HTTP_URL=http://$consul_ip:8500,WORKER_SERVICE=select-ai-a2a-worker,SESSION_TTL_SECONDS=${_SESSION_TTL_SECONDS}$mtls_env" \ "${mtls_args[@]}" gateway_url=$(gcloud run services describe "${_GATEWAY_SERVICE}" \ --region="${_REGION}" --format='value(status.url)') gcloud run services update "${_GATEWAY_SERVICE}" \ --region="${_REGION}" \ - --update-env-vars="AGENT_URL=$gateway_url" + --update-env-vars="PUBLIC_URL=$gateway_url" images: - ${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/select-ai:${_IMAGE_TAG} diff --git a/gcloud/gateway/gke/worker-mtls.yaml b/gcloud/gateway/gke/worker-mtls.yaml index bdab56e..624ad8e 100644 --- a/gcloud/gateway/gke/worker-mtls.yaml +++ b/gcloud/gateway/gke/worker-mtls.yaml @@ -26,6 +26,12 @@ spec: - 0.0.0.0 - --port - "8443" + - --consul-url + - http://consul-server.select-ai-gateway.svc.cluster.local:8500 + - --worker-id + - $(WORKER_ID) + - --worker-endpoint + - https://$(POD_NAME).select-ai-worker.select-ai-gateway.svc.GKE_DNS_DOMAIN:8443 - --session-ttl-seconds - "SESSION_TTL_SECONDS" - --tls-cert-file @@ -38,8 +44,6 @@ spec: - name: https containerPort: 8443 env: - - name: CONSUL_HTTP_URL - value: http://consul-server.select-ai-gateway.svc.cluster.local:8500 - name: WORKER_ID valueFrom: fieldRef: @@ -52,10 +56,6 @@ spec: valueFrom: fieldRef: fieldPath: status.podIP - - name: WORKER_PORT - value: "8443" - - name: WORKER_ENDPOINT - value: https://$(POD_NAME).select-ai-worker.select-ai-gateway.svc.GKE_DNS_DOMAIN:8443 volumeMounts: - name: worker-server-tls mountPath: /var/run/select-ai-mtls/tls.crt diff --git a/gcloud/gateway/gke/worker.yaml b/gcloud/gateway/gke/worker.yaml index 3b5392b..1fb2481 100644 --- a/gcloud/gateway/gke/worker.yaml +++ b/gcloud/gateway/gke/worker.yaml @@ -26,14 +26,16 @@ spec: - 0.0.0.0 - --port - "8080" + - --consul-url + - http://consul-server.select-ai-gateway.svc.cluster.local:8500 + - --worker-id + - $(WORKER_ID) - --session-ttl-seconds - "SESSION_TTL_SECONDS" ports: - name: http containerPort: 8080 env: - - name: CONSUL_HTTP_URL - value: http://consul-server.select-ai-gateway.svc.cluster.local:8500 - name: WORKER_ID valueFrom: fieldRef: @@ -42,8 +44,6 @@ spec: valueFrom: fieldRef: fieldPath: status.podIP - - name: WORKER_PORT - value: "8080" readinessProbe: httpGet: path: /health diff --git a/samples/a2a/gateway/README.md b/samples/a2a/gateway/README.md index d5056f4..37ac6b6 100644 --- a/samples/a2a/gateway/README.md +++ b/samples/a2a/gateway/README.md @@ -50,11 +50,12 @@ Terminal 2, one worker: ```bash source .venv/bin/activate -CONSUL_HTTP_URL=http://127.0.0.1:8500 \ -WORKER_ID=local-worker \ -WORKER_ADDRESS=127.0.0.1 \ -WORKER_PORT=8081 \ -select-ai a2a worker --host 127.0.0.1 --port 8081 +select-ai a2a worker \ + --host 127.0.0.1 \ + --port 8081 \ + --consul-url http://127.0.0.1:8500 \ + --worker-id local-worker \ + --worker-endpoint http://127.0.0.1:8081 ``` Terminal 3, the gateway: @@ -62,10 +63,11 @@ Terminal 3, the gateway: ```bash source .venv/bin/activate -select-ai a2a gateway \ +select-ai a2a serve \ + --deployment clustered \ --host 127.0.0.1 \ --port 8000 \ - --agent-url http://127.0.0.1:8000 \ + --public-url http://127.0.0.1:8000 \ --consul-url http://127.0.0.1:8500 ``` diff --git a/src/select_ai/agent/a2a/__init__.py b/src/select_ai/agent/a2a/__init__.py index 7424546..cdc9347 100644 --- a/src/select_ai/agent/a2a/__init__.py +++ b/src/select_ai/agent/a2a/__init__.py @@ -7,7 +7,13 @@ """A2A support for Select AI Agent Teams and temporary sessions.""" -from .models import GatewaySettings, SessionInfo, SessionRoute +from .models import ( + ConnectionConfig, + GatewaySettings, + SessionInfo, + SessionRoute, + WorkerSettings, +) def create_gateway_app(settings): @@ -17,20 +23,19 @@ def create_gateway_app(settings): return factory(settings) -def create_worker_app( - session_ttl_seconds: int = 900, - session_start_timeout_seconds: int = 30, -): +def create_worker_app(settings: WorkerSettings): """Build the internal worker application.""" from select_ai.agent.a2a.worker import create_worker_app as factory - return factory(session_ttl_seconds, session_start_timeout_seconds) + return factory(settings) __all__ = [ "GatewaySettings", + "ConnectionConfig", "SessionInfo", "SessionRoute", + "WorkerSettings", "create_gateway_app", "create_worker_app", ] diff --git a/src/select_ai/agent/a2a/context_store.py b/src/select_ai/agent/a2a/context_store.py index 6dacc37..74ba492 100644 --- a/src/select_ai/agent/a2a/context_store.py +++ b/src/select_ai/agent/a2a/context_store.py @@ -20,15 +20,15 @@ _CREATE_TABLE = """ BEGIN EXECUTE IMMEDIATE ' - CREATE TABLE SELECT_AI_A2A_CONTEXTS ( + CREATE TABLE DBMS_AI_A2A_CONTEXTS$ ( owner VARCHAR2(512) NOT NULL, context_id VARCHAR2(255) NOT NULL, conversation_id VARCHAR2(255) NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL, - CONSTRAINT select_ai_a2a_contexts_pk PRIMARY KEY (owner, context_id) + CONSTRAINT dbms_ai_a2a_contexts_pk PRIMARY KEY (owner, context_id) )'; EXECUTE IMMEDIATE ' - COMMENT ON TABLE SELECT_AI_A2A_CONTEXTS + COMMENT ON TABLE DBMS_AI_A2A_CONTEXTS$ IS ''Managed by select_ai.a2a.context_store'''; EXCEPTION WHEN OTHERS THEN @@ -38,6 +38,13 @@ END; """ +_CREATE_VIEW = """ + CREATE OR REPLACE VIEW DBMS_AI_A2A_CONTEXTS AS + SELECT owner, context_id, conversation_id, created_at + FROM DBMS_AI_A2A_CONTEXTS$ + WITH READ ONLY +""" + class OracleContextStore: """Persist one Oracle conversation for each A2A context.""" @@ -58,6 +65,7 @@ async def initialize(self) -> None: if self.initialized: return await self._execute(_CREATE_TABLE) + await self._execute(_CREATE_VIEW) self.initialized = True async def get_or_create( @@ -83,7 +91,7 @@ async def get_or_create( try: await self._execute( """ - INSERT INTO SELECT_AI_A2A_CONTEXTS ( + INSERT INTO DBMS_AI_A2A_CONTEXTS$ ( owner, context_id, conversation_id, created_at ) VALUES ( :owner, :context_id, :conversation_id, SYSTIMESTAMP @@ -106,7 +114,7 @@ async def _get(self, owner: str, context_id: str) -> Optional[str]: row = await self._fetchone( """ SELECT conversation_id - FROM SELECT_AI_A2A_CONTEXTS + FROM DBMS_AI_A2A_CONTEXTS$ WHERE owner = :owner AND context_id = :context_id """, owner=owner, diff --git a/src/select_ai/agent/a2a/forms.py b/src/select_ai/agent/a2a/forms.py index 9d82bff..c05472a 100644 --- a/src/select_ai/agent/a2a/forms.py +++ b/src/select_ai/agent/a2a/forms.py @@ -5,20 +5,58 @@ # https://oss.oracle.com/licenses/upl. # ----------------------------------------------------------------------------- -"""A2UI connection form emitted by the public gateway.""" +"""Generation and validation for the A2UI database connection form.""" +from __future__ import annotations + +import json +from collections.abc import Iterable +from copy import deepcopy +from pathlib import Path from uuid import uuid4 from select_ai.agent.a2a.a2ui import A2UI_CATALOG_ID, A2UI_VERSION +from select_ai.agent.a2a.models import CONNECTION_FIELDS + +_ACTION_NAME = "submit_database_connection" +_FIELD_COMPONENTS = { + "dsn": ("Database DSN", "shortText"), + "username": ("Database username", "shortText"), + "password": ("Database password", "obscured"), + "team_name": ("Select AI team name", "shortText"), +} -def connection_form(surface_id: str | None = None) -> list[dict]: - """Return the non-persistent database connection form.""" - # A2UI surface IDs must be globally unique for the renderer's lifetime. - # Gemini retains surfaces for an A2A conversation after the connection - # form is submitted, so reusing a fixed ID prevents a reconnect form from - # being created in that same conversation. +def connection_form( + surface_id: str | None = None, + missing_fields: Iterable[str] | None = None, + template: tuple[dict, ...] | None = None, +) -> list[dict]: + """Return a fresh form containing only missing connection properties.""" + fields = ( + tuple(CONNECTION_FIELDS) + if missing_fields is None + else tuple(missing_fields) + ) + if not fields: + return [] + if set(fields) - set(CONNECTION_FIELDS): + raise ValueError("Connection form contains unsupported fields.") surface_id = surface_id or f"db-connect-{uuid4().hex}" + if template is not None: + return _rebind_surface(template, surface_id) + + field_components = [ + { + "id": field, + "component": "TextField", + "label": _FIELD_COMPONENTS[field][0], + "value": {"path": f"/{field}"}, + "variant": _FIELD_COMPONENTS[field][1], + } + for field in fields + ] + action_context = {field: {"path": f"/{field}"} for field in fields} return [ { "version": A2UI_VERSION, @@ -36,14 +74,7 @@ def connection_form(surface_id: str | None = None) -> list[dict]: { "id": "column", "component": "Column", - "children": [ - "title", - "dsn", - "user", - "password", - "team", - "connect", - ], + "children": ["title", *fields, "connect"], }, { "id": "title", @@ -51,34 +82,7 @@ def connection_form(surface_id: str | None = None) -> list[dict]: "text": "Connect to Oracle Database", "variant": "h2", }, - { - "id": "dsn", - "component": "TextField", - "label": "Database DSN", - "value": {"path": "/dsn"}, - "variant": "shortText", - }, - { - "id": "user", - "component": "TextField", - "label": "Database username", - "value": {"path": "/username"}, - "variant": "shortText", - }, - { - "id": "password", - "component": "TextField", - "label": "Database password", - "value": {"path": "/password"}, - "variant": "obscured", - }, - { - "id": "team", - "component": "TextField", - "label": "Select AI team name", - "value": {"path": "/team_name"}, - "variant": "shortText", - }, + *field_components, { "id": "connect_label", "component": "Text", @@ -91,13 +95,8 @@ def connection_form(surface_id: str | None = None) -> list[dict]: "variant": "primary", "action": { "event": { - "name": "submit_database_connection", - "context": { - "dsn": {"path": "/dsn"}, - "username": {"path": "/username"}, - "password": {"path": "/password"}, - "team_name": {"path": "/team_name"}, - }, + "name": _ACTION_NAME, + "context": action_context, } }, }, @@ -109,12 +108,111 @@ def connection_form(surface_id: str | None = None) -> list[dict]: "updateDataModel": { "surfaceId": surface_id, "path": "/", - "value": { - "dsn": "", - "username": "", - "password": "", - "team_name": "", - }, + "value": {field: "" for field in fields}, }, }, ] + + +def load_connection_form( + path: str, + missing_fields: Iterable[str], +) -> tuple[dict, ...]: + """Load and validate one custom A2UI form template.""" + try: + value = json.loads(Path(path).read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as error: + raise ValueError(f"Could not load A2UI form: {error}") from error + return validate_connection_form(value, missing_fields) + + +def validate_connection_form( + operations, + missing_fields: Iterable[str], +) -> tuple[dict, ...]: + """Validate the supported custom connection-form contract.""" + fields = tuple(missing_fields) + if not isinstance(operations, list) or not operations: + raise ValueError("A2UI form must be a non-empty JSON array.") + if not all(isinstance(operation, dict) for operation in operations): + raise ValueError("Every A2UI form operation must be an object.") + if any( + operation.get("version") != A2UI_VERSION for operation in operations + ): + raise ValueError(f"Every A2UI operation must use {A2UI_VERSION}.") + + surface_ids = set() + actions = [] + has_catalog = False + password_is_obscured = False + for operation in operations: + for value in operation.values(): + if isinstance(value, dict) and value.get("surfaceId"): + surface_ids.add(value["surfaceId"]) + created = operation.get("createSurface") + if isinstance(created, dict): + has_catalog = created.get("catalogId") == A2UI_CATALOG_ID + updated = operation.get("updateComponents") + if isinstance(updated, dict): + for component in updated.get("components") or []: + if not isinstance(component, dict): + continue + event = (component.get("action") or {}).get("event") + if ( + isinstance(event, dict) + and event.get("name") == _ACTION_NAME + ): + actions.append(event) + if ( + component.get("component") == "TextField" + and component.get("value") == {"path": "/password"} + and component.get("variant") == "obscured" + ): + password_is_obscured = True + data_model = operation.get("updateDataModel") + if isinstance(data_model, dict): + value = data_model.get("value") + if isinstance(value, dict) and value.get("password"): + raise ValueError("A2UI form must not embed a password value.") + + if not has_catalog: + raise ValueError( + "A2UI form must create the advertised catalog surface." + ) + if len(surface_ids) != 1: + raise ValueError( + "A2UI form must use one consistent template surfaceId." + ) + if len(actions) != 1: + raise ValueError( + f"A2UI form must contain exactly one {_ACTION_NAME} action." + ) + context = actions[0].get("context") + if not isinstance(context, dict) or set(context) != set(fields): + raise ValueError( + "A2UI form action must submit exactly the missing fields." + ) + if any(context[field] != {"path": f"/{field}"} for field in fields): + raise ValueError( + "A2UI form action uses an invalid connection field path." + ) + if "password" in fields and not password_is_obscured: + raise ValueError("A2UI password input must use the obscured variant.") + return tuple(deepcopy(operations)) + + +def _rebind_surface(template: tuple[dict, ...], surface_id: str) -> list[dict]: + operations = deepcopy(list(template)) + + def replace(value) -> None: + if isinstance(value, dict): + if "surfaceId" in value: + value["surfaceId"] = surface_id + for child in value.values(): + replace(child) + elif isinstance(value, list): + for child in value: + replace(child) + + replace(operations) + return operations diff --git a/src/select_ai/agent/a2a/gateway.py b/src/select_ai/agent/a2a/gateway.py index 1fc9834..da37ca6 100644 --- a/src/select_ai/agent/a2a/gateway.py +++ b/src/select_ai/agent/a2a/gateway.py @@ -58,8 +58,11 @@ a2ui_part, find_action, ) -from select_ai.agent.a2a.forms import connection_form -from select_ai.agent.a2a.models import GatewaySettings, SessionInfo +from select_ai.agent.a2a.forms import ( + connection_form, + validate_connection_form, +) +from select_ai.agent.a2a.models import ConnectionConfig, GatewaySettings from select_ai.agent.a2a.worker_client import ReconnectRequired, WorkerClient from select_ai.version import __version__ @@ -94,9 +97,17 @@ def _unsupported_stream(*_args, **_kwargs): class GatewayRequestHandler(RequestHandler): """Proxy A2A requests to the Oracle-backed handler in a worker child.""" - def __init__(self, worker_client: WorkerClient, agent_card: AgentCard): + def __init__( + self, + worker_client: WorkerClient, + agent_card: AgentCard, + connection: ConnectionConfig | None = None, + connection_form_template: tuple[dict, ...] | None = None, + ): self.worker_client = worker_client self.agent_card = agent_card + self.connection = connection or ConnectionConfig() + self.connection_form_template = connection_form_template # RequestHandler requires every operation even when the Agent Card does # not advertise streaming or push notifications. @@ -128,11 +139,18 @@ async def on_message_send( self.worker_client.session_exists, context_id, ): - return self._build_connection_form_task( - task_id=message.task_id or None, - context_id=context_id, - history=None if had_task_id else [message], - ) + if self._connection_config.missing_fields: + return self._build_connection_form_task( + task_id=message.task_id or None, + context_id=context_id, + history=None if had_task_id else [message], + ) + if await self._open_session({}, context_id) is None: + return self._build_connection_failure_task( + task_id=message.task_id or None, + context_id=context_id, + history=None if had_task_id else [message], + ) return await self._forward_message(context_id, params) @@ -155,14 +173,14 @@ async def _handle_connection_action( action.get("context") or {}, message.context_id, ) - text = ( - _CONNECTION_SUCCESS_MESSAGE - if session_id is not None - else _CONNECTION_FAILURE_MESSAGE - ) + if session_id is None: + return self._build_connection_failure_task( + task_id=task.id, + context_id=task.context_id, + ) return self._complete_task( task, - [new_text_part(text)], + [new_text_part(_CONNECTION_SUCCESS_MESSAGE)], "database-session", ) @@ -248,7 +266,7 @@ async def _open_session( context_id: str, ) -> str | None: try: - session_info = SessionInfo.from_a2ui_event(action_context) + session_info = self._connection_config.resolve(action_context) session_id = await asyncio.to_thread( self.worker_client.open_session, context_id, @@ -331,15 +349,17 @@ async def on_get_extended_agent_card( ) -> AgentCardMessage: return self.agent_card - @staticmethod - def _session_expired_error(context_id: str) -> InvalidParamsError: + def _session_expired_error(self, context_id: str) -> InvalidParamsError: """Build the reconnect error returned by the task-list operation.""" return InvalidParamsError( "Database session expired. Reconnect using contextId.", data={ "reason": "SESSION_EXPIRED", "contextId": context_id, - "connectionForm": connection_form(), + "connectionForm": connection_form( + missing_fields=self._connection_config.missing_fields, + template=self._form_template, + ), }, ) @@ -375,6 +395,12 @@ def _build_connection_form_task( history: list[Message] | None = None, ) -> Task: """Build a completed, transient task containing the connection form.""" + if not self._connection_config.missing_fields: + return self._build_connection_failure_task( + task_id=task_id, + context_id=context_id, + history=history, + ) task = _new_bootstrap_task( task_id=task_id, context_id=context_id, @@ -382,14 +408,52 @@ def _build_connection_form_task( ) return self._complete_task( task, - [ - a2ui_part(item) - for item in connection_form(f"db-connect-{task.id}") - ], + self._connection_form_parts(task.id), "database-connection-form", [A2UI_EXTENSION_URI], ) + def _build_connection_failure_task( + self, + *, + task_id: str | None, + context_id: str, + history: list[Message] | None = None, + ) -> Task: + """Return a safe failure and a fresh form when input is still needed.""" + task = _new_bootstrap_task( + task_id=task_id, + context_id=context_id, + history=history, + ) + parts = [new_text_part(_CONNECTION_FAILURE_MESSAGE)] + parts.extend(self._connection_form_parts(task.id)) + extensions = [A2UI_EXTENSION_URI] if len(parts) > 1 else None + return self._complete_task( + task, + parts, + "database-connection-failure", + extensions, + ) + + def _connection_form_parts(self, task_id: str) -> list: + return [ + a2ui_part(item) + for item in connection_form( + surface_id=f"db-connect-{task_id}", + missing_fields=self._connection_config.missing_fields, + template=self._form_template, + ) + ] + + @property + def _connection_config(self) -> ConnectionConfig: + return getattr(self, "connection", ConnectionConfig()) + + @property + def _form_template(self) -> tuple[dict, ...] | None: + return getattr(self, "connection_form_template", None) + def _new_bootstrap_task( *, @@ -408,8 +472,14 @@ def _new_bootstrap_task( def create_gateway_app(settings: GatewaySettings) -> Starlette: """Build a Gemini Enterprise-compatible A2A v0.3 gateway application.""" + form_template = settings.connection_form_template + if form_template is not None: + form_template = validate_connection_form( + list(form_template), + settings.connection.missing_fields, + ) description = "Connects a user to a temporary Select AI database session." - endpoint = f"{settings.agent_url}/a2a/jsonrpc/" + endpoint = f"{settings.public_url}/a2a/jsonrpc/" card = AgentCard( name="Select AI Database Gateway", description=description, @@ -444,7 +514,12 @@ def create_gateway_app(settings: GatewaySettings) -> Starlette: ) ], ) - handler = GatewayRequestHandler(WorkerClient(settings), card) + handler = GatewayRequestHandler( + WorkerClient(settings), + card, + settings.connection, + form_template, + ) compat_card = to_compat_agent_card(card).model_dump( by_alias=True, exclude_none=True, diff --git a/src/select_ai/agent/a2a/models.py b/src/select_ai/agent/a2a/models.py index 2812f61..413c733 100644 --- a/src/select_ai/agent/a2a/models.py +++ b/src/select_ai/agent/a2a/models.py @@ -7,20 +7,99 @@ """Configuration and transient request models for the A2A runtime.""" -from dataclasses import dataclass +from dataclasses import dataclass, field + +CONNECTION_FIELDS = ("dsn", "username", "password", "team_name") + + +@dataclass(frozen=True) +class ConnectionConfig: + """Deployment-provided values for a database session.""" + + dsn: str | None = None + username: str | None = None + password: str | None = None + team_name: str | None = None + + def __post_init__(self) -> None: + for name in CONNECTION_FIELDS: + value = getattr(self, name) + if value is not None and (not isinstance(value, str) or not value): + raise ValueError(f"{name} must be a non-empty string") + + @property + def missing_fields(self) -> tuple[str, ...]: + """Return canonical properties that must come from the form.""" + return tuple( + name for name in CONNECTION_FIELDS if getattr(self, name) is None + ) + + def resolve(self, submitted: dict) -> "SessionInfo": + """Merge validated submitted values with immutable server values.""" + if not isinstance(submitted, dict): + raise ValueError("Connection form context must be an object.") + unknown = set(submitted) - set(CONNECTION_FIELDS) + if unknown: + raise ValueError("Connection form contains unsupported fields.") + configured = { + name + for name in CONNECTION_FIELDS + if getattr(self, name) is not None + } + if configured.intersection(submitted): + raise ValueError( + "Configured connection fields cannot be overridden." + ) + values = { + name: getattr(self, name, None) or submitted.get(name) + for name in CONNECTION_FIELDS + } + return SessionInfo.from_values(values) + + +@dataclass(frozen=True) +class WorkerSettings: + """Configuration for the internal session worker.""" + + consul_url: str + worker_id: str + worker_address: str + worker_port: int + session_ttl_seconds: int + session_start_timeout_seconds: int + worker_endpoint: str | None = None + + def __post_init__(self) -> None: + if self.worker_port < 1 or self.worker_port > 65_535: + raise ValueError("worker_port must be between 1 and 65535") + if self.session_ttl_seconds < 1: + raise ValueError("session_ttl_seconds must be at least 1") + if self.session_start_timeout_seconds < 1: + raise ValueError( + "session_start_timeout_seconds must be at least 1" + ) + object.__setattr__(self, "consul_url", self.consul_url.rstrip("/")) + if self.worker_endpoint: + object.__setattr__( + self, + "worker_endpoint", + self.worker_endpoint.rstrip("/"), + ) @dataclass(frozen=True) class GatewaySettings: """Configuration for the public gateway process.""" - agent_url: str + public_url: str consul_url: str worker_service: str session_ttl_seconds: int worker_tls_ca_file: str | None = None worker_tls_cert_file: str | None = None worker_tls_key_file: str | None = None + connection: ConnectionConfig = field(default_factory=ConnectionConfig) + connection_form_template: tuple[dict, ...] | None = None def __post_init__(self) -> None: if self.session_ttl_seconds < 1: @@ -35,7 +114,7 @@ def __post_init__(self) -> None: "worker mTLS requires a CA file, client certificate, and " "client key." ) - object.__setattr__(self, "agent_url", self.agent_url.rstrip("/")) + object.__setattr__(self, "public_url", self.public_url.rstrip("/")) object.__setattr__(self, "consul_url", self.consul_url.rstrip("/")) @property @@ -54,13 +133,14 @@ class SessionInfo: team_name: str @classmethod - def from_a2ui_event(cls, event: dict) -> "SessionInfo": - required = ("dsn", "username", "password", "team_name") + def from_values(cls, values: dict) -> "SessionInfo": + """Build a complete connection after resolution.""" if not all( - isinstance(event.get(key), str) and event[key] for key in required + isinstance(values.get(key), str) and values[key] + for key in CONNECTION_FIELDS ): raise ValueError("All database connection fields are required.") - return cls(**{key: event[key] for key in required}) + return cls(**{key: values[key] for key in CONNECTION_FIELDS}) @dataclass(frozen=True) diff --git a/src/select_ai/agent/a2a/session_process.py b/src/select_ai/agent/a2a/session_process.py new file mode 100644 index 0000000..bfc9507 --- /dev/null +++ b/src/select_ai/agent/a2a/session_process.py @@ -0,0 +1,447 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# https://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +"""Reusable child-process backend for isolated database sessions.""" + +from __future__ import annotations + +import asyncio +import contextlib +import logging +import multiprocessing +import time +from dataclasses import dataclass +from multiprocessing.connection import Connection +from typing import Protocol + +from select_ai.agent.a2a.session_runtime import SessionRuntime +from select_ai.agent.a2a.worker_protocol import ( + PipeMessageType, + ResultKind, + WorkerResult, +) + +LOGGER = logging.getLogger(__name__) +_SESSION_REAPER_INTERVAL_SECONDS = 1 + + +@dataclass(frozen=True) +class SessionSpec: + """Complete inputs needed to start one isolated session.""" + + session_id: str + dsn: str + username: str + password: str + team_name: str + + +@dataclass +class ChildSession: + """In-memory ownership record for one database-session process.""" + + process: multiprocessing.Process + connection: Connection + expires_at: float + lock: asyncio.Lock + + +class SessionBackend(Protocol): + """Internal contract shared by worker and embedded session backends.""" + + async def open(self, spec: SessionSpec) -> None: ... + + async def dispatch( + self, + session_id: str, + method: str, + payload: bytes, + ) -> WorkerResult: ... + + async def close(self, session_id: str) -> None: ... + + async def close_all(self) -> None: ... + + async def reap_expired(self) -> None: ... + + +class SessionBackendError(RuntimeError): + """Base error raised by an isolated session backend.""" + + +class SessionNotFound(SessionBackendError): + """The requested session does not exist or is no longer live.""" + + +class SessionUnavailable(SessionBackendError): + """The session process failed while handling a command.""" + + +class SessionStartTimeout(SessionBackendError): + """The session process did not become ready before its deadline.""" + + +class SessionLoginError(SessionBackendError): + """The database connection or team initialization failed.""" + + +class SessionCommandError(SessionBackendError): + """The database runtime rejected one A2A command.""" + + +class ProcessSessionBackend: + """Own one isolated Select AI runtime process for each session.""" + + def __init__( + self, + session_ttl_seconds: int, + session_start_timeout_seconds: int, + ) -> None: + self.session_ttl_seconds = session_ttl_seconds + self.session_start_timeout_seconds = session_start_timeout_seconds + self.sessions: dict[str, ChildSession] = {} + + async def open(self, spec: SessionSpec) -> None: + """Start a child runtime and wait until its database pool is ready.""" + parent_connection, child_connection = multiprocessing.Pipe() + credentials = { + "user": spec.username, + "password": spec.password, + "dsn": spec.dsn, + } + process = multiprocessing.Process( + target=_session_process_main, + args=( + child_connection, + credentials, + spec.session_id, + spec.team_name, + ), + daemon=True, + ) + process.start() + child_connection.close() + session = ChildSession( + process=process, + connection=parent_connection, + expires_at=time.monotonic() + self.session_ttl_seconds, + lock=asyncio.Lock(), + ) + try: + await self._wait_ready(session, spec) + except Exception: + await self._terminate(session) + raise + previous = self.sessions.pop(spec.session_id, None) + self.sessions[spec.session_id] = session + if previous: + await self._terminate(previous) + + async def get(self, session_id: str) -> ChildSession: + """Return a live session, closing it if its expiry has elapsed.""" + expired_session = None + session = self.sessions.get(session_id) + if session and ( + session.expires_at <= time.monotonic() + or not session.process.is_alive() + ): + self.sessions.pop(session_id, None) + expired_session = session + session = None + if expired_session: + await self._terminate(expired_session) + if session is None: + raise SessionNotFound( + "Database session expired; reconnect required." + ) + return session + + async def dispatch( + self, + session_id: str, + method: str, + payload: bytes, + ) -> WorkerResult: + """Run one A2A operation in the process owning this session.""" + session = await self.get(session_id) + response = None + failure = None + async with session.lock: + if session.process.is_alive(): + try: + await asyncio.to_thread( + session.connection.send, + { + "type": PipeMessageType.A2A.value, + "method": method, + "payload": payload, + }, + ) + response = await self._receive( + session, + timeout_seconds=120, + ) + except (EOFError, OSError, TimeoutError) as error: + failure = error + if failure is not None: + await self._discard(session_id, session) + raise SessionUnavailable( + "Database session is unavailable; reconnect required." + ) from failure + if response is None: + await self._discard(session_id, session) + raise SessionNotFound( + "Database session expired; reconnect required." + ) + if response.get("type") == PipeMessageType.RESULT.value: + return response.get( + "result", + WorkerResult(ResultKind.NONE), + ) + if response.get("type") == PipeMessageType.A2A_ERROR.value: + raise SessionCommandError( + response.get("detail", "A2A request failed.") + ) + LOGGER.error("Select AI session process reported a command failure.") + raise SessionUnavailable( + "Database session is unavailable; reconnect required." + ) + + async def close(self, session_id: str) -> None: + """Terminate a session explicitly.""" + session = self.sessions.pop(session_id, None) + if session is None: + raise SessionNotFound( + "Database session expired; reconnect required." + ) + await self._terminate(session) + + async def reap_expired(self) -> None: + """Continuously terminate expired or dead child sessions.""" + while True: + await asyncio.sleep(_SESSION_REAPER_INTERVAL_SECONDS) + await self._reap_expired_sessions() + + async def close_all(self) -> None: + """Terminate all child sessions during backend shutdown.""" + sessions = list(self.sessions.values()) + self.sessions.clear() + for session in sessions: + await self._terminate(session) + + async def _reap_expired_sessions(self) -> None: + now = time.monotonic() + expired = [ + (session_id, session) + for session_id, session in self.sessions.items() + if session.expires_at <= now or not session.process.is_alive() + ] + for session_id, _session in expired: + self.sessions.pop(session_id, None) + + for session_id, session in expired: + try: + await self._terminate(session) + except Exception: + LOGGER.exception( + "Failed to terminate expired session %s.", + session_id, + ) + + async def _wait_ready( + self, + session: ChildSession, + spec: SessionSpec, + ) -> None: + try: + response = await self._receive( + session, + timeout_seconds=self.session_start_timeout_seconds, + ) + except (EOFError, OSError, TimeoutError) as error: + raise SessionStartTimeout( + "Database session start timed out." + ) from error + if response.get("type") == PipeMessageType.READY.value: + return + detail = response.get("detail", "Database login failed.") + for secret in (spec.password, spec.username, spec.dsn): + detail = detail.replace(secret, "[REDACTED]") + LOGGER.error("Select AI session startup failed: %s", detail[-2_000:]) + raise SessionLoginError("Database login failed.") + + @staticmethod + async def _receive( + session: ChildSession, + timeout_seconds: float, + ) -> dict: + available = await asyncio.to_thread( + session.connection.poll, + timeout_seconds, + ) + if not available: + raise TimeoutError() + return await asyncio.to_thread(session.connection.recv) + + async def _discard(self, session_id: str, session: ChildSession) -> None: + if self.sessions.get(session_id) is session: + self.sessions.pop(session_id, None) + await self._terminate(session) + + @staticmethod + async def _terminate(session: ChildSession) -> None: + async with session.lock: + try: + with contextlib.suppress(OSError): + await asyncio.to_thread( + session.connection.send, + {"type": PipeMessageType.CLOSE.value}, + ) + await asyncio.to_thread(session.process.join, timeout=5) + for stop in ( + session.process.terminate, + session.process.kill, + ): + if not session.process.is_alive(): + break + await asyncio.to_thread(stop) + await asyncio.to_thread(session.process.join, timeout=5) + finally: + await asyncio.to_thread(session.connection.close) + + +def _session_process_main( + connection: Connection, + credentials: dict[str, str], + session_id: str, + team_name: str, +) -> None: + """Entrypoint for a child that owns one Select AI database session.""" + try: + asyncio.run( + _run_session_process( + connection, + credentials, + session_id, + team_name, + ) + ) + finally: + connection.close() + + +async def _run_session_process( + connection: Connection, + credentials: dict[str, str], + session_id: str, + team_name: str, +) -> None: + """Open one async connection and execute A2A operations.""" + import select_ai + + runtime: SessionRuntime | None = None + ready = False + try: + await select_ai.async_connect( + user=credentials["user"], + password=credentials["password"], + dsn=credentials["dsn"], + ) + if not await select_ai.async_is_connected(): + raise RuntimeError("Database login failed.") + runtime = SessionRuntime(session_id, team_name) + await runtime.initialize() + connection.send({"type": PipeMessageType.READY.value}) + ready = True + await _serve_session_commands(connection, runtime) + except Exception as error: + _report_session_process_error(connection, error, not ready) + finally: + await _close_session_process(runtime, select_ai) + + +async def _serve_session_commands( + connection: Connection, + runtime: SessionRuntime, +) -> None: + """Serve commands for one initialized database session.""" + while True: + command = await _receive_session_command(connection) + if ( + command is None + or command.get("type") == PipeMessageType.CLOSE.value + ): + return + if command.get("type") != PipeMessageType.A2A.value: + connection.send( + { + "type": PipeMessageType.ERROR.value, + "detail": "Invalid command.", + } + ) + continue + await _handle_a2a_command(connection, runtime, command) + + +async def _receive_session_command(connection: Connection) -> dict | None: + """Read one command without blocking the event loop.""" + try: + return await asyncio.to_thread(connection.recv) + except EOFError: + return None + + +async def _handle_a2a_command( + connection: Connection, + runtime: SessionRuntime, + command: dict, +) -> None: + """Execute one internal A2A command and send its result.""" + try: + result = await runtime.handle( + command["method"], + command.get("payload", b""), + ) + except Exception as error: + LOGGER.exception("Select AI session A2A command failed") + connection.send( + {"type": PipeMessageType.A2A_ERROR.value, "detail": str(error)} + ) + return + connection.send({"type": PipeMessageType.RESULT.value, "result": result}) + + +def _report_session_process_error( + connection: Connection, + error: Exception, + during_startup: bool, +) -> None: + """Report startup failures without sending errors after readiness.""" + message = ( + "Select AI session process startup failed" + if during_startup + else "Select AI session process failed" + ) + LOGGER.error(message) + if during_startup: + with contextlib.suppress(OSError): + connection.send( + {"type": PipeMessageType.ERROR.value, "detail": str(error)} + ) + + +async def _close_session_process( + runtime: SessionRuntime | None, + select_ai, +) -> None: + """Close the A2A handler and database connection owned by the child.""" + handler = getattr(runtime, "handler", None) + if handler is not None: + with contextlib.suppress(Exception): + await handler.aclose() + with contextlib.suppress(Exception): + await select_ai.async_disconnect() diff --git a/src/select_ai/agent/a2a/task_store.py b/src/select_ai/agent/a2a/task_store.py index 418c145..a98d660 100644 --- a/src/select_ai/agent/a2a/task_store.py +++ b/src/select_ai/agent/a2a/task_store.py @@ -27,16 +27,16 @@ _CREATE_TABLE = """ BEGIN EXECUTE IMMEDIATE ' - CREATE TABLE SELECT_AI_A2A_TASKS ( + CREATE TABLE DBMS_AI_A2A_TASKS$ ( owner VARCHAR2(512) NOT NULL, task_id VARCHAR2(255) NOT NULL, context_id VARCHAR2(255), task_json CLOB NOT NULL CHECK (task_json IS JSON), updated_at TIMESTAMP WITH TIME ZONE NOT NULL, - CONSTRAINT select_ai_a2a_tasks_pk PRIMARY KEY (owner, task_id) + CONSTRAINT dbms_ai_a2a_tasks_pk PRIMARY KEY (owner, task_id) )'; EXECUTE IMMEDIATE ' - COMMENT ON TABLE SELECT_AI_A2A_TASKS + COMMENT ON TABLE DBMS_AI_A2A_TASKS$ IS ''Managed by select_ai.a2a.task_store'''; EXCEPTION WHEN OTHERS THEN @@ -46,6 +46,13 @@ END; """ +_CREATE_VIEW = """ + CREATE OR REPLACE VIEW DBMS_AI_A2A_TASKS AS + SELECT owner, task_id, context_id, task_json, updated_at + FROM DBMS_AI_A2A_TASKS$ + WITH READ ONLY +""" + class OracleTaskStore(TaskStore): """Persist A2A tasks in Oracle Database using Select AI's connection pool.""" @@ -66,6 +73,7 @@ async def initialize(self) -> None: if self.initialized: return await self._execute(_CREATE_TABLE) + await self._execute(_CREATE_VIEW) self.initialized = True async def save(self, task: Task, context: ServerCallContext) -> None: @@ -73,7 +81,7 @@ async def save(self, task: Task, context: ServerCallContext) -> None: await self.initialize() await self._execute( """ - MERGE INTO SELECT_AI_A2A_TASKS target + MERGE INTO DBMS_AI_A2A_TASKS$ target USING ( SELECT :owner AS owner, :task_id AS task_id FROM dual ) source @@ -104,7 +112,7 @@ async def get( row = await self._fetchone( """ SELECT task_json - FROM SELECT_AI_A2A_TASKS + FROM DBMS_AI_A2A_TASKS$ WHERE owner = :owner AND task_id = :task_id """, owner=self._owner(context), @@ -124,7 +132,7 @@ async def list( rows = await self._fetchall( """ SELECT task_json - FROM SELECT_AI_A2A_TASKS + FROM DBMS_AI_A2A_TASKS$ WHERE owner = :owner AND ( :context_id IS NULL OR context_id = :context_id @@ -166,7 +174,7 @@ async def delete(self, task_id: str, context: ServerCallContext) -> None: await self.initialize() await self._execute( """ - DELETE FROM SELECT_AI_A2A_TASKS + DELETE FROM DBMS_AI_A2A_TASKS$ WHERE owner = :owner AND task_id = :task_id """, owner=self._owner(context), diff --git a/src/select_ai/agent/a2a/worker.py b/src/select_ai/agent/a2a/worker.py index 5043955..0028178 100644 --- a/src/select_ai/agent/a2a/worker.py +++ b/src/select_ai/agent/a2a/worker.py @@ -5,39 +5,37 @@ # https://oss.oracle.com/licenses/upl. # ----------------------------------------------------------------------------- -"""Internal worker that owns temporary Select AI child processes.""" +"""Internal HTTP and Consul wrapper for isolated session processes.""" from __future__ import annotations import asyncio import contextlib -import logging -import multiprocessing -import os -import socket import time from contextlib import asynccontextmanager -from dataclasses import dataclass -from multiprocessing.connection import Connection import httpx from fastapi import FastAPI, HTTPException, Request, status from pydantic import BaseModel, Field, SecretStr from starlette.responses import Response -from select_ai.agent.a2a.session_runtime import SessionRuntime +from select_ai.agent.a2a.models import WorkerSettings +from select_ai.agent.a2a.session_process import ( + ProcessSessionBackend, + SessionBackendError, + SessionCommandError, + SessionLoginError, + SessionNotFound, + SessionSpec, + SessionStartTimeout, + SessionUnavailable, +) from select_ai.agent.a2a.worker_protocol import ( PROTOBUF_CONTENT_TYPE, WORKER_A2A_METHOD_HEADER, WORKER_RESULT_KIND_HEADER, - PipeMessageType, - ResultKind, - WorkerResult, ) -LOGGER = logging.getLogger(__name__) -_SESSION_REAPER_INTERVAL_SECONDS = 1 - class OpenSessionRequest(BaseModel): """Sensitive request accepted only on the internal worker interface.""" @@ -48,382 +46,15 @@ class OpenSessionRequest(BaseModel): password: SecretStr = Field(min_length=1, max_length=1_024) team_name: str = Field(min_length=1, max_length=128) - -@dataclass -class ChildSession: - """In-memory ownership record for one database-session process.""" - - process: multiprocessing.Process - connection: Connection - expires_at: float - lock: asyncio.Lock - - -class SessionWorker: - """Own one isolated Select AI runtime process for each session.""" - - def __init__( - self, - session_ttl_seconds: int, - session_start_timeout_seconds: int, - ) -> None: - self.session_ttl_seconds = session_ttl_seconds - self.session_start_timeout_seconds = session_start_timeout_seconds - self.sessions: dict[str, ChildSession] = {} - - async def open(self, request: OpenSessionRequest) -> None: - """Start a child runtime and wait until its database pool is ready.""" - parent_connection, child_connection = multiprocessing.Pipe() - credentials = { - "user": request.username, - "password": request.password.get_secret_value(), - "dsn": request.dsn, - } - process = multiprocessing.Process( - target=_session_process_main, - args=( - child_connection, - credentials, - request.session_id, - request.team_name, - ), - daemon=True, - ) - process.start() - child_connection.close() - session = ChildSession( - process=process, - connection=parent_connection, - expires_at=time.monotonic() + self.session_ttl_seconds, - lock=asyncio.Lock(), - ) - try: - await self._wait_ready(session, request) - except Exception: - await self._terminate(session) - raise - previous = self.sessions.pop(request.session_id, None) - self.sessions[request.session_id] = session - if previous: - await self._terminate(previous) - - async def get(self, session_id: str) -> ChildSession: - """Return a live session, closing it if its expiry has elapsed.""" - expired_session = None - session = self.sessions.get(session_id) - if session and ( - session.expires_at <= time.monotonic() - or not session.process.is_alive() - ): - self.sessions.pop(session_id, None) - expired_session = session - session = None - if expired_session: - await self._terminate(expired_session) - if session is None: - raise HTTPException( - status_code=404, - detail="Database session expired; reconnect required.", - ) - return session - - async def dispatch( - self, - session_id: str, - method: str, - payload: bytes, - ) -> WorkerResult: - """Run one A2A operation in the process owning this session.""" - session = await self.get(session_id) - response = None - failure = None - async with session.lock: - if session.process.is_alive(): - try: - await asyncio.to_thread( - session.connection.send, - { - "type": PipeMessageType.A2A.value, - "method": method, - "payload": payload, - }, - ) - response = await self._receive( - session, - timeout_seconds=120, - ) - except (EOFError, OSError, TimeoutError) as error: - failure = error - if failure is not None: - await self._discard(session_id, session) - raise HTTPException( - status_code=502, - detail=( - "Database session is unavailable; reconnect required." - ), - ) from failure - if response is None: - await self._discard(session_id, session) - raise HTTPException( - status_code=404, - detail="Database session expired; reconnect required.", - ) - if response.get("type") == PipeMessageType.RESULT.value: - return response.get( - "result", - WorkerResult(ResultKind.NONE), - ) - if response.get("type") == PipeMessageType.A2A_ERROR.value: - raise HTTPException( - status_code=400, - detail=response.get("detail", "A2A request failed."), - ) - LOGGER.error("Select AI session process reported a command failure.") - raise HTTPException( - status_code=502, - detail="Database session is unavailable; reconnect required.", - ) - - async def close(self, session_id: str) -> None: - """Terminate a session on an explicit gateway DELETE request.""" - session = self.sessions.pop(session_id, None) - if session is None: - raise HTTPException( - status_code=404, - detail="Database session expired; reconnect required.", - ) - await self._terminate(session) - - async def reap_expired(self) -> None: - """Continuously terminate expired or dead child sessions.""" - while True: - await asyncio.sleep(_SESSION_REAPER_INTERVAL_SECONDS) - await self._reap_expired_sessions() - - async def close_all(self) -> None: - """Terminate all child sessions during worker shutdown.""" - sessions = list(self.sessions.values()) - self.sessions.clear() - for session in sessions: - await self._terminate(session) - - async def _reap_expired_sessions(self) -> None: - now = time.monotonic() - expired = [ - (session_id, session) - for session_id, session in self.sessions.items() - if session.expires_at <= now or not session.process.is_alive() - ] - for session_id, _session in expired: - self.sessions.pop(session_id, None) - - for session_id, session in expired: - try: - await self._terminate(session) - except Exception: - LOGGER.exception( - "Failed to terminate expired session %s.", - session_id, - ) - - async def _wait_ready( - self, - session: ChildSession, - request: OpenSessionRequest, - ) -> None: - try: - response = await self._receive( - session, - timeout_seconds=self.session_start_timeout_seconds, - ) - except (EOFError, OSError, TimeoutError) as error: - raise HTTPException( - status_code=504, - detail="Database session start timed out.", - ) from error - if response.get("type") == PipeMessageType.READY.value: - return - detail = response.get("detail", "Database login failed.") - for secret in ( - request.password.get_secret_value(), - request.username, - request.dsn, - ): - detail = detail.replace(secret, "[REDACTED]") - LOGGER.error("Select AI session startup failed: %s", detail[-2_000:]) - raise HTTPException(status_code=400, detail="Database login failed.") - - @staticmethod - async def _receive( - session: ChildSession, - timeout_seconds: float, - ) -> dict: - available = await asyncio.to_thread( - session.connection.poll, - timeout_seconds, - ) - if not available: - raise TimeoutError() - return await asyncio.to_thread(session.connection.recv) - - async def _discard(self, session_id: str, session: ChildSession) -> None: - if self.sessions.get(session_id) is session: - self.sessions.pop(session_id, None) - await self._terminate(session) - - @staticmethod - async def _terminate(session: ChildSession) -> None: - async with session.lock: - try: - with contextlib.suppress(OSError): - await asyncio.to_thread( - session.connection.send, - {"type": PipeMessageType.CLOSE.value}, - ) - await asyncio.to_thread(session.process.join, timeout=5) - for stop in ( - session.process.terminate, - session.process.kill, - ): - if not session.process.is_alive(): - break - await asyncio.to_thread(stop) - await asyncio.to_thread(session.process.join, timeout=5) - finally: - await asyncio.to_thread(session.connection.close) - - -def _session_process_main( - connection: Connection, - credentials: dict[str, str], - session_id: str, - team_name: str, -) -> None: - """Entrypoint for a child that owns one Select AI database session.""" - try: - asyncio.run( - _run_session_process( - connection, - credentials, - session_id, - team_name, - ) - ) - finally: - connection.close() - - -async def _run_session_process( - connection: Connection, - credentials: dict[str, str], - session_id: str, - team_name: str, -) -> None: - """Open one async connection and execute A2A operations.""" - import select_ai - - runtime: SessionRuntime | None = None - ready = False - try: - await select_ai.async_connect( - user=credentials["user"], - password=credentials["password"], - dsn=credentials["dsn"], - ) - if not await select_ai.async_is_connected(): - raise RuntimeError("Database login failed.") - runtime = SessionRuntime(session_id, team_name) - await runtime.initialize() - connection.send({"type": PipeMessageType.READY.value}) - ready = True - await _serve_session_commands(connection, runtime) - except Exception as error: - _report_session_process_error(connection, error, not ready) - finally: - await _close_session_process(runtime, select_ai) - - -async def _serve_session_commands( - connection: Connection, - runtime: SessionRuntime, -) -> None: - """Serve commands for one initialized database session.""" - while True: - command = await _receive_session_command(connection) - if ( - command is None - or command.get("type") == PipeMessageType.CLOSE.value - ): - return - if command.get("type") != PipeMessageType.A2A.value: - connection.send( - { - "type": PipeMessageType.ERROR.value, - "detail": "Invalid command.", - } - ) - continue - await _handle_a2a_command(connection, runtime, command) - - -async def _receive_session_command(connection: Connection) -> dict | None: - """Read one command without blocking the event loop.""" - try: - return await asyncio.to_thread(connection.recv) - except EOFError: - return None - - -async def _handle_a2a_command( - connection: Connection, - runtime: SessionRuntime, - command: dict, -) -> None: - """Execute one internal A2A command and send its result.""" - try: - result = await runtime.handle( - command["method"], - command.get("payload", b""), + def to_session_spec(self) -> SessionSpec: + """Convert validated HTTP input to the backend-neutral contract.""" + return SessionSpec( + session_id=self.session_id, + dsn=self.dsn, + username=self.username, + password=self.password.get_secret_value(), + team_name=self.team_name, ) - except Exception as error: - LOGGER.exception("Select AI session A2A command failed") - connection.send( - {"type": PipeMessageType.A2A_ERROR.value, "detail": str(error)} - ) - return - connection.send({"type": PipeMessageType.RESULT.value, "result": result}) - - -def _report_session_process_error( - connection: Connection, - error: Exception, - during_startup: bool, -) -> None: - """Report startup failures without sending errors after readiness.""" - message = ( - "Select AI session process startup failed" - if during_startup - else "Select AI session process failed" - ) - LOGGER.error(message) - if during_startup: - with contextlib.suppress(OSError): - connection.send( - {"type": PipeMessageType.ERROR.value, "detail": str(error)} - ) - - -async def _close_session_process( - runtime: SessionRuntime | None, - select_ai, -) -> None: - """Close the A2A handler and database connection owned by the child.""" - handler = getattr(runtime, "handler", None) - if handler is not None: - with contextlib.suppress(Exception): - await handler.aclose() - with contextlib.suppress(Exception): - await select_ai.async_disconnect() async def _register_with_consul( @@ -434,7 +65,7 @@ async def _register_with_consul( worker_endpoint: str | None = None, ) -> None: payload = { - "Name": "select-ai-worker", + "Name": "select-ai-a2a-worker", "ID": worker_id, "Address": worker_address, "Port": worker_port, @@ -477,31 +108,41 @@ async def _deregister_from_consul(consul_url: str, worker_id: str) -> None: ) -def create_worker_app( - session_ttl_seconds: int = 900, - session_start_timeout_seconds: int = 30, -) -> FastAPI: - """Build the internal session-worker HTTP application.""" - worker = SessionWorker(session_ttl_seconds, session_start_timeout_seconds) +def _http_error(error: SessionBackendError) -> HTTPException: + """Map backend-neutral failures to the existing worker HTTP contract.""" + if isinstance(error, SessionNotFound): + status_code = status.HTTP_404_NOT_FOUND + elif isinstance(error, SessionStartTimeout): + status_code = status.HTTP_504_GATEWAY_TIMEOUT + elif isinstance(error, (SessionLoginError, SessionCommandError)): + status_code = status.HTTP_400_BAD_REQUEST + elif isinstance(error, SessionUnavailable): + status_code = status.HTTP_502_BAD_GATEWAY + else: + status_code = status.HTTP_500_INTERNAL_SERVER_ERROR + return HTTPException(status_code=status_code, detail=str(error)) - consul_url = os.environ.get("CONSUL_HTTP_URL", "http://consul:8500") - consul_url = consul_url.rstrip("/") - worker_id = os.environ.get("WORKER_ID", socket.gethostname()) - worker_address = os.environ.get("WORKER_ADDRESS", socket.gethostname()) - worker_port = int(os.environ.get("WORKER_PORT", "8080")) - worker_endpoint = os.environ.get("WORKER_ENDPOINT") + +def create_worker_app(settings: WorkerSettings) -> FastAPI: + """Build the internal session-worker HTTP application.""" + backend = ProcessSessionBackend( + settings.session_ttl_seconds, + settings.session_start_timeout_seconds, + ) @asynccontextmanager async def lifespan(_app): await _register_with_consul( - consul_url, - worker_id, - worker_address, - worker_port, - worker_endpoint, + settings.consul_url, + settings.worker_id, + settings.worker_address, + settings.worker_port, + settings.worker_endpoint, + ) + heartbeat = asyncio.create_task( + _heartbeat(settings.consul_url, settings.worker_id) ) - heartbeat = asyncio.create_task(_heartbeat(consul_url, worker_id)) - reaper = asyncio.create_task(worker.reap_expired()) + reaper = asyncio.create_task(backend.reap_expired()) try: yield finally: @@ -511,8 +152,11 @@ async def lifespan(_app): heartbeat.cancel() with contextlib.suppress(asyncio.CancelledError): await heartbeat - await worker.close_all() - await _deregister_from_consul(consul_url, worker_id) + await backend.close_all() + await _deregister_from_consul( + settings.consul_url, + settings.worker_id, + ) app = FastAPI( title="Select AI Session Worker", @@ -527,7 +171,10 @@ async def health() -> dict[str, str]: @app.post("/sessions", status_code=status.HTTP_201_CREATED) async def open_session(request: OpenSessionRequest) -> dict[str, str]: - await worker.open(request) + try: + await backend.open(request.to_session_spec()) + except SessionBackendError as error: + raise _http_error(error) from error return {"status": "opened"} @app.post("/sessions/{session_id}/a2a") @@ -541,11 +188,14 @@ async def handle_a2a( status_code=400, detail=f"Missing {WORKER_A2A_METHOD_HEADER} header.", ) - result = await worker.dispatch( - session_id, - method, - await request.body(), - ) + try: + result = await backend.dispatch( + session_id, + method, + await request.body(), + ) + except SessionBackendError as error: + raise _http_error(error) from error return Response( content=result.payload, media_type=PROTOBUF_CONTENT_TYPE, @@ -559,6 +209,9 @@ async def handle_a2a( status_code=status.HTTP_204_NO_CONTENT, ) async def close_session(session_id: str) -> None: - await worker.close(session_id) + try: + await backend.close(session_id) + except SessionBackendError as error: + raise _http_error(error) from error return app diff --git a/src/select_ai/agent/a2a/worker_client.py b/src/select_ai/agent/a2a/worker_client.py index 3c38b82..abcd985 100644 --- a/src/select_ai/agent/a2a/worker_client.py +++ b/src/select_ai/agent/a2a/worker_client.py @@ -40,8 +40,8 @@ LOGGER = logging.getLogger(__name__) -_SESSION_PREFIX = "select-ai/sessions/" -_TASK_PREFIX = "select-ai/tasks/" +_SESSION_PREFIX = "select-ai-a2a/sessions/" +_TASK_PREFIX = "select-ai-a2a/tasks/" class ReconnectRequired(RuntimeError): diff --git a/src/select_ai/cli/a2a.py b/src/select_ai/cli/a2a.py index cab69e8..807decf 100644 --- a/src/select_ai/cli/a2a.py +++ b/src/select_ai/cli/a2a.py @@ -7,6 +7,8 @@ import getpass import json +import os +import socket import ssl import click @@ -29,11 +31,30 @@ def a2a(): @a2a.command() -@click.option("--team", "team_name", required=True, help="Database AI team.") +@click.option( + "--deployment", + type=click.Choice(("standalone", "clustered"), case_sensitive=False), + default="standalone", + show_default=True, + help="A2A runtime deployment topology.", +) +@click.option( + "--team", + "team_name", + envvar="SELECT_AI_A2A_TEAM", + help="Database AI team. Required for standalone deployment.", +) @click.option("--host", default="127.0.0.1", show_default=True) -@click.option("--port", default=8000, show_default=True, type=int) +@click.option( + "--port", + default=8000, + show_default=True, + envvar="PORT", + type=click.IntRange(min=1, max=65_535), +) @click.option( "--public-url", + envvar="PUBLIC_URL", help="Public base URL advertised in the A2A Agent Card.", ) @click.option("--description", help="A2A agent description.") @@ -44,48 +65,152 @@ def a2a(): type=click.IntRange(min=1), help="Maximum asynchronous Oracle connections.", ) +@click.option( + "--consul-url", + default="http://consul:8500", + show_default=True, + envvar="CONSUL_HTTP_URL", + help="Consul HTTP API URL for clustered deployment.", +) +@click.option( + "--worker-service", + default="select-ai-a2a-worker", + show_default=True, + envvar="WORKER_SERVICE", + help="Consul worker service for clustered deployment.", +) +@click.option( + "--session-ttl-seconds", + default=900, + show_default=True, + type=click.IntRange(min=1), + envvar="SESSION_TTL_SECONDS", +) +@click.option( + "--worker-tls-ca-file", + envvar="WORKER_TLS_CA_FILE", + type=click.Path(exists=True, dir_okay=False, readable=True), + help="CA bundle used to validate clustered workers.", +) +@click.option( + "--worker-tls-cert-file", + envvar="WORKER_TLS_CERT_FILE", + type=click.Path(exists=True, dir_okay=False, readable=True), + help="Client certificate presented to clustered workers.", +) +@click.option( + "--worker-tls-key-file", + envvar="WORKER_TLS_KEY_FILE", + type=click.Path(exists=True, dir_okay=False, readable=True), + help="Client private key presented to clustered workers.", +) +@click.option( + "--a2ui-form", + type=click.Path(exists=True, dir_okay=False, readable=True), + help="Custom A2UI connection-form JSON file.", +) @connection_options def serve( + deployment, team_name, host, port, public_url, description, pool_max_size, + consul_url, + worker_service, + session_ttl_seconds, + worker_tls_ca_file, + worker_tls_cert_file, + worker_tls_key_file, + a2ui_form, user, password, dsn, wallet_location, wallet_password, ): - """Start an A2A HTTP server for one database AI agent team.""" - if create_app is None or uvicorn is None: + """Start the public A2A server in standalone or clustered mode.""" + if uvicorn is None: raise click.ClickException( "A2A server support requires the optional 'cli' extra. " "Install it with: pip install 'select_ai[cli]'" ) - if password is None: - password = getpass.getpass("Database password: ") - if user is None or dsn is None: - raise click.ClickException( - "--user and --dsn (or their SELECT_AI_* environment variables) " - "are required" - ) if public_url is None: public_url = f"http://{host}:{port}" - app = create_app( - team_name=team_name, - public_url=public_url, - user=user, - password=password, - dsn=dsn, - wallet_location=wallet_location, - wallet_password=wallet_password, - description=description, - pool_max_size=pool_max_size, - ) + if deployment == "standalone": + if create_app is None: + raise click.ClickException( + "Standalone A2A support requires the optional 'cli' extra. " + "Install it with: pip install 'select_ai[cli]'" + ) + if team_name is None: + raise click.ClickException( + "--team or SELECT_AI_A2A_TEAM is required for standalone " + "deployment" + ) + if user is None or dsn is None: + raise click.ClickException( + "--user and --dsn (or their SELECT_AI_* environment " + "variables) are required for standalone deployment" + ) + if password is None: + password = getpass.getpass("Database password: ") + app = create_app( + team_name=team_name, + public_url=public_url, + user=user, + password=password, + dsn=dsn, + wallet_location=wallet_location, + wallet_password=wallet_password, + description=description, + pool_max_size=pool_max_size, + ) + else: + try: + from select_ai.agent.a2a import ( + ConnectionConfig, + GatewaySettings, + create_gateway_app, + ) + from select_ai.agent.a2a.forms import load_connection_form + except ImportError as error: + raise click.ClickException( + "Clustered A2A support requires the optional 'a2a' extra. " + "Install it with: pip install 'select_ai[a2a]'" + ) from error + + connection = ConnectionConfig( + dsn=dsn, + username=user, + password=password, + team_name=team_name, + ) + form_template = None + if a2ui_form: + try: + form_template = load_connection_form( + a2ui_form, + connection.missing_fields, + ) + except ValueError as error: + raise click.ClickException(str(error)) from error + settings = GatewaySettings( + public_url=public_url, + consul_url=consul_url, + worker_service=worker_service, + session_ttl_seconds=session_ttl_seconds, + worker_tls_ca_file=worker_tls_ca_file, + worker_tls_cert_file=worker_tls_cert_file, + worker_tls_key_file=worker_tls_key_file, + connection=connection, + connection_form_template=form_template, + ) + app = create_gateway_app(settings) click.echo( f"A2A Agent Card: {public_url.rstrip('/')}/.well-known/agent-card.json" @@ -95,7 +220,31 @@ def serve( @a2a.command("worker") @click.option("--host", default="0.0.0.0", show_default=True) -@click.option("--port", default=8080, show_default=True, type=int) +@click.option( + "--port", + default=8080, + show_default=True, + type=click.IntRange(min=1, max=65_535), +) +@click.option( + "--worker-id", + envvar="WORKER_ID", + default=socket.gethostname, + show_default="host name", + help="Unique worker ID registered with Consul.", +) +@click.option( + "--consul-url", + envvar="CONSUL_HTTP_URL", + default="http://consul:8500", + show_default=True, + help="Consul HTTP API URL.", +) +@click.option( + "--worker-endpoint", + envvar="WORKER_ENDPOINT", + help="Worker URL advertised through Consul, including scheme and port.", +) @click.option( "--session-ttl-seconds", default=900, @@ -126,6 +275,9 @@ def serve( def worker( host, port, + worker_id, + consul_url, + worker_endpoint, session_ttl_seconds, session_start_timeout_seconds, tls_cert_file, @@ -134,17 +286,23 @@ def worker( ): """Start the internal Select AI session worker.""" try: - from select_ai.agent.a2a import create_worker_app + from select_ai.agent.a2a import WorkerSettings, create_worker_app except ImportError as error: raise click.ClickException( "Worker support requires the optional 'a2a' extra. " "Install it with: pip install 'select_ai[a2a]'" ) from error - app = create_worker_app( + settings = WorkerSettings( + consul_url=consul_url, + worker_id=worker_id, + worker_address=os.environ.get("WORKER_ADDRESS", socket.gethostname()), + worker_port=port, session_ttl_seconds=session_ttl_seconds, session_start_timeout_seconds=session_start_timeout_seconds, + worker_endpoint=worker_endpoint, ) + app = create_worker_app(settings) tls_files = (tls_cert_file, tls_key_file, tls_ca_file) if any(tls_files) and not all(tls_files): raise click.ClickException( @@ -162,87 +320,6 @@ def worker( uvicorn.run(app, host=host, port=port, **uvicorn_options) -@a2a.command("gateway") -@click.option("--host", default="0.0.0.0", show_default=True) -@click.option("--port", default=8080, show_default=True, type=int) -@click.option( - "--agent-url", - required=True, - envvar="AGENT_URL", - help="Public base URL advertised in the gateway Agent Card.", -) -@click.option( - "--consul-url", - default="http://consul:8500", - show_default=True, - envvar="CONSUL_HTTP_URL", - help="Consul HTTP API URL.", -) -@click.option( - "--worker-service", - default="select-ai-worker", - show_default=True, - envvar="WORKER_SERVICE", - help="Consul service name for Select AI workers.", -) -@click.option( - "--session-ttl-seconds", - default=900, - show_default=True, - type=click.IntRange(min=1), - envvar="SESSION_TTL_SECONDS", -) -@click.option( - "--worker-tls-ca-file", - envvar="WORKER_TLS_CA_FILE", - type=click.Path(exists=True, dir_okay=False, readable=True), - help="CA bundle used to validate worker certificates.", -) -@click.option( - "--worker-tls-cert-file", - envvar="WORKER_TLS_CERT_FILE", - type=click.Path(exists=True, dir_okay=False, readable=True), - help="Gateway client certificate used for worker mTLS.", -) -@click.option( - "--worker-tls-key-file", - envvar="WORKER_TLS_KEY_FILE", - type=click.Path(exists=True, dir_okay=False, readable=True), - help="Gateway client private key used for worker mTLS.", -) -def gateway( - host, - port, - agent_url, - consul_url, - worker_service, - session_ttl_seconds, - worker_tls_ca_file, - worker_tls_cert_file, - worker_tls_key_file, -): - """Start the public A2A/A2UI database-session gateway.""" - try: - from select_ai.agent.a2a import GatewaySettings, create_gateway_app - except ImportError as error: - raise click.ClickException( - "Gateway support requires the optional 'a2a' extra. " - "Install it with: pip install 'select_ai[a2a]'" - ) from error - - settings = GatewaySettings( - agent_url=agent_url, - consul_url=consul_url, - worker_service=worker_service, - session_ttl_seconds=session_ttl_seconds, - worker_tls_ca_file=worker_tls_ca_file, - worker_tls_cert_file=worker_tls_cert_file, - worker_tls_key_file=worker_tls_key_file, - ) - app = create_gateway_app(settings) - uvicorn.run(app, host=host, port=port) - - @a2a.command("agent-card") @click.option("--team", "team_name", required=True, help="Database AI team.") @click.option( diff --git a/tests/a2a/test_agent_card.py b/tests/a2a/test_agent_card.py index edd594c..199f3e4 100644 --- a/tests/a2a/test_agent_card.py +++ b/tests/a2a/test_agent_card.py @@ -75,9 +75,9 @@ def test_discovery_route_serves_only_the_v03_agent_card(): def test_gateway_card_advertises_a2ui_input_and_output(): app = create_gateway_app( GatewaySettings( - agent_url="https://agent.example.com", + public_url="https://agent.example.com", consul_url="http://consul:8500", - worker_service="select-ai-worker", + worker_service="select-ai-a2a-worker", session_ttl_seconds=900, ) ) diff --git a/tests/a2a/test_worker_runtime.py b/tests/a2a/test_worker_runtime.py index 89671f6..cd1a7d5 100644 --- a/tests/a2a/test_worker_runtime.py +++ b/tests/a2a/test_worker_runtime.py @@ -33,7 +33,12 @@ TaskNotFoundError, UnsupportedOperationError, ) -from select_ai.agent.a2a import GatewaySettings, session_runtime, worker +from select_ai.agent.a2a import ( + GatewaySettings, + session_process, + session_runtime, + worker, +) from select_ai.agent.a2a.a2ui import ( A2UI_CATALOG_ID, A2UI_EXTENSION_URI, @@ -119,13 +124,17 @@ def process_factory(**kwargs): return process monkeypatch.setattr( - worker.multiprocessing, + session_process.multiprocessing, "Pipe", lambda: (parent, child), ) - monkeypatch.setattr(worker.multiprocessing, "Process", process_factory) - session_worker = worker.SessionWorker(60, 1) - request = worker.OpenSessionRequest( + monkeypatch.setattr( + session_process.multiprocessing, + "Process", + process_factory, + ) + session_worker = session_process.ProcessSessionBackend(60, 1) + request = session_process.SessionSpec( session_id="session-1", username="user", password="password", @@ -145,7 +154,7 @@ def process_factory(**kwargs): assert result == WorkerResult(ResultKind.NONE) assert child.closed - assert processes[0].target is worker._session_process_main + assert processes[0].target is session_process._session_process_main assert processes[0].daemon is True assert parent.sent == [ { @@ -167,13 +176,17 @@ def process_factory(**kwargs): return process monkeypatch.setattr( - worker.multiprocessing, + session_process.multiprocessing, "Pipe", lambda: (parent, child), ) - monkeypatch.setattr(worker.multiprocessing, "Process", process_factory) - session_worker = worker.SessionWorker(60, 1) - request = worker.OpenSessionRequest( + monkeypatch.setattr( + session_process.multiprocessing, + "Process", + process_factory, + ) + session_worker = session_process.ProcessSessionBackend(60, 1) + request = session_process.SessionSpec( session_id="session-1", username="user", password="password", @@ -183,7 +196,10 @@ def process_factory(**kwargs): asyncio.run(session_worker.open(request)) session_worker.sessions["session-1"].expires_at = 0 - with pytest.raises(worker.HTTPException, match="reconnect required"): + with pytest.raises( + session_process.SessionNotFound, + match="reconnect required", + ): asyncio.run(session_worker.get("session-1")) assert process is not None @@ -203,14 +219,22 @@ def process_factory(**kwargs): return process monkeypatch.setattr( - worker.multiprocessing, + session_process.multiprocessing, "Pipe", lambda: (parent, child), ) - monkeypatch.setattr(worker.multiprocessing, "Process", process_factory) - monkeypatch.setattr(worker, "_SESSION_REAPER_INTERVAL_SECONDS", 0.01) - session_worker = worker.SessionWorker(60, 1) - request = worker.OpenSessionRequest( + monkeypatch.setattr( + session_process.multiprocessing, + "Process", + process_factory, + ) + monkeypatch.setattr( + session_process, + "_SESSION_REAPER_INTERVAL_SECONDS", + 0.01, + ) + session_worker = session_process.ProcessSessionBackend(60, 1) + request = session_process.SessionSpec( session_id="session-1", username="user", password="password", @@ -281,10 +305,10 @@ async def disconnect(): monkeypatch.setattr(select_ai, "async_connect", async_connect) monkeypatch.setattr(select_ai, "async_is_connected", connected) monkeypatch.setattr(select_ai, "async_disconnect", disconnect) - monkeypatch.setattr(worker, "SessionRuntime", Runtime) + monkeypatch.setattr(session_process, "SessionRuntime", Runtime) asyncio.run( - worker._run_session_process( + session_process._run_session_process( connection, { "user": "user", @@ -486,9 +510,9 @@ def post(*args, **kwargs): def test_worker_client_uses_consul_https_endpoint_with_mtls(monkeypatch): settings = GatewaySettings( - agent_url="https://gateway.example.com", + public_url="https://gateway.example.com", consul_url="http://consul:8500", - worker_service="select-ai-worker", + worker_service="select-ai-a2a-worker", session_ttl_seconds=60, worker_tls_ca_file="/tls/ca.pem", worker_tls_cert_file="/tls/gateway.pem", @@ -527,9 +551,9 @@ def json(): def test_mtls_requires_all_three_gateway_files(): with pytest.raises(ValueError, match="worker mTLS requires"): GatewaySettings( - agent_url="https://gateway.example.com", + public_url="https://gateway.example.com", consul_url="http://consul:8500", - worker_service="select-ai-worker", + worker_service="select-ai-a2a-worker", session_ttl_seconds=60, worker_tls_ca_file="/tls/ca.pem", ) @@ -569,6 +593,7 @@ async def put(self, _url, json): ) assert registered["Meta"] == {"endpoint": "https://worker-0.internal"} + assert registered["Name"] == "select-ai-a2a-worker" def test_a2ui_operation_uses_a_metadata_marked_data_part(): From 7d0d4ef8e72eee2db60b9ba06804f6e7c612c7c8 Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Tue, 15 Sep 2026 15:48:44 -0700 Subject: [PATCH 27/31] Scope A2A sessions to authenticated owners --- src/select_ai/agent/a2a/auth.py | 141 +++++++++++++++++++++ src/select_ai/agent/a2a/gateway.py | 59 +++++++-- src/select_ai/agent/a2a/models.py | 2 + src/select_ai/agent/a2a/server.py | 13 +- src/select_ai/agent/a2a/session_process.py | 7 +- src/select_ai/agent/a2a/session_runtime.py | 11 +- src/select_ai/agent/a2a/worker.py | 2 + src/select_ai/agent/a2a/worker_client.py | 139 ++++++++++++-------- src/select_ai/cli/a2a.py | 24 ++++ tests/a2a/test_worker_runtime.py | 69 ++++++---- 10 files changed, 372 insertions(+), 95 deletions(-) create mode 100644 src/select_ai/agent/a2a/auth.py diff --git a/src/select_ai/agent/a2a/auth.py b/src/select_ai/agent/a2a/auth.py new file mode 100644 index 0000000..2c97363 --- /dev/null +++ b/src/select_ai/agent/a2a/auth.py @@ -0,0 +1,141 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# https://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +"""Authentication integration for public A2A Starlette applications.""" + +from __future__ import annotations + +import base64 +import binascii +import hashlib +import json + +from a2a.types import ( + AgentCard, + HTTPAuthSecurityScheme, + SecurityScheme, + StringList, +) +from starlette.authentication import ( + AuthCredentials, + AuthenticationBackend, + AuthenticationError, + SimpleUser, +) +from starlette.middleware import Middleware +from starlette.middleware.authentication import AuthenticationMiddleware +from starlette.responses import JSONResponse + + +class TrustedBearerAuthenticationBackend(AuthenticationBackend): + """Map an upstream-validated bearer JWT's ``sub`` to a Starlette user. + + Signature and claims validation belongs to the deployment's authenticating + proxy (for example Gemini Enterprise plus Cloud Run IAM). This backend only + establishes the SDK user object from the already trusted token. + """ + + async def authenticate(self, connection): + header = connection.headers.get("authorization") + if header is None: + return None + scheme, separator, token = header.partition(" ") + if separator != " " or scheme.lower() != "bearer" or not token: + raise AuthenticationError("Authorization must use Bearer syntax.") + owner = _jwt_owner(token) + return AuthCredentials(["authenticated"]), SimpleUser(owner) + + +class RequireA2AAuthenticationMiddleware: + """Require an SDK-visible user on the JSON-RPC endpoint.""" + + def __init__(self, app, allow_unauthenticated: bool = False) -> None: + self.app = app + self.allow_unauthenticated = allow_unauthenticated + + async def __call__(self, scope, receive, send) -> None: + if ( + scope["type"] == "http" + and scope.get("path", "").rstrip("/") == "/a2a/jsonrpc" + and not self.allow_unauthenticated + and not scope["user"].is_authenticated + ): + response = JSONResponse( + {"detail": "Bearer authentication is required."}, + status_code=401, + headers={"WWW-Authenticate": "Bearer"}, + ) + await response(scope, receive, send) + return + await self.app(scope, receive, send) + + +def authentication_middleware( + allow_unauthenticated: bool, +) -> list[Middleware]: + """Return middleware ordered so authentication precedes enforcement.""" + return [ + Middleware( + AuthenticationMiddleware, + backend=TrustedBearerAuthenticationBackend(), + on_error=_authentication_error, + ), + Middleware( + RequireA2AAuthenticationMiddleware, + allow_unauthenticated=allow_unauthenticated, + ), + ] + + +def add_bearer_security(agent_card: AgentCard) -> None: + """Advertise the bearer contract on an authenticated Agent Card.""" + agent_card.security_schemes["bearer"].CopyFrom( + SecurityScheme( + http_auth_security_scheme=HTTPAuthSecurityScheme( + description="Upstream-validated user identity token.", + scheme="bearer", + bearer_format="JWT", + ) + ) + ) + requirement = agent_card.security_requirements.add() + requirement.schemes["bearer"].CopyFrom(StringList()) + + +def _authentication_error(_connection, error): + return JSONResponse( + {"detail": str(error)}, + status_code=401, + headers={"WWW-Authenticate": "Bearer"}, + ) + + +def _jwt_owner(token: str) -> str: + """Derive a stable opaque owner from an upstream-validated JWT.""" + parts = token.split(".") + if len(parts) != 3: + raise AuthenticationError("Bearer token must be a JWT.") + try: + encoded = parts[1] + "=" * (-len(parts[1]) % 4) + payload = json.loads(base64.urlsafe_b64decode(encoded)) + except ( + binascii.Error, + UnicodeDecodeError, + ValueError, + TypeError, + ) as error: + raise AuthenticationError( + "Bearer token has an invalid payload." + ) from error + subject = payload.get("sub") if isinstance(payload, dict) else None + issuer = payload.get("iss") if isinstance(payload, dict) else None + if not isinstance(subject, str) or not subject: + raise AuthenticationError("Bearer token has no subject.") + if not isinstance(issuer, str) or not issuer: + raise AuthenticationError("Bearer token has no issuer.") + digest = hashlib.sha256(f"{issuer}\0{subject}".encode("utf-8")).hexdigest() + return f"jwt:{digest}" diff --git a/src/select_ai/agent/a2a/gateway.py b/src/select_ai/agent/a2a/gateway.py index da37ca6..41ae48f 100644 --- a/src/select_ai/agent/a2a/gateway.py +++ b/src/select_ai/agent/a2a/gateway.py @@ -19,6 +19,7 @@ new_text_part, ) from a2a.server.context import ServerCallContext +from a2a.server.owner_resolver import resolve_user_scope from a2a.server.request_handlers import RequestHandler from a2a.server.routes import create_jsonrpc_routes from a2a.types import ( @@ -58,6 +59,10 @@ a2ui_part, find_action, ) +from select_ai.agent.a2a.auth import ( + add_bearer_security, + authentication_middleware, +) from select_ai.agent.a2a.forms import ( connection_form, validate_connection_form, @@ -103,11 +108,13 @@ def __init__( agent_card: AgentCard, connection: ConnectionConfig | None = None, connection_form_template: tuple[dict, ...] | None = None, + allow_unauthenticated: bool = False, ): self.worker_client = worker_client self.agent_card = agent_card self.connection = connection or ConnectionConfig() self.connection_form_template = connection_form_template + self.allow_unauthenticated = allow_unauthenticated # RequestHandler requires every operation even when the Agent Card does # not advertise streaming or push notifications. @@ -121,14 +128,15 @@ def __init__( async def on_message_send( self, params: SendMessageRequest, - _context: ServerCallContext, + context: ServerCallContext, ) -> Task | Message: """Handle connection bootstrap or forward the request to a worker.""" message = params.message + owner = self._owner(context) if action := find_action(message, _CONNECTION_ACTION_NAME): - return await self._handle_connection_action(message, action) + return await self._handle_connection_action(message, action, owner) - response = await self._recover_task_context(message) + response = await self._recover_task_context(message, owner) if response is not None: return response @@ -137,6 +145,7 @@ async def on_message_send( if not await asyncio.to_thread( self.worker_client.session_exists, + owner, context_id, ): if self._connection_config.missing_fields: @@ -145,19 +154,20 @@ async def on_message_send( context_id=context_id, history=None if had_task_id else [message], ) - if await self._open_session({}, context_id) is None: + if await self._open_session({}, owner, context_id) is None: return self._build_connection_failure_task( task_id=message.task_id or None, context_id=context_id, history=None if had_task_id else [message], ) - return await self._forward_message(context_id, params) + return await self._forward_message(owner, context_id, params) async def _handle_connection_action( self, message: Message, action: dict, + owner: str, ) -> Task: """Open a database session from the submitted A2UI form.""" if not message.context_id: @@ -171,6 +181,7 @@ async def _handle_connection_action( ) session_id = await self._open_session( action.get("context") or {}, + owner, message.context_id, ) if session_id is None: @@ -184,7 +195,11 @@ async def _handle_connection_action( "database-session", ) - async def _recover_task_context(self, message: Message) -> Task | None: + async def _recover_task_context( + self, + message: Message, + owner: str, + ) -> Task | None: """Recover a context when a client sends only a previous task ID.""" if message.context_id or not message.task_id: return None @@ -192,6 +207,7 @@ async def _recover_task_context(self, message: Message) -> Task | None: try: existing_task = await asyncio.to_thread( self.worker_client.get_task, + owner, GetTaskRequest(id=message.task_id), ) except ReconnectRequired as error: @@ -224,6 +240,7 @@ def _ensure_context_id(message: Message) -> str: async def _forward_message( self, + owner: str, context_id: str, params: SendMessageRequest, ) -> Task | Message: @@ -232,6 +249,7 @@ async def _forward_message( try: result = await asyncio.to_thread( self.worker_client.send_message, + owner, context_id, params, ) @@ -247,6 +265,7 @@ async def _forward_message( params.message.ClearField("task_id") result = await asyncio.to_thread( self.worker_client.send_message, + owner, context_id, params, ) @@ -263,12 +282,14 @@ async def _forward_message( async def _open_session( self, action_context: dict, + owner: str, context_id: str, ) -> str | None: try: session_info = self._connection_config.resolve(action_context) session_id = await asyncio.to_thread( self.worker_client.open_session, + owner, context_id, session_info, ) @@ -279,11 +300,12 @@ async def _open_session( async def on_get_task( self, params: GetTaskRequest, - _context: ServerCallContext, + context: ServerCallContext, ) -> Task | None: try: task = await asyncio.to_thread( self.worker_client.get_task, + self._owner(context), params, ) except ReconnectRequired as error: @@ -303,7 +325,7 @@ async def on_get_task( async def on_list_tasks( self, params: ListTasksRequest, - _context: ServerCallContext, + context: ServerCallContext, ) -> ListTasksResponse: if not params.context_id: raise InvalidParamsError( @@ -312,6 +334,7 @@ async def on_list_tasks( try: return await asyncio.to_thread( self.worker_client.list_tasks, + self._owner(context), params.context_id, params, ) @@ -321,11 +344,12 @@ async def on_list_tasks( async def on_cancel_task( self, params: CancelTaskRequest, - _context: ServerCallContext, + context: ServerCallContext, ) -> Task | None: try: task = await asyncio.to_thread( self.worker_client.cancel_task, + self._owner(context), params.id, ) except ReconnectRequired as error: @@ -454,6 +478,15 @@ def _connection_config(self) -> ConnectionConfig: def _form_template(self) -> tuple[dict, ...] | None: return getattr(self, "connection_form_template", None) + def _owner(self, context: ServerCallContext) -> str: + """Resolve the SDK owner, allowing one explicit local-only scope.""" + owner = resolve_user_scope(context) + if owner: + return owner + if getattr(self, "allow_unauthenticated", True): + return "local-development" + raise PermissionError("Bearer authentication is required.") + def _new_bootstrap_task( *, @@ -514,11 +547,14 @@ def create_gateway_app(settings: GatewaySettings) -> Starlette: ) ], ) + if not settings.allow_unauthenticated: + add_bearer_security(card) handler = GatewayRequestHandler( WorkerClient(settings), card, settings.connection, form_template, + settings.allow_unauthenticated, ) compat_card = to_compat_agent_card(card).model_dump( by_alias=True, @@ -538,4 +574,7 @@ async def get_agent_card(_request): enable_v0_3_compat=True, ) ) - return Starlette(routes=routes) + return Starlette( + routes=routes, + middleware=authentication_middleware(settings.allow_unauthenticated), + ) diff --git a/src/select_ai/agent/a2a/models.py b/src/select_ai/agent/a2a/models.py index 413c733..aac2570 100644 --- a/src/select_ai/agent/a2a/models.py +++ b/src/select_ai/agent/a2a/models.py @@ -100,6 +100,7 @@ class GatewaySettings: worker_tls_key_file: str | None = None connection: ConnectionConfig = field(default_factory=ConnectionConfig) connection_form_template: tuple[dict, ...] | None = None + allow_unauthenticated: bool = False def __post_init__(self) -> None: if self.session_ttl_seconds < 1: @@ -149,3 +150,4 @@ class SessionRoute: endpoint: str expires_at: float + session_id: str diff --git a/src/select_ai/agent/a2a/server.py b/src/select_ai/agent/a2a/server.py index d2c06fa..ebf2bc1 100644 --- a/src/select_ai/agent/a2a/server.py +++ b/src/select_ai/agent/a2a/server.py @@ -24,6 +24,10 @@ import select_ai from select_ai.agent import AsyncTeam from select_ai.agent.a2a.a2ui import A2UI_MIME_TYPE, a2ui_extension +from select_ai.agent.a2a.auth import ( + add_bearer_security, + authentication_middleware, +) from select_ai.agent.a2a.context_store import OracleContextStore from select_ai.agent.a2a.results import add_team_result from select_ai.agent.a2a.task_store import OracleTaskStore @@ -83,12 +87,15 @@ def create_app( # noqa: PLR0913 wallet_password: Optional[str] = None, description: Optional[str] = None, pool_max_size: int = 10, + allow_unauthenticated: bool = False, ) -> Starlette: """Build an A2A JSON-RPC application for one database AI Agent Team.""" if pool_max_size < 1: raise ValueError("pool_max_size must be at least 1") agent_card = _build_agent_card(team_name, public_url, description) + if not allow_unauthenticated: + add_bearer_security(agent_card) compat_agent_card = _build_v03_agent_card(agent_card) task_store = OracleTaskStore() context_store = OracleContextStore() @@ -138,7 +145,11 @@ async def get_agent_card(request): enable_v0_3_compat=True, ) ) - return Starlette(routes=routes, lifespan=lifespan) + return Starlette( + routes=routes, + lifespan=lifespan, + middleware=authentication_middleware(allow_unauthenticated), + ) def _build_agent_card( diff --git a/src/select_ai/agent/a2a/session_process.py b/src/select_ai/agent/a2a/session_process.py index bfc9507..2f70b83 100644 --- a/src/select_ai/agent/a2a/session_process.py +++ b/src/select_ai/agent/a2a/session_process.py @@ -34,6 +34,7 @@ class SessionSpec: """Complete inputs needed to start one isolated session.""" session_id: str + owner: str dsn: str username: str password: str @@ -119,6 +120,7 @@ async def open(self, spec: SessionSpec) -> None: child_connection, credentials, spec.session_id, + spec.owner, spec.team_name, ), daemon=True, @@ -318,6 +320,7 @@ def _session_process_main( connection: Connection, credentials: dict[str, str], session_id: str, + owner: str, team_name: str, ) -> None: """Entrypoint for a child that owns one Select AI database session.""" @@ -327,6 +330,7 @@ def _session_process_main( connection, credentials, session_id, + owner, team_name, ) ) @@ -338,6 +342,7 @@ async def _run_session_process( connection: Connection, credentials: dict[str, str], session_id: str, + owner: str, team_name: str, ) -> None: """Open one async connection and execute A2A operations.""" @@ -353,7 +358,7 @@ async def _run_session_process( ) if not await select_ai.async_is_connected(): raise RuntimeError("Database login failed.") - runtime = SessionRuntime(session_id, team_name) + runtime = SessionRuntime(session_id, owner, team_name) await runtime.initialize() connection.send({"type": PipeMessageType.READY.value}) ready = True diff --git a/src/select_ai/agent/a2a/session_runtime.py b/src/select_ai/agent/a2a/session_runtime.py index f516557..086b049 100644 --- a/src/select_ai/agent/a2a/session_runtime.py +++ b/src/select_ai/agent/a2a/session_runtime.py @@ -43,8 +43,8 @@ class SessionUser(User): """Internal A2A user used to scope one worker session's database rows.""" - def __init__(self, session_id: str) -> None: - self.session_id = session_id + def __init__(self, owner: str) -> None: + self.owner = owner @property def is_authenticated(self) -> bool: @@ -52,14 +52,15 @@ def is_authenticated(self) -> bool: @property def user_name(self) -> str: - return self.session_id + return self.owner class SessionRuntime: """Own the A2A handler and Oracle stores for one connected database.""" - def __init__(self, session_id: str, team_name: str) -> None: + def __init__(self, session_id: str, owner: str, team_name: str) -> None: self.session_id = session_id + self.owner = owner self.team_name = team_name self.task_store = OracleTaskStore() self.context_store = OracleContextStore() @@ -96,7 +97,7 @@ async def handle( raise RuntimeError("A2A session runtime is not initialized.") operation = A2AMethod(method) - context = ServerCallContext(user=SessionUser(self.session_id)) + context = ServerCallContext(user=SessionUser(self.owner)) if operation == A2AMethod.DELETE_TASK: request = parse_request(GetTaskRequest, payload) await self.task_store.delete(request.id, context) diff --git a/src/select_ai/agent/a2a/worker.py b/src/select_ai/agent/a2a/worker.py index 0028178..cb5b972 100644 --- a/src/select_ai/agent/a2a/worker.py +++ b/src/select_ai/agent/a2a/worker.py @@ -41,6 +41,7 @@ class OpenSessionRequest(BaseModel): """Sensitive request accepted only on the internal worker interface.""" session_id: str + owner: str = Field(min_length=1, max_length=512) dsn: str = Field(min_length=1, max_length=4_000) username: str = Field(min_length=1, max_length=128) password: SecretStr = Field(min_length=1, max_length=1_024) @@ -50,6 +51,7 @@ def to_session_spec(self) -> SessionSpec: """Convert validated HTTP input to the backend-neutral contract.""" return SessionSpec( session_id=self.session_id, + owner=self.owner, dsn=self.dsn, username=self.username, password=self.password.get_secret_value(), diff --git a/src/select_ai/agent/a2a/worker_client.py b/src/select_ai/agent/a2a/worker_client.py index abcd985..71e6df5 100644 --- a/src/select_ai/agent/a2a/worker_client.py +++ b/src/select_ai/agent/a2a/worker_client.py @@ -10,11 +10,13 @@ from __future__ import annotations import base64 +import hashlib import json import logging import time from threading import Lock from urllib.parse import quote +from uuid import uuid4 import requests from a2a.types.a2a_pb2 import ( @@ -69,12 +71,22 @@ def __init__(self, settings: GatewaySettings): ), } - def open_session(self, session_id: str, session_info: SessionInfo) -> str: + def open_session( + self, + owner: str, + context_id: str, + session_info: SessionInfo, + ) -> str: """Open a session and save a non-secret route in Consul.""" + session_id = str(uuid4()) endpoint = self._select_worker() response = requests.post( f"{endpoint}/sessions", - json={"session_id": session_id, **session_info.__dict__}, + json={ + "session_id": session_id, + "owner": owner, + **session_info.__dict__, + }, timeout=45, **self._worker_request_kwargs, ) @@ -82,34 +94,37 @@ def open_session(self, session_id: str, session_info: SessionInfo) -> str: route = SessionRoute( endpoint=endpoint, expires_at=time.time() + self.settings.session_ttl_seconds, + session_id=session_id, ) - if not self._save_route(session_id, route): + if not self._save_route(owner, context_id, route): self._close_worker_session(route, session_id) raise RuntimeError("Could not create the database session.") return session_id def send_message( self, - session_id: str, + owner: str, + context_id: str, request: SendMessageRequest, ) -> Task | Message | None: """Forward one A2A message to the worker session.""" result = self._dispatch( - session_id, + owner, + context_id, A2AMethod.SEND_MESSAGE, request, ) value = decode_result(result) if isinstance(value, Task): - self._save_task_route(value.id, session_id) + self._save_task_route(owner, value.id, context_id) return value - def get_task(self, params: GetTaskRequest) -> Task | None: + def get_task(self, owner: str, params: GetTaskRequest) -> Task | None: """Retrieve a task from its owning worker session.""" - session_id = self.task_session_id(params.id) - if session_id is None: + context_id = self.task_context_id(owner, params.id) + if context_id is None: return None - result = self._dispatch(session_id, A2AMethod.GET_TASK, params) + result = self._dispatch(owner, context_id, A2AMethod.GET_TASK, params) value = decode_result(result) if isinstance(value, Task): return value @@ -117,56 +132,60 @@ def get_task(self, params: GetTaskRequest) -> Task | None: def list_tasks( self, - session_id: str, + owner: str, + context_id: str, params: ListTasksRequest, ) -> ListTasksResponse: """Retrieve one session's database-paginated task response.""" result = self._dispatch( - session_id, + owner, + context_id, A2AMethod.LIST_TASKS, params, ) return decode_result(result) or ListTasksResponse() - def cancel_task(self, task_id: str) -> Task | None: + def cancel_task(self, owner: str, task_id: str) -> Task | None: """Cancel a task in the worker that owns it.""" - session_id = self.task_session_id(task_id) - if session_id is None: + context_id = self.task_context_id(owner, task_id) + if context_id is None: return None result = self._dispatch( - session_id, + owner, + context_id, A2AMethod.CANCEL_TASK, CancelTaskRequest(id=task_id), ) value = decode_result(result) return value if isinstance(value, Task) else None - def delete_task(self, task_id: str) -> None: + def delete_task(self, owner: str, task_id: str) -> None: """Delete a task and its routing metadata.""" - session_id = self.task_session_id(task_id) - if session_id: + context_id = self.task_context_id(owner, task_id) + if context_id: try: self._dispatch( - session_id, + owner, + context_id, A2AMethod.DELETE_TASK, GetTaskRequest(id=task_id), ) except ReconnectRequired: pass - self._delete_task_route(task_id) + self._delete_task_route(owner, task_id) - def session_exists(self, session_id: str) -> bool: + def session_exists(self, owner: str, context_id: str) -> bool: """Return whether Consul still has a live route for a session.""" try: - self._route_for(session_id) + self._route_for(owner, context_id) except ReconnectRequired: return False return True - def task_session_id(self, task_id: str) -> str | None: - """Return the worker session recorded for a task, if any.""" + def task_context_id(self, owner: str, task_id: str) -> str | None: + """Return the owner-scoped context recorded for a task, if any.""" response = requests.get( - f"{self.settings.consul_url}/v1/kv/{_TASK_PREFIX}" + f"{self.settings.consul_url}/v1/kv/{_TASK_PREFIX}{_owner_key(owner)}/" f"{quote(task_id, safe='')}", timeout=10, ) @@ -180,14 +199,15 @@ def task_session_id(self, task_id: str) -> str | None: def _dispatch( self, - session_id: str, + owner: str, + context_id: str, method: A2AMethod | str, message, ) -> WorkerResult: """Send one protobuf-serialized A2A operation to a worker session.""" - route = self._route_for(session_id) + route = self._route_for(owner, context_id) response = requests.post( - f"{route.endpoint}/sessions/{quote(session_id, safe='')}/a2a", + f"{route.endpoint}/sessions/{quote(route.session_id, safe='')}/a2a", data=message.SerializeToString(), headers={ "content-type": PROTOBUF_CONTENT_TYPE, @@ -197,10 +217,10 @@ def _dispatch( **getattr(self, "_worker_request_kwargs", {}), ) if response.status_code in (404, 502): - self._close_worker_session(route, session_id) + self._close_worker_session(route, context_id, owner) raise ReconnectRequired( "Database session ended; reconnect required.", - session_id, + context_id, ) response.raise_for_status() return WorkerResult( @@ -213,13 +233,19 @@ def _dispatch( payload=response.content, ) - def _save_task_route(self, task_id: str, session_id: str) -> None: + def _save_task_route( + self, + owner: str, + task_id: str, + context_id: str, + ) -> None: """Save only task-to-session routing metadata in Consul.""" try: response = requests.put( f"{self.settings.consul_url}/v1/kv/{_TASK_PREFIX}" + f"{_owner_key(owner)}/" f"{quote(task_id, safe='')}", - data=session_id, + data=context_id, timeout=10, ) response.raise_for_status() @@ -228,21 +254,22 @@ def _save_task_route(self, task_id: str, session_id: str) -> None: # are required because workers may use different databases. LOGGER.warning("Could not save route for task %s", task_id) - def _delete_task_route(self, task_id: str) -> None: + def _delete_task_route(self, owner: str, task_id: str) -> None: requests.delete( f"{self.settings.consul_url}/v1/kv/{_TASK_PREFIX}" + f"{_owner_key(owner)}/" f"{quote(task_id, safe='')}", timeout=10, ) - def close_session(self, session_id: str) -> None: + def close_session(self, owner: str, context_id: str) -> None: """Close the child process and remove the Consul route.""" try: - route = self._route_for(session_id) + route = self._route_for(owner, context_id) except ReconnectRequired: - self._delete_route(session_id) + self._delete_route(owner, context_id) return - self._close_worker_session(route, session_id) + self._close_worker_session(route, context_id, owner) def _select_worker(self) -> str: response = requests.get( @@ -279,52 +306,58 @@ def _select_worker(self) -> str: address = service.get("Address") or worker["Node"]["Address"] return f"http://{address}:{service['Port']}" - def _route_for(self, session_id: str) -> SessionRoute: + def _route_for(self, owner: str, context_id: str) -> SessionRoute: response = requests.get( f"{self.settings.consul_url}/v1/kv/{_SESSION_PREFIX}" - f"{quote(session_id, safe='')}", + f"{_owner_key(owner)}/{quote(context_id, safe='')}", timeout=10, ) if response.status_code == 404: raise ReconnectRequired( "Database session expired; reconnect required.", - session_id, + context_id, ) response.raise_for_status() value = response.json()[0]["Value"] route = SessionRoute(**json.loads(base64.b64decode(value).decode())) if route.expires_at <= time.time(): - self._close_worker_session(route, session_id) + self._close_worker_session(route, context_id, owner) raise ReconnectRequired( "Database session expired; reconnect required.", - session_id, + context_id, ) return route - def _save_route(self, session_id: str, route: SessionRoute) -> bool: + def _save_route( + self, + owner: str, + context_id: str, + route: SessionRoute, + ) -> bool: response = requests.put( f"{self.settings.consul_url}/v1/kv/{_SESSION_PREFIX}" - f"{quote(session_id, safe='')}?cas=0", + f"{_owner_key(owner)}/{quote(context_id, safe='')}?cas=0", data=json.dumps(route.__dict__), timeout=10, ) return response.ok and response.text.strip().lower() == "true" - def _delete_route(self, session_id: str) -> None: + def _delete_route(self, owner: str, context_id: str) -> None: requests.delete( f"{self.settings.consul_url}/v1/kv/{_SESSION_PREFIX}" - f"{quote(session_id, safe='')}", + f"{_owner_key(owner)}/{quote(context_id, safe='')}", timeout=10, ) def _close_worker_session( self, route: SessionRoute, - session_id: str, + context_id: str, + owner: str | None = None, ) -> None: try: response = requests.delete( - f"{route.endpoint}/sessions/{quote(session_id, safe='')}", + f"{route.endpoint}/sessions/{quote(route.session_id, safe='')}", timeout=10, **getattr(self, "_worker_request_kwargs", {}), ) @@ -333,4 +366,10 @@ def _close_worker_session( except requests.RequestException: pass finally: - self._delete_route(session_id) + if owner is not None: + self._delete_route(owner, context_id) + + +def _owner_key(owner: str) -> str: + """Return a stable non-PII namespace for one authenticated owner.""" + return hashlib.sha256(owner.encode("utf-8")).hexdigest() diff --git a/src/select_ai/cli/a2a.py b/src/select_ai/cli/a2a.py index 807decf..2ab0ccc 100644 --- a/src/select_ai/cli/a2a.py +++ b/src/select_ai/cli/a2a.py @@ -6,6 +6,7 @@ # ----------------------------------------------------------------------------- import getpass +import ipaddress import json import os import socket @@ -109,6 +110,11 @@ def a2a(): type=click.Path(exists=True, dir_okay=False, readable=True), help="Custom A2UI connection-form JSON file.", ) +@click.option( + "--allow-unauthenticated", + is_flag=True, + help="Allow unauthenticated requests for local development only.", +) @connection_options def serve( deployment, @@ -125,6 +131,7 @@ def serve( worker_tls_cert_file, worker_tls_key_file, a2ui_form, + allow_unauthenticated, user, password, dsn, @@ -140,6 +147,11 @@ def serve( if public_url is None: public_url = f"http://{host}:{port}" + if allow_unauthenticated and not _is_loopback_host(host): + raise click.ClickException( + "--allow-unauthenticated requires a loopback --host " + "(127.0.0.1, ::1, or localhost)" + ) if deployment == "standalone": if create_app is None: @@ -169,6 +181,7 @@ def serve( wallet_password=wallet_password, description=description, pool_max_size=pool_max_size, + allow_unauthenticated=allow_unauthenticated, ) else: try: @@ -209,6 +222,7 @@ def serve( worker_tls_key_file=worker_tls_key_file, connection=connection, connection_form_template=form_template, + allow_unauthenticated=allow_unauthenticated, ) app = create_gateway_app(settings) @@ -218,6 +232,16 @@ def serve( uvicorn.run(app, host=host, port=port) +def _is_loopback_host(host: str) -> bool: + """Return whether a bind host is safe for unauthenticated development.""" + if host.lower() == "localhost": + return True + try: + return ipaddress.ip_address(host).is_loopback + except ValueError: + return False + + @a2a.command("worker") @click.option("--host", default="0.0.0.0", show_default=True) @click.option( diff --git a/tests/a2a/test_worker_runtime.py b/tests/a2a/test_worker_runtime.py index cd1a7d5..0eab777 100644 --- a/tests/a2a/test_worker_runtime.py +++ b/tests/a2a/test_worker_runtime.py @@ -136,6 +136,7 @@ def process_factory(**kwargs): session_worker = session_process.ProcessSessionBackend(60, 1) request = session_process.SessionSpec( session_id="session-1", + owner="owner-1", username="user", password="password", dsn="database", @@ -188,6 +189,7 @@ def process_factory(**kwargs): session_worker = session_process.ProcessSessionBackend(60, 1) request = session_process.SessionSpec( session_id="session-1", + owner="owner-1", username="user", password="password", dsn="database", @@ -236,6 +238,7 @@ def process_factory(**kwargs): session_worker = session_process.ProcessSessionBackend(60, 1) request = session_process.SessionSpec( session_id="session-1", + owner="owner-1", username="user", password="password", dsn="database", @@ -281,8 +284,9 @@ def test_session_runtime_uses_one_async_connection_and_dispatches_a2a( connection_arguments = {} class Runtime: - def __init__(self, session_id, team_name): + def __init__(self, session_id, owner, team_name): assert session_id == "session-1" + assert owner == "owner-1" assert team_name == "TEAM" async def initialize(self): @@ -316,6 +320,7 @@ async def disconnect(): "dsn": "database", }, "session-1", + "owner-1", "TEAM", ) ) @@ -364,7 +369,7 @@ def __init__(self, **kwargs): lambda *args: "agent-card", ) - runtime = session_runtime.SessionRuntime("session-1", "TEAM") + runtime = session_runtime.SessionRuntime("session-1", "owner-1", "TEAM") asyncio.run(runtime.initialize()) assert initialized == [ @@ -395,7 +400,9 @@ async def initialize(self): monkeypatch.setattr(session_runtime, "OracleTaskStore", Store) monkeypatch.setattr(session_runtime, "OracleContextStore", Store) - runtime = session_runtime.SessionRuntime("session-1", "MISSPELLED_TEAM") + runtime = session_runtime.SessionRuntime( + "session-1", "owner-1", "MISSPELLED_TEAM" + ) with pytest.raises(RuntimeError, match="team does not exist"): asyncio.run(runtime.initialize()) @@ -408,7 +415,7 @@ class Handler: async def on_get_task(self, _request, _context): raise TaskNotFoundError - runtime = session_runtime.SessionRuntime("session-1", "TEAM") + runtime = session_runtime.SessionRuntime("session-1", "owner-1", "TEAM") runtime.handler = Handler() result = asyncio.run( @@ -486,7 +493,11 @@ def post(*args, **kwargs): monkeypatch.setattr( client, "_route_for", - lambda session_id: type("Route", (), {"endpoint": "http://worker"})(), + lambda owner, context_id: type( + "Route", + (), + {"endpoint": "http://worker", "session_id": "session-1"}, + )(), ) request = SendMessageRequest( @@ -496,7 +507,7 @@ def post(*args, **kwargs): parts=[new_text_part("hello")], ) ) - result = client.send_message("context-1", request) + result = client.send_message("owner-1", "context-1", request) assert result is not None assert result.message_id == "m1" @@ -687,7 +698,7 @@ def test_gateway_returns_connection_error_when_worker_rejects_opening(): class Client: @staticmethod - def open_session(_context_id, _session_info): + def open_session(_owner, _context_id, _session_info): raise requests.HTTPError("worker rejected the connection") handler.worker_client = Client() @@ -700,6 +711,7 @@ def open_session(_context_id, _session_info): "password": "password", "team_name": "TEAM", }, + "owner-1", "context-1", ) ) @@ -713,14 +725,15 @@ def __init__(self): self.opened = False self.saved = None - def session_exists(self, _session_id): + def session_exists(self, _owner, _context_id): return self.opened - def open_session(self, session_id, session_info): - assert session_id == "context-1" + def open_session(self, owner, context_id, session_info): + assert owner == "local-development" + assert context_id == "context-1" assert session_info.team_name == "TEAM" self.opened = True - return session_id + return "session-1" client = Client() handler = GatewayRequestHandler.__new__(GatewayRequestHandler) @@ -781,11 +794,11 @@ class Client: calls = 0 @staticmethod - def session_exists(_session_id): + def session_exists(_owner, _context_id): return True - def send_message(self, session_id, request): - assert session_id == "context-1" + def send_message(self, _owner, context_id, request): + assert context_id == "context-1" self.calls += 1 if self.calls == 1: assert request.message.task_id == ( @@ -795,7 +808,7 @@ def send_message(self, session_id, request): assert not request.message.task_id return Task( id="worker-task", - context_id=session_id, + context_id=context_id, status={"state": TaskState.TASK_STATE_COMPLETED}, ) @@ -822,11 +835,11 @@ def send_message(self, session_id, request): def test_gateway_preserves_missing_worker_task_error(): class Client: @staticmethod - def session_exists(_session_id): + def session_exists(_owner, _context_id): return True @staticmethod - def send_message(_session_id, _request): + def send_message(_owner, _context_id, _request): raise TaskNotFoundError handler = GatewayRequestHandler.__new__(GatewayRequestHandler) @@ -848,8 +861,8 @@ def send_message(_session_id, _request): def test_gateway_bootstrap_without_context_returns_transient_form_task(): class Client: @staticmethod - def session_exists(session_id): - assert session_id + def session_exists(_owner, context_id): + assert context_id return False handler = GatewayRequestHandler.__new__(GatewayRequestHandler) @@ -881,7 +894,7 @@ def session_exists(session_id): def test_gateway_returns_task_form_when_task_session_expires_before_send(): class Client: @staticmethod - def get_task(_request): + def get_task(_owner, _request): raise ReconnectRequired( "Database session ended; reconnect required.", "context-1", @@ -911,7 +924,7 @@ def get_task(_request): def test_gateway_returns_task_form_when_task_route_is_missing_before_send(): class Client: @staticmethod - def get_task(_request): + def get_task(_owner, _request): return None handler = GatewayRequestHandler.__new__(GatewayRequestHandler) @@ -938,11 +951,11 @@ def get_task(_request): def test_gateway_uses_task_form_for_expired_context_only_message(): class Client: @staticmethod - def session_exists(_session_id): + def session_exists(_owner, _context_id): return True @staticmethod - def send_message(_session_id, _request): + def send_message(_owner, _context_id, _request): raise ReconnectRequired( "Database session ended; reconnect required.", "context-1", @@ -972,7 +985,7 @@ def send_message(_session_id, _request): def test_gateway_uses_connection_form_when_get_task_session_expires(): class Client: @staticmethod - def get_task(_request): + def get_task(_owner, _request): raise ReconnectRequired( "Database session ended; reconnect required.", "context-1", @@ -998,7 +1011,7 @@ def get_task(_request): def test_gateway_uses_connection_form_when_get_task_route_is_missing(): class Client: @staticmethod - def get_task(_request): + def get_task(_owner, _request): return None handler = GatewayRequestHandler.__new__(GatewayRequestHandler) @@ -1021,7 +1034,7 @@ def get_task(_request): def test_gateway_uses_connection_form_when_cancel_task_session_expires(): class Client: @staticmethod - def cancel_task(_task_id): + def cancel_task(_owner, _task_id): raise ReconnectRequired( "Database session ended; reconnect required.", "context-1", @@ -1047,7 +1060,7 @@ def cancel_task(_task_id): def test_gateway_uses_connection_form_when_cancel_task_route_is_missing(): class Client: @staticmethod - def cancel_task(_task_id): + def cancel_task(_owner, _task_id): return None handler = GatewayRequestHandler.__new__(GatewayRequestHandler) @@ -1070,7 +1083,7 @@ def cancel_task(_task_id): def test_gateway_exposes_connection_form_data_when_list_session_expires(): class Client: @staticmethod - def list_tasks(_context_id, _request): + def list_tasks(_owner, _context_id, _request): raise ReconnectRequired( "Database session ended; reconnect required.", "context-1", From a91306d602aec40bdbcb61623a1538d8efc6fb87 Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Tue, 15 Sep 2026 16:03:11 -0700 Subject: [PATCH 28/31] Add dynamic standalone A2A sessions --- src/select_ai/agent/a2a/__init__.py | 12 ++ src/select_ai/agent/a2a/embedded.py | 293 ++++++++++++++++++++++++++++ src/select_ai/agent/a2a/gateway.py | 42 +++- src/select_ai/agent/a2a/models.py | 23 +++ src/select_ai/cli/a2a.py | 89 +++++---- 5 files changed, 408 insertions(+), 51 deletions(-) create mode 100644 src/select_ai/agent/a2a/embedded.py diff --git a/src/select_ai/agent/a2a/__init__.py b/src/select_ai/agent/a2a/__init__.py index cdc9347..0517008 100644 --- a/src/select_ai/agent/a2a/__init__.py +++ b/src/select_ai/agent/a2a/__init__.py @@ -12,6 +12,7 @@ GatewaySettings, SessionInfo, SessionRoute, + StandaloneSessionSettings, WorkerSettings, ) @@ -30,12 +31,23 @@ def create_worker_app(settings: WorkerSettings): return factory(settings) +def create_embedded_session_app(settings: StandaloneSessionSettings): + """Build a dynamic standalone A2A/A2UI application.""" + from select_ai.agent.a2a.embedded import ( + create_embedded_session_app as factory, + ) + + return factory(settings) + + __all__ = [ "GatewaySettings", "ConnectionConfig", "SessionInfo", "SessionRoute", + "StandaloneSessionSettings", "WorkerSettings", "create_gateway_app", + "create_embedded_session_app", "create_worker_app", ] diff --git a/src/select_ai/agent/a2a/embedded.py b/src/select_ai/agent/a2a/embedded.py new file mode 100644 index 0000000..f8918a2 --- /dev/null +++ b/src/select_ai/agent/a2a/embedded.py @@ -0,0 +1,293 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# https://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +"""Embedded session routing for dynamic standalone A2A deployment.""" + +from __future__ import annotations + +import asyncio +from concurrent.futures import Future +from contextlib import asynccontextmanager +from threading import Event, Lock, Thread +from uuid import uuid4 + +from a2a.types.a2a_pb2 import ( + CancelTaskRequest, + GetTaskRequest, + ListTasksRequest, + ListTasksResponse, + Message, + SendMessageRequest, + Task, +) + +from select_ai.agent.a2a.forms import validate_connection_form +from select_ai.agent.a2a.gateway import create_session_app +from select_ai.agent.a2a.models import ( + SessionInfo, + StandaloneSessionSettings, +) +from select_ai.agent.a2a.session_process import ( + ProcessSessionBackend, + SessionNotFound, + SessionSpec, + SessionUnavailable, +) +from select_ai.agent.a2a.worker_client import ReconnectRequired +from select_ai.agent.a2a.worker_protocol import ( + A2AMethod, + decode_result, +) + + +class EmbeddedSessionClient: + """Expose the clustered client contract over an in-process router.""" + + def __init__(self, settings: StandaloneSessionSettings) -> None: + self.backend = ProcessSessionBackend( + settings.session_ttl_seconds, + settings.session_start_timeout_seconds, + ) + self._bindings: dict[tuple[str, str], str] = {} + self._tasks: dict[tuple[str, str], str] = {} + self._routing_lock = Lock() + self._loop: asyncio.AbstractEventLoop | None = None + self._thread: Thread | None = None + self._reaper: Future | None = None + + def start(self) -> None: + """Start the private event loop that owns the async backend.""" + if self._loop is not None: + return + ready = Event() + + def run_loop() -> None: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + self._loop = loop + ready.set() + loop.run_forever() + loop.close() + + self._thread = Thread( + target=run_loop, + name="select-ai-a2a-sessions", + daemon=True, + ) + self._thread.start() + ready.wait() + self._reaper = asyncio.run_coroutine_threadsafe( + self.backend.reap_expired(), + self._require_loop(), + ) + + def shutdown(self) -> None: + """Close every child process and stop the private event loop.""" + if self._loop is None: + return + if self._reaper is not None: + self._reaper.cancel() + self._run(self.backend.close_all()) + loop = self._require_loop() + loop.call_soon_threadsafe(loop.stop) + if self._thread is not None: + self._thread.join(timeout=10) + self._loop = None + self._thread = None + self._reaper = None + with self._routing_lock: + self._bindings.clear() + self._tasks.clear() + + def open_session( + self, + owner: str, + context_id: str, + session_info: SessionInfo, + ) -> str: + """Create one isolated child and atomically bind it to a context.""" + session_id = str(uuid4()) + self._run( + self.backend.open( + SessionSpec( + session_id=session_id, + owner=owner, + dsn=session_info.dsn, + username=session_info.username, + password=session_info.password, + team_name=session_info.team_name, + ) + ) + ) + key = (owner, context_id) + with self._routing_lock: + created = key not in self._bindings + if created: + self._bindings[key] = session_id + if not created: + self._run(self.backend.close(session_id)) + raise RuntimeError("A database session already exists.") + return session_id + + def session_exists(self, owner: str, context_id: str) -> bool: + """Return whether the owner/context binding still has a live child.""" + session_id = self._session_id(owner, context_id) + if session_id is None: + return False + try: + self._run(self.backend.get(session_id)) + except SessionNotFound: + self._drop_binding(owner, context_id, session_id) + return False + return True + + def send_message( + self, + owner: str, + context_id: str, + request: SendMessageRequest, + ) -> Task | Message | None: + result = self._dispatch( + owner, + context_id, + A2AMethod.SEND_MESSAGE, + request, + ) + value = decode_result(result) + if isinstance(value, Task): + with self._routing_lock: + self._tasks[(owner, value.id)] = context_id + return value + + def get_task(self, owner: str, request: GetTaskRequest) -> Task | None: + context_id = self._task_context(owner, request.id) + if context_id is None: + return None + value = decode_result( + self._dispatch(owner, context_id, A2AMethod.GET_TASK, request) + ) + return value if isinstance(value, Task) else None + + def list_tasks( + self, + owner: str, + context_id: str, + request: ListTasksRequest, + ) -> ListTasksResponse: + return ( + decode_result( + self._dispatch( + owner, context_id, A2AMethod.LIST_TASKS, request + ) + ) + or ListTasksResponse() + ) + + def cancel_task(self, owner: str, task_id: str) -> Task | None: + context_id = self._task_context(owner, task_id) + if context_id is None: + return None + value = decode_result( + self._dispatch( + owner, + context_id, + A2AMethod.CANCEL_TASK, + CancelTaskRequest(id=task_id), + ) + ) + return value if isinstance(value, Task) else None + + def close_session(self, owner: str, context_id: str) -> None: + """Close and forget one owner/context binding.""" + with self._routing_lock: + session_id = self._bindings.pop((owner, context_id), None) + if session_id is None: + return + try: + self._run(self.backend.close(session_id)) + except SessionNotFound: + pass + + def _dispatch(self, owner, context_id, method, request): + session_id = self._session_id(owner, context_id) + if session_id is None: + raise ReconnectRequired( + "Database session expired; reconnect required.", + context_id, + ) + try: + return self._run( + self.backend.dispatch( + session_id, + method.value, + request.SerializeToString(), + ) + ) + except (SessionNotFound, SessionUnavailable) as error: + self._drop_binding(owner, context_id, session_id) + raise ReconnectRequired(str(error), context_id) from error + + def _session_id(self, owner: str, context_id: str) -> str | None: + with self._routing_lock: + return self._bindings.get((owner, context_id)) + + def _task_context(self, owner: str, task_id: str) -> str | None: + with self._routing_lock: + return self._tasks.get((owner, task_id)) + + def _drop_binding( + self, + owner: str, + context_id: str, + session_id: str, + ) -> None: + with self._routing_lock: + key = (owner, context_id) + if self._bindings.get(key) == session_id: + self._bindings.pop(key, None) + + def _run(self, coroutine): + return asyncio.run_coroutine_threadsafe( + coroutine, + self._require_loop(), + ).result() + + def _require_loop(self) -> asyncio.AbstractEventLoop: + if self._loop is None: + raise RuntimeError("Embedded session manager is not running.") + return self._loop + + +def create_embedded_session_app( + settings: StandaloneSessionSettings, +): + """Build a dynamic standalone app backed by local child processes.""" + form_template = settings.connection_form_template + if form_template is not None: + form_template = validate_connection_form( + list(form_template), + settings.connection.missing_fields, + ) + client = EmbeddedSessionClient(settings) + + @asynccontextmanager + async def lifespan(_app): + client.start() + try: + yield + finally: + await asyncio.to_thread(client.shutdown) + + return create_session_app( + public_url=settings.public_url, + session_client=client, + connection=settings.connection, + connection_form_template=form_template, + allow_unauthenticated=settings.allow_unauthenticated, + description=settings.description, + lifespan=lifespan, + ) diff --git a/src/select_ai/agent/a2a/gateway.py b/src/select_ai/agent/a2a/gateway.py index 41ae48f..33ecb7a 100644 --- a/src/select_ai/agent/a2a/gateway.py +++ b/src/select_ai/agent/a2a/gateway.py @@ -511,10 +511,33 @@ def create_gateway_app(settings: GatewaySettings) -> Starlette: list(form_template), settings.connection.missing_fields, ) - description = "Connects a user to a temporary Select AI database session." - endpoint = f"{settings.public_url}/a2a/jsonrpc/" + return create_session_app( + public_url=settings.public_url, + session_client=WorkerClient(settings), + connection=settings.connection, + connection_form_template=form_template, + allow_unauthenticated=settings.allow_unauthenticated, + description=settings.description, + ) + + +def create_session_app( + *, + public_url: str, + session_client, + connection: ConnectionConfig, + connection_form_template: tuple[dict, ...] | None, + allow_unauthenticated: bool, + description: str | None = None, + lifespan=None, +) -> Starlette: + """Build the common dynamic-session A2A application.""" + description = description or ( + "Connects a user to a temporary Select AI database session." + ) + endpoint = f"{public_url.rstrip('/')}/a2a/jsonrpc/" card = AgentCard( - name="Select AI Database Gateway", + name="Select AI Database Agent", description=description, version=__version__, default_input_modes=["text/plain", A2UI_MIME_TYPE], @@ -547,14 +570,14 @@ def create_gateway_app(settings: GatewaySettings) -> Starlette: ) ], ) - if not settings.allow_unauthenticated: + if not allow_unauthenticated: add_bearer_security(card) handler = GatewayRequestHandler( - WorkerClient(settings), + session_client, card, - settings.connection, - form_template, - settings.allow_unauthenticated, + connection, + connection_form_template, + allow_unauthenticated, ) compat_card = to_compat_agent_card(card).model_dump( by_alias=True, @@ -576,5 +599,6 @@ async def get_agent_card(_request): ) return Starlette( routes=routes, - middleware=authentication_middleware(settings.allow_unauthenticated), + middleware=authentication_middleware(allow_unauthenticated), + lifespan=lifespan, ) diff --git a/src/select_ai/agent/a2a/models.py b/src/select_ai/agent/a2a/models.py index aac2570..8a3ca36 100644 --- a/src/select_ai/agent/a2a/models.py +++ b/src/select_ai/agent/a2a/models.py @@ -98,6 +98,7 @@ class GatewaySettings: worker_tls_ca_file: str | None = None worker_tls_cert_file: str | None = None worker_tls_key_file: str | None = None + description: str | None = None connection: ConnectionConfig = field(default_factory=ConnectionConfig) connection_form_template: tuple[dict, ...] | None = None allow_unauthenticated: bool = False @@ -124,6 +125,28 @@ def worker_mtls_enabled(self) -> bool: return self.worker_tls_ca_file is not None +@dataclass(frozen=True) +class StandaloneSessionSettings: + """Configuration for dynamic sessions embedded in one server process.""" + + public_url: str + session_ttl_seconds: int + session_start_timeout_seconds: int = 30 + description: str | None = None + connection: ConnectionConfig = field(default_factory=ConnectionConfig) + connection_form_template: tuple[dict, ...] | None = None + allow_unauthenticated: bool = False + + def __post_init__(self) -> None: + if self.session_ttl_seconds < 1: + raise ValueError("session_ttl_seconds must be at least 1") + if self.session_start_timeout_seconds < 1: + raise ValueError( + "session_start_timeout_seconds must be at least 1" + ) + object.__setattr__(self, "public_url", self.public_url.rstrip("/")) + + @dataclass(frozen=True) class SessionInfo: """Credentials used only while opening one in-memory worker session.""" diff --git a/src/select_ai/cli/a2a.py b/src/select_ai/cli/a2a.py index 2ab0ccc..84bd964 100644 --- a/src/select_ai/cli/a2a.py +++ b/src/select_ai/cli/a2a.py @@ -5,7 +5,6 @@ # http://oss.oracle.com/licenses/upl. # ----------------------------------------------------------------------------- -import getpass import ipaddress import json import os @@ -43,7 +42,7 @@ def a2a(): "--team", "team_name", envvar="SELECT_AI_A2A_TEAM", - help="Database AI team. Required for standalone deployment.", + help="Database AI team; otherwise collected by the connection form.", ) @click.option("--host", default="127.0.0.1", show_default=True) @click.option( @@ -153,24 +152,43 @@ def serve( "(127.0.0.1, ::1, or localhost)" ) - if deployment == "standalone": + try: + from select_ai.agent.a2a import ( + ConnectionConfig, + GatewaySettings, + StandaloneSessionSettings, + create_embedded_session_app, + create_gateway_app, + ) + from select_ai.agent.a2a.forms import load_connection_form + except ImportError as error: + raise click.ClickException( + "A2A support requires the optional 'a2a' extra. " + "Install it with: pip install 'select_ai[a2a]'" + ) from error + + connection = ConnectionConfig( + dsn=dsn, + username=user, + password=password, + team_name=team_name, + ) + form_template = None + if a2ui_form: + try: + form_template = load_connection_form( + a2ui_form, + connection.missing_fields, + ) + except ValueError as error: + raise click.ClickException(str(error)) from error + + if deployment == "standalone" and not connection.missing_fields: if create_app is None: raise click.ClickException( "Standalone A2A support requires the optional 'cli' extra. " "Install it with: pip install 'select_ai[cli]'" ) - if team_name is None: - raise click.ClickException( - "--team or SELECT_AI_A2A_TEAM is required for standalone " - "deployment" - ) - if user is None or dsn is None: - raise click.ClickException( - "--user and --dsn (or their SELECT_AI_* environment " - "variables) are required for standalone deployment" - ) - if password is None: - password = getpass.getpass("Database password: ") app = create_app( team_name=team_name, public_url=public_url, @@ -183,35 +201,21 @@ def serve( pool_max_size=pool_max_size, allow_unauthenticated=allow_unauthenticated, ) - else: - try: - from select_ai.agent.a2a import ( - ConnectionConfig, - GatewaySettings, - create_gateway_app, - ) - from select_ai.agent.a2a.forms import load_connection_form - except ImportError as error: + elif deployment == "standalone": + if os.environ.get("WEB_CONCURRENCY", "1") != "1": raise click.ClickException( - "Clustered A2A support requires the optional 'a2a' extra. " - "Install it with: pip install 'select_ai[a2a]'" - ) from error - - connection = ConnectionConfig( - dsn=dsn, - username=user, - password=password, - team_name=team_name, + "Dynamic standalone deployment requires WEB_CONCURRENCY=1." + ) + settings = StandaloneSessionSettings( + public_url=public_url, + session_ttl_seconds=session_ttl_seconds, + description=description, + connection=connection, + connection_form_template=form_template, + allow_unauthenticated=allow_unauthenticated, ) - form_template = None - if a2ui_form: - try: - form_template = load_connection_form( - a2ui_form, - connection.missing_fields, - ) - except ValueError as error: - raise click.ClickException(str(error)) from error + app = create_embedded_session_app(settings) + else: settings = GatewaySettings( public_url=public_url, consul_url=consul_url, @@ -220,6 +224,7 @@ def serve( worker_tls_ca_file=worker_tls_ca_file, worker_tls_cert_file=worker_tls_cert_file, worker_tls_key_file=worker_tls_key_file, + description=description, connection=connection, connection_form_template=form_template, allow_unauthenticated=allow_unauthenticated, From 2f3c636c6f75b11c4cc5baad0ddc6d1972c6238f Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Tue, 15 Sep 2026 16:35:01 -0700 Subject: [PATCH 29/31] Update dynamic A2A verification samples --- samples/a2a/gateway/README.md | 78 ++++++++++++++++++++------ samples/a2a/gateway/_common.py | 82 +++++++++++++++++++++------- samples/a2a/gateway/blocking_task.py | 2 +- samples/a2a/gateway/inspect_form.py | 17 ++++++ samples/a2a/gateway/task_poll.py | 2 +- 5 files changed, 142 insertions(+), 39 deletions(-) create mode 100644 samples/a2a/gateway/inspect_form.py diff --git a/samples/a2a/gateway/README.md b/samples/a2a/gateway/README.md index 37ac6b6..e89c08d 100644 --- a/samples/a2a/gateway/README.md +++ b/samples/a2a/gateway/README.md @@ -1,30 +1,30 @@ -# Dynamic A2A gateway samples +# Dynamic A2A session samples -These samples connect to a dynamic gateway, submit its A2UI database -connection form, open a temporary worker session, and execute Select AI tasks -against the database. +These samples connect to either standalone or clustered dynamic deployment, +inspect and submit its A2UI database connection form, and execute Select AI +tasks against the database. -The gateway advertises `streaming: false` and supports request/response task +The dynamic server advertises `streaming: false` and supports request/response task operations. Long-running work is returned as a task and can be followed with `tasks/get`. -The gateway supports asynchronous work with `message/send` and +The server supports asynchronous work with `message/send` and `configuration.blocking: false`, followed by `tasks/get`. These samples use the A2A v0.3 JSON-RPC names used by the existing samples: `message/send`, `tasks/get`, and `tasks/cancel`. A client using A2A 1.0 should send `A2A-Version: 1.0` and use `SendMessage`, `GetTask`, `ListTasks`, and `CancelTask`; its non-blocking option is `configuration.returnImmediately`. -The gateway accepts both versions, but streaming is disabled in both. +The server accepts both versions, but streaming is disabled in both. The sample scripts import `call`, `connect`, `send_prompt`, and `print_task_summary` from the adjacent [`_common.py`](https://github.com/oracle/python-select-ai/blob/main/samples/a2a/gateway/_common.py) file. This is a repository-local sample helper, not an additional Python dependency. It sends -the JSON-RPC requests, performs the A2UI connection-form handshake using the -environment variables below, and formats the final task result. If you copy a -script elsewhere, copy +the JSON-RPC requests, reads the fields requested by the returned A2UI form, +submits only those fields from the environment variables below, and formats +the final task result. If you copy a script elsewhere, copy [`_common.py`](https://github.com/oracle/python-select-ai/blob/main/samples/a2a/gateway/_common.py) with it or replace those helpers with your own A2A client code. @@ -37,6 +37,31 @@ source .venv/bin/activate pip install -e '.[a2a]' ``` +### Dynamic standalone + +This is the smallest complete local setup. Fix the DSN and team on the server +so the generated form contains only username and password. Ensure database +username and password variables are absent from the server process so it does +not select the fixed shared-pool path: + +```bash +env -u SELECT_AI_USER -u SELECT_AI_PASSWORD \ + select-ai a2a serve \ + --deployment standalone \ + --host 127.0.0.1 \ + --port 8000 \ + --public-url http://127.0.0.1:8000 \ + --allow-unauthenticated \ + --dsn '' \ + --team ORACLE_AI_DATABASE_AGENT +``` + +Dynamic standalone keeps its routing in memory and therefore runs as one +server process and one service instance. It does not require Consul or the +separate worker command. + +### Clustered + Run these commands in three terminals from the repository root. Terminal 1, Consul: @@ -68,12 +93,13 @@ select-ai a2a serve \ --host 127.0.0.1 \ --port 8000 \ --public-url http://127.0.0.1:8000 \ + --allow-unauthenticated \ --consul-url http://127.0.0.1:8500 ``` -The worker must be able to connect to the database when a sample submits the -form. Export the same values used by the other samples, plus the optional -team name: +For either deployment, export the form values in the terminal that runs the +sample scripts. The helper submits only the properties requested by the +server, so deployment-fixed DSN or team values are not sent back: ```bash export SELECT_AI_DB_CONNECT_STRING='' @@ -83,13 +109,29 @@ export SELECT_AI_A2A_TEAM='ORACLE_AI_DATABASE_AGENT' ``` For a TNS-alias DSN, set `TNS_ADMIN` in the worker terminal before starting -the worker. Wallet-based Oracle Database mTLS is not currently supported by -the gateway session connection path. +the clustered worker, or in the standalone server terminal. Wallet-based +Oracle Database mTLS is not currently supported by the dynamic session path. + +To call an authenticated server, set its bearer JWT without changing the +scripts: + +```bash +export SELECT_AI_A2A_BEARER_TOKEN='' +``` + +Override the default endpoint with `SELECT_AI_A2A_ENDPOINT`. ## Run the samples -The gateway-specific samples perform the connection-form handshake -automatically and validate that the final artifact is +First inspect the exact A2UI artifact and requested fields without submitting +credentials: + +```bash +python samples/a2a/gateway/inspect_form.py +``` + +The task samples perform the connection-form handshake automatically and +validate that the final artifact is `database-agent-result`. Blocking database task: @@ -118,3 +160,5 @@ For a quick health check: curl http://127.0.0.1:8081/health curl -sS http://127.0.0.1:8000/.well-known/agent-card.json | jq ``` + +The worker health endpoint applies only to clustered deployment. diff --git a/samples/a2a/gateway/_common.py b/samples/a2a/gateway/_common.py index 25b03a5..3aa6c01 100644 --- a/samples/a2a/gateway/_common.py +++ b/samples/a2a/gateway/_common.py @@ -5,7 +5,7 @@ # https://oss.oracle.com/licenses/upl. # ----------------------------------------------------------------------------- -"""Small A2A v0.3 client helpers for the dynamic gateway samples.""" +"""Small A2A v0.3 client helpers for dynamic session samples.""" import json import os @@ -13,14 +13,23 @@ from urllib.request import Request, urlopen ENDPOINT = os.environ.get( - "SELECT_AI_A2A_GATEWAY_ENDPOINT", + "SELECT_AI_A2A_ENDPOINT", "http://127.0.0.1:8000/a2a/jsonrpc/", ) TEAM_NAME = os.environ.get("SELECT_AI_A2A_TEAM", "ORACLE_AI_DATABASE_AGENT") +_CONNECTION_ENV = { + "dsn": "SELECT_AI_DB_CONNECT_STRING", + "username": "SELECT_AI_USER", + "password": "SELECT_AI_PASSWORD", + "team_name": "SELECT_AI_A2A_TEAM", +} def call(method: str, params: dict) -> dict: """Make one A2A v0.3 JSON-RPC call and return its result.""" + headers = {"Content-Type": "application/json"} + if token := os.environ.get("SELECT_AI_A2A_BEARER_TOKEN"): + headers["Authorization"] = f"Bearer {token}" request = Request( ENDPOINT, data=json.dumps( @@ -31,7 +40,7 @@ def call(method: str, params: dict) -> dict: "params": params, } ).encode(), - headers={"Content-Type": "application/json"}, + headers=headers, method="POST", ) with urlopen(request) as response: # noqa: S310 @@ -53,13 +62,20 @@ def _message(text: str, context_id: str | None = None) -> dict: def connect(prompt: str) -> str: - """Bootstrap one gateway session and return its context ID.""" - form_task = call("message/send", {"message": _message(prompt)}) - if form_task["artifacts"][0]["name"] != "database-connection-form": - raise RuntimeError( - "Expected database-connection-form, got " - f"{form_task['artifacts'][0].get('name')}" - ) + """Submit the requested form fields and return the connected context.""" + form_task, fields = request_connection_form(prompt) + submitted = {} + for field in fields: + env_name = _CONNECTION_ENV[field] + value = os.environ.get(env_name) + if field == "team_name" and not value: + value = TEAM_NAME + if not value: + raise RuntimeError( + f"The connection form requires {field}; set {env_name}." + ) + + submitted[field] = value context_id = form_task["contextId"] connection_task = call( @@ -77,16 +93,7 @@ def connect(prompt: str) -> str: "version": "v0.9", "action": { "name": "submit_database_connection", - "context": { - "dsn": os.environ[ - "SELECT_AI_DB_CONNECT_STRING" - ], - "username": os.environ["SELECT_AI_USER"], - "password": os.environ[ - "SELECT_AI_PASSWORD" - ], - "team_name": TEAM_NAME, - }, + "context": submitted, }, }, "metadata": {"mimeType": "application/json+a2ui"}, @@ -101,6 +108,41 @@ def connect(prompt: str) -> str: return context_id +def request_connection_form(prompt: str) -> tuple[dict, tuple[str, ...]]: + """Request and inspect the server-generated A2UI connection form.""" + form_task = call("message/send", {"message": _message(prompt)}) + if form_task["artifacts"][0]["name"] != "database-connection-form": + raise RuntimeError( + "Expected database-connection-form, got " + f"{form_task['artifacts'][0].get('name')}" + ) + + fields = _connection_form_fields(form_task) + if not fields: + raise RuntimeError("The A2UI connection form contains no fields.") + return form_task, fields + + +def _connection_form_fields(form_task: dict) -> tuple[str, ...]: + """Read canonical fields from the A2UI submit action.""" + artifact = form_task["artifacts"][0] + for part in artifact.get("parts") or []: + operation = part.get("data") or {} + update = operation.get("updateComponents") or {} + for component in update.get("components") or []: + event = (component.get("action") or {}).get("event") or {} + if event.get("name") == "submit_database_connection": + context = event.get("context") or {} + unsupported = set(context) - set(_CONNECTION_ENV) + if unsupported: + raise RuntimeError( + "Connection form requested unsupported fields: " + + ", ".join(sorted(unsupported)) + ) + return tuple(context) + return () + + def send_prompt( prompt: str, context_id: str, diff --git a/samples/a2a/gateway/blocking_task.py b/samples/a2a/gateway/blocking_task.py index 5f18f8a..9d92307 100644 --- a/samples/a2a/gateway/blocking_task.py +++ b/samples/a2a/gateway/blocking_task.py @@ -5,7 +5,7 @@ # https://oss.oracle.com/licenses/upl. # ----------------------------------------------------------------------------- -"""Connect to a dynamic A2A gateway, then send a blocking database request.""" +"""Connect to a dynamic A2A server, then send a blocking database request.""" from _common import connect, print_task_summary, send_prompt diff --git a/samples/a2a/gateway/inspect_form.py b/samples/a2a/gateway/inspect_form.py new file mode 100644 index 0000000..f4622fa --- /dev/null +++ b/samples/a2a/gateway/inspect_form.py @@ -0,0 +1,17 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# https://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +"""Request and print the dynamic A2UI database connection form.""" + +import json + +from _common import request_connection_form + +task, fields = request_connection_form("Show the database connection form.") +print(f"Context: {task['contextId']}") +print("Requested fields: " + ", ".join(fields)) +print(json.dumps(task["artifacts"][0], indent=2)) diff --git a/samples/a2a/gateway/task_poll.py b/samples/a2a/gateway/task_poll.py index 698611c..b675a09 100644 --- a/samples/a2a/gateway/task_poll.py +++ b/samples/a2a/gateway/task_poll.py @@ -5,7 +5,7 @@ # https://oss.oracle.com/licenses/upl. # ----------------------------------------------------------------------------- -"""Connect to a dynamic A2A gateway, then poll a database task.""" +"""Connect to a dynamic A2A server, then poll a database task.""" import time From 0878d680d3f40ab07f8d83aa96771d657cc27efe Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Tue, 15 Sep 2026 16:43:43 -0700 Subject: [PATCH 30/31] Rename dynamic A2A samples --- README.md | 4 +-- doc/source/user_guide/a2a.rst | 28 +++++++++---------- gcloud/README.md | 2 +- samples/README.md | 16 +++++------ samples/a2a/{gateway => dynamic}/README.md | 12 ++++---- samples/a2a/{gateway => dynamic}/_common.py | 2 +- .../a2a/{gateway => dynamic}/blocking_task.py | 0 .../a2a/{gateway => dynamic}/inspect_form.py | 0 samples/a2a/{gateway => dynamic}/task_poll.py | 0 9 files changed, 32 insertions(+), 32 deletions(-) rename samples/a2a/{gateway => dynamic}/README.md (95%) rename samples/a2a/{gateway => dynamic}/_common.py (98%) rename samples/a2a/{gateway => dynamic}/blocking_task.py (100%) rename samples/a2a/{gateway => dynamic}/inspect_form.py (100%) rename samples/a2a/{gateway => dynamic}/task_poll.py (100%) diff --git a/README.md b/README.md index 00e33f8..a1463d2 100644 --- a/README.md +++ b/README.md @@ -122,8 +122,8 @@ The CLI passes both values to the Select AI SDK as `wallet_location` and The server accepts both A2A 1.x and the A2A v0.3 JSON-RPC streaming protocol for compatibility with Gemini Enterprise. -See the [A2A user guide](doc/source/user_guide/a2a.rst) for the dynamic -gateway, A2UI connection flow, persistent task state, task polling and +See the [A2A user guide](doc/source/user_guide/a2a.rst) for dynamic sessions, +the A2UI connection flow, persistent task state, task polling and cancellation, wallet configuration, and Google Cloud deployment modes. Generate the A2A v0.3 Agent Card to paste into Gemini Enterprise after the diff --git a/doc/source/user_guide/a2a.rst b/doc/source/user_guide/a2a.rst index a4d3bf7..074e2bb 100644 --- a/doc/source/user_guide/a2a.rst +++ b/doc/source/user_guide/a2a.rst @@ -487,9 +487,9 @@ session routing metadata. The credentials remain in the worker child process for the lifetime of that session, so use TLS for client-to-gateway traffic and follow the security policies for any client that renders the form. -The gateway samples perform this handshake automatically. They use the +The dynamic-session samples perform this handshake automatically. They use the repository-local helper module -`samples/a2a/gateway/_common.py `__; +`samples/a2a/dynamic/_common.py `__; ``_common`` is not a package that users install with ``pip``. Running the samples from the repository root as shown below makes that helper available automatically. @@ -497,7 +497,7 @@ repository root as shown below makes that helper available automatically. The helper functions are: ``call(method, params)`` - Sends one A2A v0.3 JSON-RPC request to the configured gateway endpoint. + Sends one A2A v0.3 JSON-RPC request to the configured server endpoint. ``connect(prompt)`` Sends the initial prompt, reads the A2UI connection form, submits the @@ -506,26 +506,26 @@ The helper functions are: A2A context ID for the connected session. ``send_prompt(prompt, context_id, blocking=None)`` - Sends a database prompt in an existing gateway context. Passing + Sends a database prompt in an existing dynamic session. Passing ``blocking=False`` adds the non-blocking request option. ``print_task_summary(task)`` Prints the task state, result artifact name, and text parts without dumping the connection-form details. -If you copy a gateway sample into another directory, copy the -`_common.py helper `__ +If you copy a dynamic-session sample into another directory, copy the +`_common.py helper `__ with it, or replace these helpers with an A2A client implementation of your own. -The complete gateway setup is also documented in the -`gateway sample README `__. +The complete dynamic-session setup is also documented in the +`dynamic sample README `__. The examples below are the -`blocking_task.py sample `__ +`blocking_task.py sample `__ and the -`task_poll.py sample `__. +`task_poll.py sample `__. -.. literalinclude:: ../../../samples/a2a/gateway/blocking_task.py +.. literalinclude:: ../../../samples/a2a/dynamic/blocking_task.py :language: python :lines: 8- @@ -542,7 +542,7 @@ Run it with: .. code-block:: bash - python samples/a2a/gateway/blocking_task.py + python samples/a2a/dynamic/blocking_task.py Representative output is: @@ -552,10 +552,10 @@ Representative output is: Artifact: database-agent-result The database contains ... -The gateway polling sample uses the same connection-form handshake, then +The dynamic polling sample uses the same connection-form handshake, then passes ``configuration.blocking: false`` and polls ``tasks/get``: -.. literalinclude:: ../../../samples/a2a/gateway/task_poll.py +.. literalinclude:: ../../../samples/a2a/dynamic/task_poll.py :language: python :lines: 8- diff --git a/gcloud/README.md b/gcloud/README.md index dca668e..62f34d0 100644 --- a/gcloud/README.md +++ b/gcloud/README.md @@ -103,7 +103,7 @@ Both deployments expose the public A2A endpoint at: Both accept A2A 1.0 method names and the A2A v0.3 compatibility method names. The gateway client flow is documented in the -[gateway samples](../samples/a2a/gateway/README.md). +[dynamic-session samples](../samples/a2a/dynamic/README.md). ## Which deployment should you choose? diff --git a/samples/README.md b/samples/README.md index c083e41..932b4f9 100644 --- a/samples/README.md +++ b/samples/README.md @@ -219,22 +219,22 @@ Task 7e1...: completed } ``` -## A2A dynamic gateway +## Dynamic A2A sessions -The dynamic gateway samples submit the A2UI database connection form, open a -temporary worker session, and execute database tasks. The gateway advertises +The dynamic session samples submit the A2UI database connection form, open a +temporary database session, and execute database tasks. The server advertises `streaming: false` and supports non-blocking task execution with `configuration.blocking: false` and `tasks/get`. -Gateway-specific samples that perform the form handshake and then execute a -real database task are in [a2a/gateway](a2a/gateway/README.md): +Dynamic-session samples that perform the form handshake and then execute a +real database task are in [a2a/dynamic](a2a/dynamic/README.md): ```bash -python samples/a2a/gateway/blocking_task.py -python samples/a2a/gateway/task_poll.py +python samples/a2a/dynamic/blocking_task.py +python samples/a2a/dynamic/task_poll.py ``` -See that README for local Consul, worker, and gateway startup instructions. +See that README for standalone and clustered startup instructions. The full A2A architecture, protocol details, session lifecycle, and Google Cloud deployment explanation are in the diff --git a/samples/a2a/gateway/README.md b/samples/a2a/dynamic/README.md similarity index 95% rename from samples/a2a/gateway/README.md rename to samples/a2a/dynamic/README.md index e89c08d..34eca84 100644 --- a/samples/a2a/gateway/README.md +++ b/samples/a2a/dynamic/README.md @@ -19,13 +19,13 @@ The server accepts both versions, but streaming is disabled in both. The sample scripts import `call`, `connect`, `send_prompt`, and `print_task_summary` from the adjacent -[`_common.py`](https://github.com/oracle/python-select-ai/blob/main/samples/a2a/gateway/_common.py) +[`_common.py`](https://github.com/oracle/python-select-ai/blob/main/samples/a2a/dynamic/_common.py) file. This is a repository-local sample helper, not an additional Python dependency. It sends the JSON-RPC requests, reads the fields requested by the returned A2UI form, submits only those fields from the environment variables below, and formats the final task result. If you copy a script elsewhere, copy -[`_common.py`](https://github.com/oracle/python-select-ai/blob/main/samples/a2a/gateway/_common.py) +[`_common.py`](https://github.com/oracle/python-select-ai/blob/main/samples/a2a/dynamic/_common.py) with it or replace those helpers with your own A2A client code. ## Local setup @@ -83,7 +83,7 @@ select-ai a2a worker \ --worker-endpoint http://127.0.0.1:8081 ``` -Terminal 3, the gateway: +Terminal 3, the dynamic server: ```bash source .venv/bin/activate @@ -127,7 +127,7 @@ First inspect the exact A2UI artifact and requested fields without submitting credentials: ```bash -python samples/a2a/gateway/inspect_form.py +python samples/a2a/dynamic/inspect_form.py ``` The task samples perform the connection-form handshake automatically and @@ -137,13 +137,13 @@ validate that the final artifact is Blocking database task: ```bash -python samples/a2a/gateway/blocking_task.py +python samples/a2a/dynamic/blocking_task.py ``` Non-blocking database task with polling: ```bash -python samples/a2a/gateway/task_poll.py +python samples/a2a/dynamic/task_poll.py ``` Expected task output is similar to: diff --git a/samples/a2a/gateway/_common.py b/samples/a2a/dynamic/_common.py similarity index 98% rename from samples/a2a/gateway/_common.py rename to samples/a2a/dynamic/_common.py index 3aa6c01..8958698 100644 --- a/samples/a2a/gateway/_common.py +++ b/samples/a2a/dynamic/_common.py @@ -148,7 +148,7 @@ def send_prompt( context_id: str, blocking: bool | None = None, ) -> dict: - """Send a normal database prompt through an existing gateway session.""" + """Send a normal database prompt through an existing dynamic session.""" params = {"message": _message(prompt, context_id)} if blocking is False: params["configuration"] = {"blocking": False} diff --git a/samples/a2a/gateway/blocking_task.py b/samples/a2a/dynamic/blocking_task.py similarity index 100% rename from samples/a2a/gateway/blocking_task.py rename to samples/a2a/dynamic/blocking_task.py diff --git a/samples/a2a/gateway/inspect_form.py b/samples/a2a/dynamic/inspect_form.py similarity index 100% rename from samples/a2a/gateway/inspect_form.py rename to samples/a2a/dynamic/inspect_form.py diff --git a/samples/a2a/gateway/task_poll.py b/samples/a2a/dynamic/task_poll.py similarity index 100% rename from samples/a2a/gateway/task_poll.py rename to samples/a2a/dynamic/task_poll.py From 106f7a49dfad13f6c0dcf79663a87f6798bcc8f8 Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Tue, 15 Sep 2026 23:20:36 -0700 Subject: [PATCH 31/31] Unify A2A standalone and clustered deployments. Add dynamic connection forms and OAuth session isolation Update Google Cloud scripts, samples, and documentation --- doc/source/user_guide/a2a.rst | 118 +++++++---- doc/source/user_guide/cli.rst | 9 +- docker/a2a-entrypoint.sh | 21 +- gcloud/README.md | 155 +++++++------- gcloud/{gateway => cluster}/README.md | 126 ++++++++---- gcloud/{gateway => cluster}/cloudbuild.yaml | 120 ++++++----- gcloud/{gateway => cluster}/deploy.sh | 152 +++++++++----- gcloud/{gateway => cluster}/gke/consul.yaml | 4 +- .../{gateway => cluster}/gke/namespace.yaml | 2 +- .../{gateway => cluster}/gke/worker-mtls.yaml | 28 +-- .../gke/worker-service.yaml | 6 +- gcloud/{gateway => cluster}/gke/worker.yaml | 14 +- gcloud/standalone/README.md | 189 +++++++++++++++--- gcloud/standalone/cloudbuild.yaml | 2 +- gcloud/standalone/deploy.sh | 187 +++++++++++++---- samples/a2a/dynamic/README.md | 8 +- samples/a2a/dynamic/_common.py | 6 +- src/select_ai/agent/a2a/auth.py | 52 ++--- src/select_ai/agent/a2a/embedded.py | 2 +- src/select_ai/agent/a2a/forms.py | 10 +- src/select_ai/agent/a2a/gateway.py | 20 +- src/select_ai/agent/a2a/models.py | 32 ++- src/select_ai/agent/a2a/server.py | 6 +- src/select_ai/cli/a2a.py | 32 +-- tests/a2a/test_worker_runtime.py | 14 +- 25 files changed, 870 insertions(+), 445 deletions(-) rename gcloud/{gateway => cluster}/README.md (67%) rename gcloud/{gateway => cluster}/cloudbuild.yaml (57%) rename gcloud/{gateway => cluster}/deploy.sh (67%) rename gcloud/{gateway => cluster}/gke/consul.yaml (94%) rename gcloud/{gateway => cluster}/gke/namespace.yaml (61%) rename gcloud/{gateway => cluster}/gke/worker-mtls.yaml (74%) rename gcloud/{gateway => cluster}/gke/worker-service.yaml (80%) rename gcloud/{gateway => cluster}/gke/worker.yaml (78%) diff --git a/doc/source/user_guide/a2a.rst b/doc/source/user_guide/a2a.rst index 074e2bb..f4a83ca 100644 --- a/doc/source/user_guide/a2a.rst +++ b/doc/source/user_guide/a2a.rst @@ -181,6 +181,34 @@ The server prints the discovery URL when it starts: The exact Uvicorn startup lines vary by version and configuration. +End-user OAuth and session ownership +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, ``select-ai a2a serve`` does not require an application OAuth +token. It gives each A2A conversation its own database session. A hosting +platform such as private Cloud Run can still authenticate the calling service, +but the A2A server does not verify which human user owns the conversation. + +Add ``--require-oauth`` when verified end-user ownership is required: + +.. code-block:: bash + + select-ai a2a serve \ + --require-oauth \ + --team ORACLE_AI_DATABASE_AGENT \ + --host 0.0.0.0 \ + --port 8000 \ + --public-url https://a2a.example.com + +Every A2A request must then contain ``Authorization: Bearer ...``. The server +uses both the authenticated owner and the A2A conversation to isolate +sessions, tasks, and conversation state. It accepts an OpenID Connect ID token +or an opaque OAuth 2.0 access token. ID-token ownership remains stable across +refreshes through the ``iss`` and ``sub`` claims; refreshing an opaque access +token starts a new owner and session scope. Tokens are never stored or logged. + +The Agent Card advertises bearer security only with ``--require-oauth``. + Agent Card discovery ~~~~~~~~~~~~~~~~~~~~ @@ -384,7 +412,7 @@ The ``-dev`` agent below is intentionally for local development only. It uses an in-memory, single-node Consul server bound to loopback. For the Google Cloud deployment, do not install or run a local agent: the deployment creates Consul in GKE from the -`Consul manifest `__. +`Consul manifest `__. Start the three local components in separate terminals: @@ -447,14 +475,14 @@ type. It contains only the values not configured on the server: * - Field - Purpose - * - ``dsn`` + * - ``connection_url`` - Oracle connect descriptor, simplified connect string, or TNS alias available to the worker. * - ``username`` - Database user for this session. * - ``password`` - Database password. The form renders this field as obscured input. - * - ``team_name`` + * - ``ai_agent`` - Select AI Agent Team to run in this session. * - ``submit_database_connection`` - A2UI action that submits exactly the displayed values. @@ -468,8 +496,9 @@ Use ``--a2ui-form PATH`` to replace the generated form. The file must be a top-level JSON array of A2UI v0.9 operations using the advertised catalog and one consistent template ``surfaceId``. It must contain exactly one ``submit_database_connection`` action whose context submits exactly the -missing canonical properties: ``dsn``, ``username``, ``password``, and/or -``team_name``. A collected password must use an obscured ``TextField`` and the +missing canonical properties: ``connection_url``, ``username``, ``password``, +and/or ``ai_agent``. A collected password must use an obscured ``TextField`` +and the template must not contain a password default. The server validates the file at startup and replaces its template surface ID with a fresh ID every time the form is displayed. @@ -643,21 +672,25 @@ Standalone Google Cloud deployment ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ `gcloud/standalone/deploy.sh `__ -deploys one private Cloud Run service for one -database and team: +deploys one private Cloud Run service. Select its connection provisioning +explicitly: .. code-block:: bash gcloud/standalone/deploy.sh \ --project PROJECT_ID \ - --a2a-team ORACLE_AI_DATABASE_AGENT \ + --connection-mode dynamic \ --build The script creates or reuses an Artifact Registry repository, runtime service -account, Secret Manager secrets, and Cloud Run service. On the first run it -prompts for the database user, password, and connect descriptor. It injects -them into the service as ``SELECT_AI_USER``, ``SELECT_AI_PASSWORD``, and -``SELECT_AI_DB_CONNECT_STRING``. The service is private; the script grants +account, any requested Secret Manager secrets, and Cloud Run service. With no +database options, dynamic mode fixes neither the Connection URL nor AI Agent; +its A2UI form requests Connection URL, username, password, and AI Agent. Pass +``--db-dsn-secret`` or ``--a2a-team`` to fix either value independently, or +``--rotate-db-config`` to configure the deployment's Connection URL. Fixed +mode prompts for Connection URL, username, and password, requires +``--a2a-team``, and creates the shared startup pool. The service is private; the +script grants ``run.routes.invoke`` to the active deployment identity and the Gemini Enterprise Discovery Engine service agent. @@ -671,13 +704,20 @@ already deployed and only updates Cloud Run configuration or secrets. Important standalone options include: +* ``--connection-mode``: required ``fixed`` or ``dynamic`` provisioning. * ``--service``: Cloud Run service name; use a different service for each - fixed team/database deployment. -* ``--a2a-team``: team installed in Oracle Database. + deployment profile. +* ``--a2a-team``: fix the AI Agent in the deployment; required in fixed mode. +* ``--require-oauth``: require a Gemini Enterprise end-user OAuth bearer token + and scope state by verified owner as well as A2A conversation. * ``--pool-max-size``: maximum Oracle connections per Cloud Run instance. -* ``--max-instances``: Cloud Run instance limit. -* ``--wallet-archive``: upload or replace an Autonomous Database wallet ZIP. -* ``--rotate-db-credentials``: prompt for and rotate the database secrets. +* ``--max-instances``: Cloud Run instance limit; dynamic mode requires one. +* ``--wallet-archive``: upload or replace an Autonomous Database wallet ZIP + in fixed mode. +* ``--db-dsn-secret``: fix the Connection URL using an existing Secret Manager + secret. +* ``--rotate-db-config``: prompt for and create or rotate the deployment's + database values. For a wallet deployment, ``--wallet-archive`` stores the ZIP and wallet password in service-specific Secret Manager secrets. Cloud Run mounts the ZIP @@ -689,42 +729,50 @@ The standalone script prints the deployed Agent Card JSON at the end. The service is ready for a client when the printed card points to the final Cloud Run URL and the client can invoke the private service. -Dynamic gateway Google Cloud deployment +The Google Cloud scripts do not require end-user OAuth by default. Cloud Run +IAM authenticates Gemini Enterprise, and ``a2a serve`` separates database +sessions by A2A conversation without asserting a verified human owner. Add +``--require-oauth`` to the deployment command when verified user ownership is +required. Gemini Enterprise must then be configured with end-user OAuth rather +than **Skip & Finish**. It uses ``X-Serverless-Authorization`` for Cloud Run +IAM and sends the user token separately in ``Authorization``. + +Clustered Google Cloud deployment ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -`gcloud/gateway/deploy.sh `__ -deploys the gateway topology: Cloud Run for the -public gateway, GKE Autopilot for worker replicas, and Consul for discovery and +`gcloud/cluster/deploy.sh `__ +deploys the clustered topology: Cloud Run for the +public server, GKE Autopilot for worker replicas, and Consul for discovery and routing. .. code-block:: bash - gcloud/gateway/deploy.sh \ + gcloud/cluster/deploy.sh \ --project PROJECT_ID \ --region us-central1 \ --worker-replicas 3 The script and -`gcloud/gateway/cloudbuild.yaml `__ +`gcloud/cluster/cloudbuild.yaml `__ perform these steps: 1. Enable the required Google Cloud APIs and create or reuse Artifact Registry and a GKE Autopilot cluster. 2. Configure additive VPC DNS so StatefulSet worker names resolve to current Pod IPs after a worker is recreated. -3. Deploy the gateway namespace, internal Consul service, and worker service. +3. Deploy the A2A namespace, internal Consul service, and worker service. 4. Build and publish the image with Cloud Build. 5. Deploy the requested worker replicas with ``select-ai a2a worker`` and the selected session TTL. 6. Deploy Cloud Run with ``select-ai a2a serve --deployment clustered`` and set ``PUBLIC_URL`` to the final Cloud Run URL. -The Cloud Run gateway uses direct VPC egress to reach the internal Consul +The Cloud Run server uses direct VPC egress to reach the internal Consul load-balancer address and worker endpoints. The GKE worker service is headless service discovery, not a public load balancer. Consul selects a healthy worker when a session opens; the route is then pinned to that worker. -Important gateway options include: +Important clustered options include: * ``--cluster`` and ``--gke-dns-domain``: GKE Autopilot cluster and immutable additive DNS domain. @@ -732,18 +780,18 @@ Important gateway options include: internal worker infrastructure. * ``--worker-replicas``: number of worker runtimes available for new sessions. * ``--session-ttl-seconds``: lifetime of temporary database sessions. -* ``--enable-worker-mtls``: create and use gateway-to-worker certificates. +* ``--enable-worker-mtls``: create and use server-to-worker certificates. * ``--rotate-worker-mtls``: replace the existing test certificates and restart the worker workload. -The default gateway deployment uses private-VPC HTTP between Cloud Run and +The default clustered deployment uses private-VPC HTTP between Cloud Run and GKE. With ``--enable-worker-mtls``, the script creates short-lived test PKI material in Secret Manager, creates the Kubernetes TLS secrets, deploys the -worker StatefulSet variant, and mounts the gateway client certificate into +worker StatefulSet variant, and mounts the server client certificate into Cloud Run. The worker certificate uses the GKE StatefulSet DNS name, so the ``--gke-dns-domain`` value must remain consistent with the cluster. -The gateway deployment does not upload an Oracle wallet because the dynamic +The clustered deployment does not upload an Oracle wallet because the dynamic worker session path currently accepts only DSN/user/password. If database mTLS is required, use the standalone deployment or provide a separate database connection mechanism to the worker implementation. @@ -763,15 +811,15 @@ The deployment files are intended to be read together: - Builds and publishes the standalone container image. * - `gcloud/standalone/README.md `__ - Documents IAM, secrets, wallet archives, and update behavior. - * - `gcloud/gateway/deploy.sh `__ - - Creates or reuses the gateway GKE/Cloud Run topology and supplies build + * - `gcloud/cluster/deploy.sh `__ + - Creates or reuses the clustered GKE/Cloud Run topology and supplies build substitutions. - * - `gcloud/gateway/cloudbuild.yaml `__ + * - `gcloud/cluster/cloudbuild.yaml `__ - Builds the image, deploys Consul and workers, and deploys Cloud Run. - * - `gcloud/gateway/gke manifests `__ + * - `gcloud/cluster/gke manifests `__ - Namespace, Consul, headless worker service, and HTTP or mTLS worker workloads. - * - `gcloud/gateway/README.md `__ + * - `gcloud/cluster/README.md `__ - Explains the topology, DNS, mTLS test mode, and operational details. Troubleshooting and security notes diff --git a/doc/source/user_guide/cli.rst b/doc/source/user_guide/cli.rst index 3adcc0b..84abdb1 100644 --- a/doc/source/user_guide/cli.rst +++ b/doc/source/user_guide/cli.rst @@ -257,6 +257,12 @@ Important options are ``--team`` (required), ``--host``, ``--port``, wallet for this standalone path. If no password is provided, the command prompts for it. +By default, ``a2a serve`` does not require an application OAuth token and +separates database sessions by A2A conversation. Add ``--require-oauth`` to +require ``Authorization: Bearer ...`` and scope sessions by authenticated +owner as well as conversation. The Agent Card advertises bearer security only +in that mode. + Dynamic gateway and worker -------------------------- @@ -297,7 +303,8 @@ The clustered serve options are ``--public-url`` (or ``PUBLIC_URL``), ``SESSION_TTL_SECONDS``). ``--dsn``, ``--user``, ``--password``, and ``--team`` fix any supplied connection values; the generated A2UI form contains only the missing values. ``--a2ui-form`` loads a custom A2UI JSON form that must submit -exactly those missing values. The optional +exactly those missing values. ``--require-oauth`` has the same ownership +behavior in standalone and clustered deployments. The optional ``--worker-tls-ca-file``, ``--worker-tls-cert-file``, and ``--worker-tls-key-file`` options configure the gateway's client side of gateway-to-worker mTLS. These TLS settings protect the internal HTTP hop and diff --git a/docker/a2a-entrypoint.sh b/docker/a2a-entrypoint.sh index 807fdfc..8f241a8 100644 --- a/docker/a2a-entrypoint.sh +++ b/docker/a2a-entrypoint.sh @@ -28,14 +28,21 @@ if [ -f "$wallet_archive" ]; then export SELECT_AI_WALLET_LOCATION="$(dirname "$wallet_file")" fi -: "${SELECT_AI_A2A_TEAM:?SELECT_AI_A2A_TEAM is required}" : "${PUBLIC_URL:?PUBLIC_URL is required}" : "${SELECT_AI_POOL_MAX_SIZE:=10}" -exec select-ai a2a serve \ - --deployment standalone \ - --team "$SELECT_AI_A2A_TEAM" \ - --host 0.0.0.0 \ - --port "${PORT:-8080}" \ - --pool-max-size "$SELECT_AI_POOL_MAX_SIZE" \ +serve_args=( + --deployment standalone + --host 0.0.0.0 + --port "${PORT:-8080}" + --pool-max-size "$SELECT_AI_POOL_MAX_SIZE" --public-url "$PUBLIC_URL" +) +if [ -n "${SELECT_AI_A2A_TEAM:-}" ]; then + serve_args+=(--team "$SELECT_AI_A2A_TEAM") +fi +if [ "${SELECT_AI_A2A_REQUIRE_OAUTH:-false}" = "true" ]; then + serve_args+=(--require-oauth) +fi + +exec select-ai a2a serve "${serve_args[@]}" diff --git a/gcloud/README.md b/gcloud/README.md index 62f34d0..01ae2bd 100644 --- a/gcloud/README.md +++ b/gcloud/README.md @@ -1,116 +1,107 @@ # Google Cloud deployment modes -Select AI for Python supports two A2A deployment architectures. The key -decisions are where the database connection and Select AI team are selected, -which components carry the session, and how capacity is added: - -- Standalone fixes the database and team at deployment time. One Cloud Run A2A - service owns the configured connection pool and serves that team. -- The gateway selects the database and team per user session. A Cloud Run A2A - gateway routes sessions through Consul to a clustered GKE worker pool, with - one isolated child runtime and database connection pool per active session. - -The gateway architecture is designed for horizontal session capacity. Gateway -instances, Consul, and worker replicas are separate components; adding worker -replicas increases the number of concurrent database sessions that can be -hosted behind the same A2A endpoint. Consul preserves session and task affinity -when requests reach different gateway instances. Oracle Database capacity and -the configured session TTL remain the limiting factors. +Select AI for Python supports three Google Cloud A2A deployment profiles. The +profiles keep deployment topology separate from connection provisioning: + +1. **Fixed standalone** runs one Cloud Run service with DSN, username, + password, and team fixed by deployment. It uses the shared connection pool + and supports streaming. +2. **Dynamic standalone** runs one Cloud Run service. Connection URL and AI + Agent may be fixed independently by deployment; each user supplies all + remaining connection values through A2UI and receives an isolated + child-process session. Its in-memory routing requires exactly one Cloud Run + instance and streaming is disabled. +3. **Clustered dynamic** runs the same public A2A contract on Cloud Run, with + Consul and workers on GKE. DSN and team are fixed; each user supplies + username and password through A2UI. Consul provides distributed routing and + workers own the isolated session processes. ![Select AI A2A deployment architecture](../doc/source/image/a2a_architecture.svg) -The A2A commands are cloud-neutral: `select-ai a2a serve`, -`select-ai a2a serve --deployment clustered`, and `select-ai a2a worker` can -run as processes or -containers on any cloud platform, a Kubernetes cluster, or self-managed -infrastructure with the required Oracle and Consul connectivity. The scripts -in this directory are optional Google Cloud automation for the Cloud Run/GKE -topologies shown below. +The A2A commands are cloud-neutral. These scripts are optional Google Cloud +automation for the Cloud Run and GKE profiles. -## What the A2A client connects to +## Standalone deployments -### Standalone server +Use the same script with an explicit connection mode. -The standalone deployment is one Cloud Run A2A service for one configured -Oracle database and one Select AI team. - -The service receives its database credentials from Secret Manager. The A2A -client can discover the Agent Card and immediately send a database prompt. -The server supports blocking tasks, task polling, and streaming responses. - -Deploy it with: +Dynamic standalone: ```bash -gcloud/standalone/deploy.sh --build +gcloud/standalone/deploy.sh \ + --project PROJECT_ID \ + --connection-mode dynamic \ + --build ``` -Use [standalone deployment](standalone/README.md) for the deployment details. +Fixed standalone: -### Dynamic gateway +```bash +gcloud/standalone/deploy.sh \ + --project PROJECT_ID \ + --connection-mode fixed \ + --a2a-team ORACLE_AI_DATABASE_AGENT \ + --build +``` -The gateway deployment provides one public A2A endpoint for users who choose -the database and Select AI team at runtime. +The modes use distinct default services: -The client first sends a message and receives an A2UI connection form. After -the client submits the DSN, username, password, and team name, the gateway -opens a temporary worker session. Subsequent A2A messages use that session and -execute against the selected database and team. +- `select-ai-a2a-standalone-dynamic` +- `select-ai-a2a-standalone-fixed` -The gateway supports blocking tasks and asynchronous task polling. Its Agent -Card advertises `streaming: false`; clients use `message/send` followed by -`tasks/get` for long-running work. The gateway-to-worker path uses internal -protobuf messages, while the public client-facing path remains A2A JSON-RPC. +See the [standalone deployment guide](standalone/README.md). -The gateway database session currently accepts a DSN, username, and password. -Wallet-based Oracle Database mTLS is not yet supported by this session path. -The optional mTLS deployment mode described in the gateway documentation -secures the gateway-to-worker connection; it is separate from database mTLS. +The standalone guide also shows how to select the newest existing image +directly from Artifact Registry by immutable digest. Use that workflow when +creating another Cloud Run service without rebuilding an identical image or +depending on an existing service as the image source. -Deploy it with: +## Clustered dynamic deployment ```bash -gcloud/gateway/deploy.sh --project PROJECT_ID +gcloud/cluster/deploy.sh --project PROJECT_ID ``` -Use [gateway deployment](gateway/README.md) for the deployment details. +Pass `--image-uri IMAGE@sha256:DIGEST` to reuse an existing immutable image +for both the Cloud Run server and GKE workers instead of building another one. + +The default GKE cluster and public Cloud Run service are both named +`select-ai-a2a-cluster` in their respective resource namespaces. The server uses direct VPC egress to +reach Consul and the GKE workers. The worker transport is private HTTP by +default; optional mTLS protects the server-to-worker hop. Wallet-based Oracle +Database mTLS is not yet supported for dynamic sessions. + +See the [cluster deployment guide](cluster/README.md). ## Client-visible differences -| Client concern | Standalone server | Dynamic gateway | -| --- | --- | --- | -| Database/team selection | Configured by the deployment | Submitted by each user session through A2UI | -| First client operation | Send the database prompt | Send a prompt, submit the connection form, then send the database prompt | -| Credentials | Stored in Secret Manager for the service | Supplied for the temporary session and held by its worker | -| Database mTLS | Supported through the standalone wallet configuration | Not yet supported for gateway database sessions | -| Public service | One Cloud Run A2A service | Cloud Run gateway backed by GKE workers and Consul | -| Agent Card input | `text/plain` | `text/plain` and `application/json+a2ui` | -| Agent Card streaming | `true` | `false` | -| Blocking request | `message/send` waits for the final task result | `message/send` waits for the final task result after the session is connected | -| Streaming response | Supported through A2A streaming methods and SSE | Not available; clients use task polling | -| Asynchronous task | `message/send` with `configuration.blocking: false` | `message/send` with `configuration.blocking: false` | -| Task polling | `tasks/get` until the task reaches a terminal state | `tasks/get` until the task reaches a terminal state | -| Session ownership | Cloud Run service database pool | One child process and async pool per active user session | -| Task/context storage | Oracle Database | Oracle Database, with Consul routing metadata | -| Capacity control | Cloud Run instances and per-instance pool size | Gateway instances, Consul routing, worker replicas, per-session pools, and session TTL | -| Best fit | One known database/team and predictable operations | Multiple databases/teams selected dynamically from one endpoint | - -Both deployments expose the public A2A endpoint at: +| Concern | Fixed standalone | Dynamic standalone | Clustered dynamic | +| --- | --- | --- | --- | +| Topology | Cloud Run | One Cloud Run instance | Cloud Run + Consul + GKE workers | +| Deployment-fixed values | Connection URL, username, password, AI Agent | Any subset of Connection URL and AI Agent | Connection URL and AI Agent | +| A2UI form | None | All connection values not fixed by deployment | Username and password | +| Database runtime | Shared configured pool | Isolated local child process | Isolated worker child process | +| Streaming | Supported | Not currently supported | Not currently supported | +| Task/context storage | Oracle Database | Oracle Database | Oracle Database with Consul routing metadata | +| Scaling | Cloud Run instances and pool size | One Cloud Run instance | Cloud Run instances and worker replicas | + +All three expose: ```text /.well-known/agent-card.json /a2a/jsonrpc/ ``` -Both accept A2A 1.0 method names and the A2A v0.3 compatibility method names. -The gateway client flow is documented in the +They accept A2A 1.0 method names and the A2A v0.3 compatibility method names. +The connection-form client flow is documented in the [dynamic-session samples](../samples/a2a/dynamic/README.md). -## Which deployment should you choose? - -Choose the standalone server when the service owner controls the database -identity and team, wants clients to send prompts immediately, and benefits -from streaming responses. +By default, the private Cloud Run deployments rely on Cloud Run IAM and keep +database sessions separate by A2A conversation. Add `--require-oauth` to +either deployment script when verified human-user ownership is required. In +that mode Gemini Enterprise must be configured with end-user OAuth and sends +its user token in `Authorization`; missing tokens receive HTTP 401. -Choose the gateway when one A2A endpoint must serve users selecting different -Oracle databases or teams, with isolated temporary sessions and worker-based -capacity. +`X-Serverless-Authorization` is separate and automatic: Gemini Enterprise uses +it to invoke the private Cloud Run service, and Cloud Run consumes it before +the request reaches `a2a serve`. diff --git a/gcloud/gateway/README.md b/gcloud/cluster/README.md similarity index 67% rename from gcloud/gateway/README.md rename to gcloud/cluster/README.md index a8814c7..44b1654 100644 --- a/gcloud/gateway/README.md +++ b/gcloud/cluster/README.md @@ -1,8 +1,9 @@ -# Dynamic gateway +# Clustered Select AI A2A deployment -Dynamic gateway mode exposes one public A2A endpoint. Each user dynamically -selects an Oracle database connection and Select AI team through the A2UI -connection form. A session remains available for 15 minutes by default. Set a +Clustered mode exposes one public A2A server. The deployment fixes the Oracle +database DSN and Select AI team; each user supplies a database username and +password through the generated A2UI form. A session remains available for 15 +minutes by default. Set a different lifetime in seconds with `--session-ttl-seconds`; for example, `--session-ttl-seconds 1800` keeps sessions for 30 minutes. @@ -10,7 +11,7 @@ different lifetime in seconds with `--session-ttl-seconds`; for example, ```text ┌──────────────┐ A2A JSON-RPC/HTTP ┌──────────────────┐ ┌────────────────────────────┐ -│ A2A client │────────────────────►│ Gateway instances│──── route lookup/update ──────────► │ Service Registry │ +│ A2A client │────────────────────►│ Server instances │──── route lookup/update ──────────► │ Service Registry │ └──────────────┘ │ Public A2A API │ │ Service discovery │ │ A2UI bootstrap │ │ Session routes │ └────────┬─────────┘ │ Task routes │ @@ -36,7 +37,7 @@ different lifetime in seconds with `--session-ttl-seconds`; for example, └──────────────────┘ ``` -The gateway is the only public A2A application. It selects a worker through +The Cloud Run server is the only public A2A application. It selects a worker through the Service Registry, opens a session there, and proxies subsequent A2A calls using the internal protobuf protocol. The selected worker starts one child runtime for that session. The child owns the database connection, @@ -44,13 +45,13 @@ runtime for that session. The child owns the database connection, The session connection path currently accepts a DSN, username, and password. Oracle Database wallet-based mTLS is not yet supported for these dynamic -sessions. The optional worker mTLS mode below protects the gateway-to-worker +sessions. The optional worker mTLS mode below protects the server-to-worker HTTP connection; it does not provide database mTLS. The Service Registry stores only service-discovery and non-secret session/task-to-worker metadata. Task payloads and context mappings remain in Oracle. Connection-form tasks are response-only bootstrap tasks: they are -created by the gateway before a database session exists and are not persisted +created by the server before a database session exists and are not persisted or routed. ## GCP deployment @@ -64,7 +65,7 @@ The protocol architecture above is implemented on GCP as follows: │ public A2A v ┌────────────────────────────┐ - │ Cloud Run gateway │ + │ Cloud Run A2A server │ │ A2A proxy + form bootstrap │ └──────┬───────────┬─────────┘ │ │ private VPC: mTLS request to worker hostname @@ -88,15 +89,68 @@ The protocol architecture above is implemented on GCP as follows: Run this from the repository root: ```bash -gcloud/gateway/deploy.sh --project PROJECT_ID +gcloud/cluster/deploy.sh --project PROJECT_ID ``` +That command builds a new image. To reuse the newest existing Select AI image +from Artifact Registry, resolve its immutable digest URI first: + +```bash +PROJECT_ID=PROJECT_ID +REGION=us-central1 +REPOSITORY=select-ai +IMAGE_NAME=select-ai + +IMAGE_RECORD="$(gcloud artifacts docker images list \ + "$REGION-docker.pkg.dev/$PROJECT_ID/$REPOSITORY/$IMAGE_NAME" \ + --project "$PROJECT_ID" \ + --include-tags \ + --sort-by='~UPDATE_TIME' \ + --limit=1 \ + --format='csv[no-heading](package,version)')" + +if [[ -z "$IMAGE_RECORD" ]]; then + echo "No Select AI image exists in Artifact Registry." >&2 + exit 1 +fi + +IMAGE_URI="${IMAGE_RECORD/,/@}" + +gcloud/cluster/deploy.sh \ + --project "$PROJECT_ID" \ + --region "$REGION" \ + --repository "$REPOSITORY" \ + --a2a-team ORACLE_AI_DATABASE_AGENT \ + --db-dsn-secret select-ai-a2a-cluster-db-connect-string \ + --image-uri "$IMAGE_URI" +``` + +The first deployment prompts for the Connection URL, creates the GKE Autopilot +cluster and service-specific secret, and uses the same pinned image for the +GKE workers and Cloud Run server. Add `--require-oauth` to the final command +only when Gemini Enterprise end-user OAuth is configured for this clustered +agent. + +By default, Cloud Run IAM authenticates Gemini Enterprise and database +sessions are separated by A2A conversation; the server does not verify the +human user who owns a conversation. Add `--require-oauth` when the deployment +requires authenticated end-user ownership. Gemini Enterprise must then be +configured with end-user OAuth and every request must carry its separate +`Authorization: Bearer ...` token. Requests without it receive HTTP 401. + +On the first deployment, the script prompts for the fixed database DSN and +stores it in Secret Manager. Override the default secret name with +`--db-dsn-secret` to reuse an existing DSN secret. The team defaults to +`ORACLE_AI_DATABASE_AGENT` and can be changed with `--a2a-team`. Because DSN +and team are fixed by deployment, the generated A2UI form asks each user for +only username and password. + The script creates the Artifact Registry repository and GKE Autopilot cluster when they do not already exist. The cluster is created with GKE additive VPC DNS: GKE owns the worker DNS records and keeps them current when a worker Pod is recreated. Cloud Build then: -1. builds the existing `docker/Dockerfile` image once; +1. builds and publishes `docker/Dockerfile`, or reuses `--image-uri`; 2. deploys the GKE namespace and internal Consul service; 3. deploys the requested number of GKE worker replicas using `select-ai a2a worker`; @@ -107,43 +161,42 @@ when a worker Pod is recreated. Cloud Build then: Common options: ```bash -gcloud/gateway/deploy.sh \ +gcloud/cluster/deploy.sh \ --project PROJECT_ID \ --region us-central1 \ - --cluster select-ai-a2a-gateway \ - --gke-dns-domain select-ai-a2a-gateway.internal \ + --cluster select-ai-a2a-cluster \ + --gke-dns-domain select-ai-a2a-cluster.internal \ --worker-replicas 3 \ --network default \ --subnet default ``` -The Cloud Run gateway uses direct VPC egress to reach the internal Consul load -balancer and GKE worker pod addresses. The gateway keeps only the connection +The Cloud Run server uses direct VPC egress to reach the internal Consul load +balancer and GKE worker pod addresses. The server keeps only the connection form task transiently, before a database session exists. Connected task and context state is stored in Oracle on the selected worker. Workers, rather -than the gateway, provide the clustered capacity for dynamic sessions. -The deployment currently keeps one gateway instance as an operational default; -the gateway does not cache forms or connected task/context state. Gateway +than the public server, provide the clustered capacity for dynamic sessions. +The public server does not cache forms or connected task/context state. Server scaling does not change session affinity because Consul stores the session and task routes. `cloudbuild.yaml` is the complete build and deployment workflow. It supplies -the generated image and Consul endpoint values to the Cloud Run gateway at -deployment time. +the selected image and Consul endpoint values to both the GKE workers and the +Cloud Run server at deployment time. ## Optional worker mTLS test mode Local testing does not use mTLS. The default GCloud deployment also keeps the current private-VPC HTTP worker transport. -This mTLS mode applies only between the Cloud Run gateway and GKE workers. It +This mTLS mode applies only between the Cloud Run server and GKE workers. It is independent of Oracle Database authentication, and does not enable wallet- -based database mTLS for gateway sessions. +based database mTLS for dynamic sessions. For a short-lived GCloud mTLS test: ```bash -gcloud/gateway/deploy.sh \ +gcloud/cluster/deploy.sh \ --project PROJECT_ID \ --enable-worker-mtls \ --mtls-cert-validity-days 365 @@ -157,7 +210,7 @@ first stored in Google Secret Manager. Cloud Build then creates the Kubernetes Secrets used by the workers. Set `--mtls-cert-validity-days DAYS` to choose the lifetime for both leaf -certificates: the gateway client certificate and the worker server certificate. +certificates: the server client certificate and the worker server certificate. The CA is issued for one additional day. The first mTLS deployment creates these certificates. Later mTLS deployments @@ -165,10 +218,10 @@ reuse them, including when changing `--worker-replicas`. To deliberately replace the CA and both leaf certificates, add `--rotate-worker-mtls`. Rotation recreates the worker StatefulSet and ends active worker sessions. -Workers run as a StatefulSet. The `select-ai-worker` headless Service gives +Workers run as a StatefulSet. The `select-ai-a2a-worker` headless Service gives each worker a stable name, for example -`select-ai-worker-0.select-ai-worker.select-ai-gateway.svc.select-ai-a2a-gateway.internal`. -Consul registers that name, so the gateway reaches the exact worker that owns a +`select-ai-a2a-worker-0.select-ai-a2a-worker.select-ai-a2a.svc.select-ai-a2a-cluster.internal`. +Consul registers that name, so the server reaches the exact worker that owns a session. GKE Cloud DNS updates its Pod-IP record automatically after a worker is recreated. There is no worker load balancer, custom Cloud DNS zone, or deployment-time Pod-IP snapshot. @@ -193,12 +246,13 @@ GKE gives a StatefulSet Pod a DNS name using this form: For this deployment, worker 0 is: ```text -select-ai-worker-0.select-ai-worker.select-ai-gateway.svc.select-ai-a2a-gateway.internal +select-ai-a2a-worker-0.select-ai-a2a-worker.select-ai-a2a.svc.select-ai-a2a-cluster.internal ``` -`select-ai-worker-0` is the StatefulSet Pod name, `select-ai-worker` is the -headless Service, `select-ai-gateway` is the Kubernetes namespace, and -`select-ai-a2a-gateway.internal` is the `--gke-dns-domain` value. GKE updates +`select-ai-a2a-worker-0` is the StatefulSet Pod name, +`select-ai-a2a-worker` is the headless Service, `select-ai-a2a` is the +Kubernetes namespace, and `select-ai-a2a-cluster.internal` is the +`--gke-dns-domain` value. GKE updates the resulting record when the Pod IP changes. ### Certificate mounts @@ -207,11 +261,11 @@ Worker certificate files are mounted from Kubernetes Secrets: | Container file | Kubernetes Secret | Secret key | Used for | | --- | --- | --- | --- | -| `/var/run/select-ai-mtls/tls.crt` | `select-ai-worker-server-tls` | `tls.crt` | worker HTTPS server certificate | -| `/var/run/select-ai-mtls/tls.key` | `select-ai-worker-server-tls` | `tls.key` | worker HTTPS private key | -| `/var/run/select-ai-mtls/gateway-ca.crt` | `select-ai-gateway-client-ca` | `ca.crt` | validates the gateway client certificate | +| `/var/run/select-ai-mtls/tls.crt` | `select-ai-a2a-worker-server-tls` | `tls.crt` | worker HTTPS server certificate | +| `/var/run/select-ai-mtls/tls.key` | `select-ai-a2a-worker-server-tls` | `tls.key` | worker HTTPS private key | +| `/var/run/select-ai-mtls/server-ca.crt` | `select-ai-a2a-server-client-ca` | `ca.crt` | validates the server client certificate | -The Cloud Run gateway certificate files are mounted from Google Secret Manager. +The Cloud Run server certificate files are mounted from Google Secret Manager. The identity that submits Cloud Build needs permission to use GKE, Cloud Run, and Secret Manager. GKE maintains the managed worker DNS records. diff --git a/gcloud/gateway/cloudbuild.yaml b/gcloud/cluster/cloudbuild.yaml similarity index 57% rename from gcloud/gateway/cloudbuild.yaml rename to gcloud/cluster/cloudbuild.yaml index 42a19b4..ad8d9aa 100644 --- a/gcloud/gateway/cloudbuild.yaml +++ b/gcloud/cluster/cloudbuild.yaml @@ -1,27 +1,25 @@ -# Build one Select AI image, deploy Consul and workers to GKE, then deploy the -# public dynamic gateway to Cloud Run. +# Build or reuse one Select AI image, deploy Consul and workers to GKE, then +# deploy the public clustered server to Cloud Run. steps: - name: gcr.io/cloud-builders/docker + entrypoint: bash args: - - build - - --file - - docker/Dockerfile - - --tag - - ${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/select-ai:${_IMAGE_TAG} - - . - - # GKE and Cloud Run consume the image during this build, so publish it before - # deploying either workload. The top-level images: field publishes only after - # every build step succeeds, which is too late for the worker rollout. - - name: gcr.io/cloud-builders/docker - args: - - push - - ${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/select-ai:${_IMAGE_TAG} + - -ceu + - | + if [[ -n "${_IMAGE_URI}" ]]; then + echo "Reusing existing image: ${_IMAGE_URI}" + exit 0 + fi + image="${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/select-ai:${_IMAGE_TAG}" + docker build --file docker/Dockerfile --tag "$image" . + # GKE and Cloud Run consume the image in later steps, so publish it + # before deploying either workload. + docker push "$image" - name: gcr.io/cloud-builders/gke-deploy args: - run - - --filename=gcloud/gateway/gke/namespace.yaml + - --filename=gcloud/cluster/gke/namespace.yaml - --location=${_REGION} - --cluster=${_CLUSTER} @@ -36,7 +34,7 @@ steps: - name: gcr.io/cloud-builders/gke-deploy args: - run - - --filename=gcloud/gateway/gke/worker-service.yaml + - --filename=gcloud/cluster/gke/worker-service.yaml - --location=${_REGION} - --cluster=${_CLUSTER} @@ -57,12 +55,12 @@ steps: - -ceu - | if [[ "${_ENABLE_WORKER_MTLS}" == "true" ]]; then - kubectl delete --namespace select-ai-gateway deployment/select-ai-worker --ignore-not-found + kubectl delete --namespace select-ai-a2a deployment/select-ai-a2a-worker --ignore-not-found if [[ "${_ROTATE_WORKER_MTLS}" == "true" ]]; then - kubectl delete --namespace select-ai-gateway statefulset/select-ai-worker --ignore-not-found --wait=true + kubectl delete --namespace select-ai-a2a statefulset/select-ai-a2a-worker --ignore-not-found --wait=true fi else - kubectl delete --namespace select-ai-gateway statefulset/select-ai-worker --ignore-not-found + kubectl delete --namespace select-ai-a2a statefulset/select-ai-a2a-worker --ignore-not-found fi - name: gcr.io/cloud-builders/gcloud @@ -73,9 +71,9 @@ steps: if [[ "${_ENABLE_WORKER_MTLS}" != "true" ]]; then exit 0 fi - gcloud secrets versions access latest --secret=select-ai-worker-mtls-cert > /workspace/worker.crt - gcloud secrets versions access latest --secret=select-ai-worker-mtls-key > /workspace/worker.key - gcloud secrets versions access latest --secret=select-ai-gateway-mtls-ca > /workspace/gateway-ca.crt + gcloud secrets versions access latest --secret=select-ai-a2a-worker-mtls-cert > /workspace/worker.crt + gcloud secrets versions access latest --secret=select-ai-a2a-worker-mtls-key > /workspace/worker.key + gcloud secrets versions access latest --secret=select-ai-a2a-mtls-ca > /workspace/server-ca.crt - name: gcr.io/cloud-builders/kubectl entrypoint: bash @@ -88,17 +86,17 @@ steps: if [[ "${_ENABLE_WORKER_MTLS}" != "true" ]]; then exit 0 fi - kubectl -n select-ai-gateway create secret tls select-ai-worker-server-tls \ + kubectl -n select-ai-a2a create secret tls select-ai-a2a-worker-server-tls \ --cert=/workspace/worker.crt --key=/workspace/worker.key \ --dry-run=client -o yaml | kubectl apply -f - - kubectl -n select-ai-gateway create secret generic select-ai-gateway-client-ca \ - --from-file=ca.crt=/workspace/gateway-ca.crt \ + kubectl -n select-ai-a2a create secret generic select-ai-a2a-server-client-ca \ + --from-file=ca.crt=/workspace/server-ca.crt \ --dry-run=client -o yaml | kubectl apply -f - - name: gcr.io/cloud-builders/gke-deploy args: - run - - --filename=gcloud/gateway/gke/consul.yaml + - --filename=gcloud/cluster/gke/consul.yaml - --location=${_REGION} - --cluster=${_CLUSTER} @@ -110,10 +108,10 @@ steps: args: - -ceu - | - kubectl wait --namespace select-ai-gateway \ + kubectl wait --namespace select-ai-a2a \ --for=jsonpath='{.status.loadBalancer.ingress[0].ip}' \ service/consul-server --timeout=300s - kubectl get --namespace select-ai-gateway service/consul-server \ + kubectl get --namespace select-ai-a2a service/consul-server \ -o jsonpath='{.status.loadBalancer.ingress[0].ip}' \ > /workspace/consul-ip @@ -122,10 +120,13 @@ steps: args: - -ceu - | - image="${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/select-ai:${_IMAGE_TAG}" - worker_manifest=gcloud/gateway/gke/worker.yaml + image="${_IMAGE_URI}" + if [[ -z "$image" ]]; then + image="${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/select-ai:${_IMAGE_TAG}" + fi + worker_manifest=gcloud/cluster/gke/worker.yaml if [[ "${_ENABLE_WORKER_MTLS}" == "true" ]]; then - worker_manifest=gcloud/gateway/gke/worker-mtls.yaml + worker_manifest=gcloud/cluster/gke/worker-mtls.yaml fi sed -e "s|WORKER_IMAGE|$image|" \ -e "s|WORKER_REPLICAS|${_WORKER_REPLICAS}|" \ @@ -155,9 +156,9 @@ steps: - -ceu - | if [[ "${_ENABLE_WORKER_MTLS}" == "true" ]]; then - kubectl rollout status --namespace select-ai-gateway statefulset/select-ai-worker --timeout=300s + kubectl rollout status --namespace select-ai-a2a statefulset/select-ai-a2a-worker --timeout=300s else - kubectl rollout status --namespace select-ai-gateway deployment/select-ai-worker --timeout=300s + kubectl rollout status --namespace select-ai-a2a deployment/select-ai-a2a-worker --timeout=300s fi - name: gcr.io/cloud-builders/gcloud @@ -166,51 +167,64 @@ steps: - -ceu - | consul_ip=$(cat /workspace/consul-ip) - image="${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/select-ai:${_IMAGE_TAG}" - mtls_args=() + image="${_IMAGE_URI}" + if [[ -z "$image" ]]; then + image="${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/select-ai:${_IMAGE_TAG}" + fi + secret_mappings=("SELECT_AI_DB_CONNECT_STRING=${_DB_DSN_SECRET}:latest") mtls_env="" if [[ "${_ENABLE_WORKER_MTLS}" == "true" ]]; then - mtls_args=( - "--update-secrets=/var/run/select-ai-mtls-ca/ca.pem=select-ai-gateway-mtls-ca:latest,/var/run/select-ai-mtls-cert/tls.crt=select-ai-gateway-mtls-cert:latest,/var/run/select-ai-mtls-key/tls.key=select-ai-gateway-mtls-key:latest" + secret_mappings+=( + "/var/run/select-ai-mtls-ca/ca.pem=select-ai-a2a-mtls-ca:latest" + "/var/run/select-ai-mtls-cert/tls.crt=select-ai-a2a-server-mtls-cert:latest" + "/var/run/select-ai-mtls-key/tls.key=select-ai-a2a-server-mtls-key:latest" ) mtls_env=",WORKER_TLS_CA_FILE=/var/run/select-ai-mtls-ca/ca.pem,WORKER_TLS_CERT_FILE=/var/run/select-ai-mtls-cert/tls.crt,WORKER_TLS_KEY_FILE=/var/run/select-ai-mtls-key/tls.key" fi - gcloud run deploy "${_GATEWAY_SERVICE}" \ + secret_mappings_csv=$(IFS=,; echo "${secret_mappings[*]}") + oauth_arg="" + if [[ "${_REQUIRE_OAUTH}" == "true" ]]; then + oauth_arg=",--require-oauth" + fi + gcloud run deploy "${_SERVER_SERVICE}" \ --image="$image" \ --region="${_REGION}" \ + --service-account="${_SERVER_RUNTIME_SA}" \ --no-allow-unauthenticated \ --network="${_NETWORK}" \ --subnet="${_SUBNET}" \ --vpc-egress=all-traffic \ - --max-instances=1 \ + --max-instances=20 \ --min-instances=1 \ --port=8080 \ --command=select-ai \ - --args=a2a,serve,--deployment,clustered,--host,0.0.0.0 \ - --set-env-vars="PUBLIC_URL=https://pending.invalid,CONSUL_HTTP_URL=http://$consul_ip:8500,WORKER_SERVICE=select-ai-a2a-worker,SESSION_TTL_SECONDS=${_SESSION_TTL_SECONDS}$mtls_env" \ - "${mtls_args[@]}" - gateway_url=$(gcloud run services describe "${_GATEWAY_SERVICE}" \ + --args=a2a,serve,--deployment,clustered,--host,0.0.0.0$oauth_arg \ + --set-env-vars="PUBLIC_URL=https://pending.invalid,SELECT_AI_A2A_TEAM=${_A2A_TEAM},CONSUL_HTTP_URL=http://$consul_ip:8500,WORKER_SERVICE=select-ai-a2a-worker,SESSION_TTL_SECONDS=${_SESSION_TTL_SECONDS}$mtls_env" \ + --set-secrets="$secret_mappings_csv" + server_url=$(gcloud run services describe "${_SERVER_SERVICE}" \ --region="${_REGION}" --format='value(status.url)') - gcloud run services update "${_GATEWAY_SERVICE}" \ + gcloud run services update "${_SERVER_SERVICE}" \ --region="${_REGION}" \ - --update-env-vars="PUBLIC_URL=$gateway_url" - -images: - - ${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/select-ai:${_IMAGE_TAG} + --update-env-vars="PUBLIC_URL=$server_url" substitutions: _REGION: us-central1 - _CLUSTER: select-ai-a2a-gateway + _CLUSTER: select-ai-a2a-cluster _REPOSITORY: select-ai _IMAGE_TAG: dev - _GATEWAY_SERVICE: select-ai-a2a-gateway + _IMAGE_URI: "" + _SERVER_SERVICE: select-ai-a2a-cluster + _SERVER_RUNTIME_SA: unused@example.invalid + _REQUIRE_OAUTH: "false" + _A2A_TEAM: ORACLE_AI_DATABASE_AGENT + _DB_DSN_SECRET: select-ai-a2a-cluster-db-connect-string _NETWORK: default _SUBNET: default _WORKER_REPLICAS: "2" _SESSION_TTL_SECONDS: "900" _ENABLE_WORKER_MTLS: "false" _ROTATE_WORKER_MTLS: "false" - _GKE_DNS_DOMAIN: select-ai-a2a-gateway.internal + _GKE_DNS_DOMAIN: select-ai-a2a-cluster.internal options: logging: CLOUD_LOGGING_ONLY diff --git a/gcloud/gateway/deploy.sh b/gcloud/cluster/deploy.sh similarity index 67% rename from gcloud/gateway/deploy.sh rename to gcloud/cluster/deploy.sh index 08a93ab..1e6c8cc 100755 --- a/gcloud/gateway/deploy.sh +++ b/gcloud/cluster/deploy.sh @@ -7,32 +7,38 @@ # https://oss.oracle.com/licenses/upl. # ----------------------------------------------------------------------------- -# Build and deploy the dynamic Select AI gateway stack: Cloud Run gateway plus +# Build and deploy clustered Select AI A2A: one public Cloud Run server plus # Consul and worker replicas in GKE. set -euo pipefail usage() { cat <<'EOF' -Usage: gcloud/gateway/deploy.sh [options] +Usage: gcloud/cluster/deploy.sh [options] Options: --project PROJECT Google Cloud project (defaults to gcloud config) --region REGION Region for GKE, Cloud Run, and Artifact Registry (default: us-central1) - --cluster NAME GKE Autopilot cluster name (default: select-ai-a2a-gateway) + --cluster NAME GKE Autopilot cluster name (default: select-ai-a2a-cluster) --gke-dns-domain DOMAIN Unique GKE additive VPC DNS domain - (default: select-ai-a2a-gateway.internal) + (default: select-ai-a2a-cluster.internal) --repository NAME Artifact Registry Docker repository (default: select-ai) - --gateway-service NAME Cloud Run gateway service name (default: select-ai-a2a-gateway) + --service NAME Cloud Run service name (default: select-ai-a2a-cluster) + --require-oauth Require end-user OAuth bearer authentication + --a2a-team TEAM Database AI team fixed by deployment + (default: ORACLE_AI_DATABASE_AGENT) + --db-dsn-secret NAME Secret containing the fixed database DSN + --rotate-db-dsn Prompt for and rotate the database DSN --network NAME VPC network for Cloud Run direct VPC egress (default: default) --subnet NAME VPC subnet for Cloud Run direct VPC egress (default: default) --worker-replicas COUNT GKE worker replica count (default: 2) --session-ttl-seconds N Dynamic session lifetime (default: 900) - --enable-worker-mtls Use ephemeral mTLS certificates for gateway-to-worker calls + --enable-worker-mtls Use ephemeral mTLS certificates for server-to-worker calls --rotate-worker-mtls Replace the existing worker mTLS CA and certificates --mtls-cert-validity-days DAYS - Gateway and worker certificate lifetime (default: 365) + Server and worker certificate lifetime (default: 365) + --image-uri URI Reuse this image for the server and workers --image-tag TAG Image tag (default: git SHA plus UTC timestamp) -h, --help Show this help EOF @@ -41,10 +47,14 @@ EOF repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" project_id="" region="us-central1" -cluster="select-ai-a2a-gateway" -gke_dns_domain="select-ai-a2a-gateway.internal" +cluster="select-ai-a2a-cluster" +gke_dns_domain="select-ai-a2a-cluster.internal" repository="select-ai" -gateway_service="select-ai-a2a-gateway" +server_service="select-ai-a2a-cluster" +require_oauth="false" +a2a_team="ORACLE_AI_DATABASE_AGENT" +db_dsn_secret="" +rotate_db_dsn="false" network="default" subnet="default" worker_replicas="2" @@ -53,6 +63,7 @@ enable_worker_mtls="false" rotate_worker_mtls="false" mtls_cert_validity_days="365" image_tag="" +image_uri="" while [[ $# -gt 0 ]]; do case "$1" in @@ -61,7 +72,11 @@ while [[ $# -gt 0 ]]; do --cluster) cluster="${2:?--cluster requires a value}"; shift 2 ;; --gke-dns-domain) gke_dns_domain="${2:?--gke-dns-domain requires a value}"; shift 2 ;; --repository) repository="${2:?--repository requires a value}"; shift 2 ;; - --gateway-service) gateway_service="${2:?--gateway-service requires a value}"; shift 2 ;; + --service) server_service="${2:?--service requires a value}"; shift 2 ;; + --require-oauth) require_oauth="true"; shift ;; + --a2a-team) a2a_team="${2:?--a2a-team requires a value}"; shift 2 ;; + --db-dsn-secret) db_dsn_secret="${2:?--db-dsn-secret requires a value}"; shift 2 ;; + --rotate-db-dsn) rotate_db_dsn="true"; shift ;; --network) network="${2:?--network requires a value}"; shift 2 ;; --subnet) subnet="${2:?--subnet requires a value}"; shift 2 ;; --worker-replicas) worker_replicas="${2:?--worker-replicas requires a value}"; shift 2 ;; @@ -76,6 +91,7 @@ while [[ $# -gt 0 ]]; do shift 2 ;; --image-tag) image_tag="${2:?--image-tag requires a value}"; shift 2 ;; + --image-uri) image_uri="${2:?--image-uri requires a value}"; shift 2 ;; -h|--help) usage; exit 0 ;; *) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;; esac @@ -88,6 +104,7 @@ if [[ -z "$project_id" || "$project_id" == "(unset)" ]]; then echo "Pass --project or configure one with: gcloud config set project PROJECT_ID" >&2 exit 2 fi +db_dsn_secret="${db_dsn_secret:-${server_service}-db-connect-string}" if ! [[ "$worker_replicas" =~ ^[1-9][0-9]*$ ]]; then echo "--worker-replicas must be a positive integer." >&2 exit 2 @@ -108,8 +125,14 @@ if ! [[ "$mtls_cert_validity_days" =~ ^[1-9][0-9]*$ ]]; then echo "--mtls-cert-validity-days must be a positive integer." >&2 exit 2 fi +if [[ -n "$image_uri" && -n "$image_tag" ]]; then + echo "--image-uri and --image-tag cannot be used together." >&2 + exit 2 +fi -image_tag="${image_tag:-$(git -C "$repo_root" rev-parse --short HEAD)-$(date -u +%Y%m%d%H%M%S)}" +if [[ -z "$image_uri" ]]; then + image_tag="${image_tag:-$(git -C "$repo_root" rev-parse --short HEAD)-$(date -u +%Y%m%d%H%M%S)}" +fi gcloud services enable \ artifactregistry.googleapis.com \ @@ -128,6 +151,30 @@ if ! gcloud artifacts repositories describe "$repository" \ --repository-format=docker --location="$region" --project="$project_id" fi +project_number="$(gcloud projects describe "$project_id" \ + --format='value(projectNumber)')" +runtime_sa="${project_number}-compute@developer.gserviceaccount.com" + +if [[ "$rotate_db_dsn" == "true" ]] \ + || ! gcloud secrets describe "$db_dsn_secret" \ + --project="$project_id" >/dev/null 2>&1; then + read -r -p "ADB connect descriptor: " db_dsn + trap 'unset db_dsn' EXIT + if gcloud secrets describe "$db_dsn_secret" \ + --project="$project_id" >/dev/null 2>&1; then + printf %s "$db_dsn" | gcloud secrets versions add "$db_dsn_secret" \ + --project="$project_id" --data-file=- >/dev/null + else + printf %s "$db_dsn" | gcloud secrets create "$db_dsn_secret" \ + --project="$project_id" --replication-policy=automatic \ + --data-file=- >/dev/null + fi +fi +gcloud secrets add-iam-policy-binding "$db_dsn_secret" \ + --project="$project_id" \ + --member="serviceAccount:$runtime_sa" \ + --role="roles/secretmanager.secretAccessor" >/dev/null + if ! gcloud container clusters describe "$cluster" \ --location="$region" --project="$project_id" >/dev/null 2>&1; then gcloud container clusters create-auto "$cluster" \ @@ -144,22 +191,19 @@ else fi fi -project_number="$(gcloud projects describe "$project_id" \ - --format='value(projectNumber)')" - cleanup_mtls() { [[ -n "${mtls_dir:-}" ]] && rm -rf "$mtls_dir" } if [[ "$enable_worker_mtls" == "true" ]]; then create_mtls_material="$rotate_worker_mtls" - worker_certificate_dns_name="*.select-ai-worker.select-ai-gateway.svc.$gke_dns_domain" + worker_certificate_dns_name="*.select-ai-a2a-worker.select-ai-a2a.svc.$gke_dns_domain" for secret_name in \ - select-ai-gateway-mtls-ca \ - select-ai-gateway-mtls-cert \ - select-ai-gateway-mtls-key \ - select-ai-worker-mtls-cert \ - select-ai-worker-mtls-key; do + select-ai-a2a-mtls-ca \ + select-ai-a2a-server-mtls-cert \ + select-ai-a2a-server-mtls-key \ + select-ai-a2a-worker-mtls-cert \ + select-ai-a2a-worker-mtls-key; do if ! gcloud secrets versions access latest --secret="$secret_name" \ --project="$project_id" >/dev/null 2>&1; then create_mtls_material="true" @@ -170,7 +214,7 @@ if [[ "$enable_worker_mtls" == "true" ]]; then # Read the portable text representation and perform a literal SAN check. existing_worker_certificate_text="$( gcloud secrets versions access latest \ - --secret=select-ai-worker-mtls-cert --project="$project_id" 2>/dev/null | \ + --secret=select-ai-a2a-worker-mtls-cert --project="$project_id" 2>/dev/null | \ openssl x509 -noout -text 2>/dev/null || true )" if ! printf '%s\n' "$existing_worker_certificate_text" | \ @@ -190,37 +234,37 @@ if [[ "$enable_worker_mtls" == "true" ]]; then -subj "/CN=select-ai ephemeral worker CA" >/dev/null 2>&1 openssl req -newkey rsa:2048 -nodes \ -keyout "$mtls_dir/worker.key" -out "$mtls_dir/worker.csr" \ - -subj "/CN=select-ai-worker" >/dev/null 2>&1 + -subj "/CN=select-ai-a2a-worker" >/dev/null 2>&1 printf 'subjectAltName=DNS:%s\nextendedKeyUsage=serverAuth\n' "$worker_certificate_dns_name" \ > "$mtls_dir/worker.ext" openssl x509 -req -days "$mtls_cert_validity_days" -in "$mtls_dir/worker.csr" \ -CA "$mtls_dir/ca.crt" -CAkey "$mtls_dir/ca.key" -CAcreateserial \ -out "$mtls_dir/worker.crt" -extfile "$mtls_dir/worker.ext" >/dev/null 2>&1 openssl req -newkey rsa:2048 -nodes \ - -keyout "$mtls_dir/gateway.key" -out "$mtls_dir/gateway.csr" \ - -subj "/CN=select-ai-gateway" >/dev/null 2>&1 - printf 'extendedKeyUsage=clientAuth\n' > "$mtls_dir/gateway.ext" - openssl x509 -req -days "$mtls_cert_validity_days" -in "$mtls_dir/gateway.csr" \ + -keyout "$mtls_dir/server.key" -out "$mtls_dir/server.csr" \ + -subj "/CN=select-ai-a2a-server" >/dev/null 2>&1 + printf 'extendedKeyUsage=clientAuth\n' > "$mtls_dir/server.ext" + openssl x509 -req -days "$mtls_cert_validity_days" -in "$mtls_dir/server.csr" \ -CA "$mtls_dir/ca.crt" -CAkey "$mtls_dir/ca.key" -CAcreateserial \ - -out "$mtls_dir/gateway.crt" -extfile "$mtls_dir/gateway.ext" >/dev/null 2>&1 + -out "$mtls_dir/server.crt" -extfile "$mtls_dir/server.ext" >/dev/null 2>&1 for secret_name in \ - select-ai-gateway-mtls-ca \ - select-ai-gateway-mtls-cert \ - select-ai-gateway-mtls-key \ - select-ai-worker-mtls-cert \ - select-ai-worker-mtls-key; do + select-ai-a2a-mtls-ca \ + select-ai-a2a-server-mtls-cert \ + select-ai-a2a-server-mtls-key \ + select-ai-a2a-worker-mtls-cert \ + select-ai-a2a-worker-mtls-key; do gcloud secrets describe "$secret_name" --project="$project_id" >/dev/null 2>&1 || \ gcloud secrets create "$secret_name" --replication-policy=automatic --project="$project_id" done - gcloud secrets versions add select-ai-gateway-mtls-ca \ + gcloud secrets versions add select-ai-a2a-mtls-ca \ --data-file="$mtls_dir/ca.crt" --project="$project_id" - gcloud secrets versions add select-ai-gateway-mtls-cert \ - --data-file="$mtls_dir/gateway.crt" --project="$project_id" - gcloud secrets versions add select-ai-gateway-mtls-key \ - --data-file="$mtls_dir/gateway.key" --project="$project_id" - gcloud secrets versions add select-ai-worker-mtls-cert \ + gcloud secrets versions add select-ai-a2a-server-mtls-cert \ + --data-file="$mtls_dir/server.crt" --project="$project_id" + gcloud secrets versions add select-ai-a2a-server-mtls-key \ + --data-file="$mtls_dir/server.key" --project="$project_id" + gcloud secrets versions add select-ai-a2a-worker-mtls-cert \ --data-file="$mtls_dir/worker.crt" --project="$project_id" - gcloud secrets versions add select-ai-worker-mtls-key \ + gcloud secrets versions add select-ai-a2a-worker-mtls-key \ --data-file="$mtls_dir/worker.key" --project="$project_id" if [[ "$rotate_worker_mtls" == "true" ]]; then echo "Rotating worker mTLS material; worker sessions will be interrupted." @@ -230,13 +274,12 @@ if [[ "$enable_worker_mtls" == "true" ]]; then else echo "Reusing existing worker mTLS material." fi - runtime_sa="${project_number}-compute@developer.gserviceaccount.com" for secret_name in \ - select-ai-gateway-mtls-ca \ - select-ai-gateway-mtls-cert \ - select-ai-gateway-mtls-key \ - select-ai-worker-mtls-cert \ - select-ai-worker-mtls-key; do + select-ai-a2a-mtls-ca \ + select-ai-a2a-server-mtls-cert \ + select-ai-a2a-server-mtls-key \ + select-ai-a2a-worker-mtls-cert \ + select-ai-a2a-worker-mtls-key; do gcloud secrets add-iam-policy-binding "$secret_name" --project="$project_id" \ --member="serviceAccount:$runtime_sa" --role="roles/secretmanager.secretAccessor" >/dev/null done @@ -247,7 +290,12 @@ build_substitutions=( "_CLUSTER=$cluster" "_REPOSITORY=$repository" "_IMAGE_TAG=$image_tag" - "_GATEWAY_SERVICE=$gateway_service" + "_IMAGE_URI=$image_uri" + "_SERVER_SERVICE=$server_service" + "_SERVER_RUNTIME_SA=$runtime_sa" + "_REQUIRE_OAUTH=$require_oauth" + "_A2A_TEAM=$a2a_team" + "_DB_DSN_SECRET=$db_dsn_secret" "_NETWORK=$network" "_SUBNET=$subnet" "_WORKER_REPLICAS=$worker_replicas" @@ -260,14 +308,14 @@ build_substitutions=( gcloud builds submit "$repo_root" \ --project="$project_id" \ --region="$region" \ - --config="$repo_root/gcloud/gateway/cloudbuild.yaml" \ + --config="$repo_root/gcloud/cluster/cloudbuild.yaml" \ --substitutions="$(IFS=,; printf '%s' "${build_substitutions[*]}")" -gateway_url="$(gcloud run services describe "$gateway_service" \ +server_url="$(gcloud run services describe "$server_service" \ --region="$region" --project="$project_id" --format='value(status.url)')" gemini_service_agent="service-$project_number@gcp-sa-discoveryengine.iam.gserviceaccount.com" -gcloud run services add-iam-policy-binding "$gateway_service" \ +gcloud run services add-iam-policy-binding "$server_service" \ --region="$region" --project="$project_id" \ --member="serviceAccount:$gemini_service_agent" \ --role="roles/run.invoker" >/dev/null @@ -283,8 +331,8 @@ if gcloud iam service-accounts describe "$active_account" \ else deployer_member="user:$active_account" fi -gcloud run services add-iam-policy-binding "$gateway_service" \ +gcloud run services add-iam-policy-binding "$server_service" \ --region="$region" --project="$project_id" \ --member="$deployer_member" --role="roles/run.invoker" >/dev/null -printf 'Gateway Agent Card URL:\n%s/.well-known/agent-card.json\n' "$gateway_url" +printf 'Clustered Agent Card URL:\n%s/.well-known/agent-card.json\n' "$server_url" diff --git a/gcloud/gateway/gke/consul.yaml b/gcloud/cluster/gke/consul.yaml similarity index 94% rename from gcloud/gateway/gke/consul.yaml rename to gcloud/cluster/gke/consul.yaml index 19569c0..796f1bb 100644 --- a/gcloud/gateway/gke/consul.yaml +++ b/gcloud/cluster/gke/consul.yaml @@ -2,7 +2,7 @@ apiVersion: apps/v1 kind: Deployment metadata: name: consul-server - namespace: select-ai-gateway + namespace: select-ai-a2a spec: replicas: 1 selector: @@ -37,7 +37,7 @@ apiVersion: v1 kind: Service metadata: name: consul-server - namespace: select-ai-gateway + namespace: select-ai-a2a annotations: networking.gke.io/load-balancer-type: Internal spec: diff --git a/gcloud/gateway/gke/namespace.yaml b/gcloud/cluster/gke/namespace.yaml similarity index 61% rename from gcloud/gateway/gke/namespace.yaml rename to gcloud/cluster/gke/namespace.yaml index dc1ff53..5209539 100644 --- a/gcloud/gateway/gke/namespace.yaml +++ b/gcloud/cluster/gke/namespace.yaml @@ -1,4 +1,4 @@ apiVersion: v1 kind: Namespace metadata: - name: select-ai-gateway + name: select-ai-a2a diff --git a/gcloud/gateway/gke/worker-mtls.yaml b/gcloud/cluster/gke/worker-mtls.yaml similarity index 74% rename from gcloud/gateway/gke/worker-mtls.yaml rename to gcloud/cluster/gke/worker-mtls.yaml index 624ad8e..3c4dc93 100644 --- a/gcloud/gateway/gke/worker-mtls.yaml +++ b/gcloud/cluster/gke/worker-mtls.yaml @@ -1,22 +1,22 @@ apiVersion: apps/v1 kind: StatefulSet metadata: - name: select-ai-worker - namespace: select-ai-gateway + name: select-ai-a2a-worker + namespace: select-ai-a2a spec: - serviceName: select-ai-worker + serviceName: select-ai-a2a-worker replicas: WORKER_REPLICAS selector: matchLabels: - app: select-ai-worker + app: select-ai-a2a-worker template: metadata: labels: - app: select-ai-worker + app: select-ai-a2a-worker spec: terminationGracePeriodSeconds: 30 containers: - - name: worker + - name: select-ai-a2a-worker image: WORKER_IMAGE command: [select-ai] args: @@ -27,11 +27,11 @@ spec: - --port - "8443" - --consul-url - - http://consul-server.select-ai-gateway.svc.cluster.local:8500 + - http://consul-server.select-ai-a2a.svc.cluster.local:8500 - --worker-id - $(WORKER_ID) - --worker-endpoint - - https://$(POD_NAME).select-ai-worker.select-ai-gateway.svc.GKE_DNS_DOMAIN:8443 + - https://$(POD_NAME).select-ai-a2a-worker.select-ai-a2a.svc.GKE_DNS_DOMAIN:8443 - --session-ttl-seconds - "SESSION_TTL_SECONDS" - --tls-cert-file @@ -39,7 +39,7 @@ spec: - --tls-key-file - /var/run/select-ai-mtls/tls.key - --tls-ca-file - - /var/run/select-ai-mtls/gateway-ca.crt + - /var/run/select-ai-mtls/server-ca.crt ports: - name: https containerPort: 8443 @@ -65,8 +65,8 @@ spec: mountPath: /var/run/select-ai-mtls/tls.key subPath: tls.key readOnly: true - - name: gateway-client-ca - mountPath: /var/run/select-ai-mtls/gateway-ca.crt + - name: server-client-ca + mountPath: /var/run/select-ai-mtls/server-ca.crt subPath: ca.crt readOnly: true readinessProbe: @@ -78,7 +78,7 @@ spec: volumes: - name: worker-server-tls secret: - secretName: select-ai-worker-server-tls - - name: gateway-client-ca + secretName: select-ai-a2a-worker-server-tls + - name: server-client-ca secret: - secretName: select-ai-gateway-client-ca + secretName: select-ai-a2a-server-client-ca diff --git a/gcloud/gateway/gke/worker-service.yaml b/gcloud/cluster/gke/worker-service.yaml similarity index 80% rename from gcloud/gateway/gke/worker-service.yaml rename to gcloud/cluster/gke/worker-service.yaml index 103fe2a..2a56722 100644 --- a/gcloud/gateway/gke/worker-service.yaml +++ b/gcloud/cluster/gke/worker-service.yaml @@ -1,14 +1,14 @@ apiVersion: v1 kind: Service metadata: - name: select-ai-worker - namespace: select-ai-gateway + name: select-ai-a2a-worker + namespace: select-ai-a2a spec: # This is service discovery only: it allocates no load balancer and no # virtual Service IP. Cloud DNS for GKE publishes the current Pod IPs. clusterIP: None selector: - app: select-ai-worker + app: select-ai-a2a-worker ports: - name: http port: 8080 diff --git a/gcloud/gateway/gke/worker.yaml b/gcloud/cluster/gke/worker.yaml similarity index 78% rename from gcloud/gateway/gke/worker.yaml rename to gcloud/cluster/gke/worker.yaml index 1fb2481..8f22b97 100644 --- a/gcloud/gateway/gke/worker.yaml +++ b/gcloud/cluster/gke/worker.yaml @@ -1,21 +1,21 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: select-ai-worker - namespace: select-ai-gateway + name: select-ai-a2a-worker + namespace: select-ai-a2a spec: replicas: WORKER_REPLICAS selector: matchLabels: - app: select-ai-worker + app: select-ai-a2a-worker template: metadata: labels: - app: select-ai-worker + app: select-ai-a2a-worker spec: terminationGracePeriodSeconds: 30 containers: - - name: worker + - name: select-ai-a2a-worker image: WORKER_IMAGE command: - select-ai @@ -27,9 +27,11 @@ spec: - --port - "8080" - --consul-url - - http://consul-server.select-ai-gateway.svc.cluster.local:8500 + - http://consul-server.select-ai-a2a.svc.cluster.local:8500 - --worker-id - $(WORKER_ID) + - --worker-endpoint + - http://$(WORKER_ADDRESS):8080 - --session-ttl-seconds - "SESSION_TTL_SECONDS" ports: diff --git a/gcloud/standalone/README.md b/gcloud/standalone/README.md index 5c2df66..ecb685d 100644 --- a/gcloud/standalone/README.md +++ b/gcloud/standalone/README.md @@ -65,27 +65,157 @@ gcloud services enable \ --project PROJECT_ID ``` -## Deploy (and update) the A2A server +## Deploy dynamic standalone ```bash -gcloud/standalone/deploy.sh --build +gcloud/standalone/deploy.sh \ + --connection-mode dynamic \ + --build ``` -On the first deployment, the script prompts for the ADB user, password, and -connect descriptor. It stores them in Secret Manager under names based on the -Cloud Run service, and grants only the runtime service account access. The -container receives the values as `SELECT_AI_USER`, `SELECT_AI_PASSWORD`, and -`SELECT_AI_DB_CONNECT_STRING`; they are never placed in the image or source -tree. +With no database options, the deployment fixes neither the Connection URL nor +the AI Agent. The generated A2UI form asks each A2A conversation for Connection +URL, username, password, and AI Agent. The resulting database session runs in +an isolated child process and remains bound to the conversation until it +expires. With `--require-oauth`, that binding also includes the authenticated +owner. Dynamic standalone is restricted to one Cloud Run instance because its +session routing is in memory. + +Use `--db-dsn-secret NAME` to fix the Connection URL from an existing Secret +Manager secret, and `--a2a-team TEAM` to fix the AI Agent. The generated form +contains only the values that are not fixed. Use `--rotate-db-config` to prompt +for a Connection URL and create or rotate the service's default DSN secret. + +| Dynamic deployment options | Generated A2UI fields | +| --- | --- | +| Neither option | Connection URL, Database username, Database password, AI Agent | +| `--db-dsn-secret` only | Database username, Database password, AI Agent | +| `--a2a-team` only | Connection URL, Database username, Database password | +| Both options | Database username, Database password | + +### Fix the Connection URL and AI Agent + +The following command updates the default dynamic service so every A2A +conversation supplies only its database username and password. If the named +secret does not exist, the script prompts for the Connection URL and creates +it. Later invocations reuse the secret without prompting. + +```bash +gcloud/standalone/deploy.sh \ + --project PROJECT_ID \ + --connection-mode dynamic \ + --a2a-team ORACLE_AI_DATABASE_AGENT \ + --db-dsn-secret select-ai-a2a-standalone-dynamic-db-connect-string +``` + +Add `--rotate-db-config` only when the secret already exists and its Connection +URL must be replaced. + +## Choose whether end-user OAuth is required + +By default, the service does not require an application OAuth token. Cloud Run +IAM still allows only authorized callers such as Gemini Enterprise to invoke +the private service. Each A2A conversation gets a separate database session, +but the server does not know or verify which human user owns that conversation. + +To require verified end-user ownership, add `--require-oauth`: + +```bash +gcloud/standalone/deploy.sh \ + --connection-mode dynamic \ + --require-oauth \ + --build +``` + +In this mode every A2A request must include `Authorization: Bearer ...` and +the session is scoped by both the authenticated owner and the conversation. +When registering the agent, configure Gemini Enterprise end-user OAuth rather +than selecting **Skip & Finish**. Gemini Enterprise automatically sends its +Cloud Run invocation identity in `X-Serverless-Authorization`; end-user OAuth +adds the separate `Authorization` header consumed by `a2a serve`. + +The server derives a stable owner from an ID token's `iss` and `sub` claims. +It also accepts opaque OAuth access tokens without storing or logging them, +but a refreshed opaque token starts a new owner/session scope. See +[Register and manage A2A agents](https://docs.cloud.google.com/gemini/enterprise/docs/register-and-manage-an-a2a-agent#configure-authentication-and-authorization) +for the OAuth registration steps. + +### Deploy a separate OAuth-enabled service from an existing image + +A new Cloud Run service cannot inherit an image from another service. Resolve +the most recently updated Select AI image directly from Artifact Registry and +construct an immutable digest URI: + +```bash +PROJECT_ID=PROJECT_ID +REGION=us-central1 +REPOSITORY=select-ai +IMAGE_NAME=select-ai + +IMAGE_RECORD="$(gcloud artifacts docker images list \ + "$REGION-docker.pkg.dev/$PROJECT_ID/$REPOSITORY/$IMAGE_NAME" \ + --project "$PROJECT_ID" \ + --include-tags \ + --sort-by='~UPDATE_TIME' \ + --limit=1 \ + --format='csv[no-heading](package,version)')" + +if [[ -z "$IMAGE_RECORD" ]]; then + echo "No Select AI image exists in Artifact Registry." >&2 + exit 1 +fi + +IMAGE_URI="${IMAGE_RECORD/,/@}" +echo "$IMAGE_URI" +``` + +Then create a separate service that fixes the Connection URL and AI Agent and +requires end-user OAuth: + +```bash +gcloud/standalone/deploy.sh \ + --project "$PROJECT_ID" \ + --region "$REGION" \ + --repository "$REPOSITORY" \ + --connection-mode dynamic \ + --service select-ai-a2a-standalone-oauth \ + --a2a-team ORACLE_AI_DATABASE_AGENT \ + --db-dsn-secret select-ai-a2a-standalone-oauth-db-connect-string \ + --require-oauth \ + --image-uri "$IMAGE_URI" +``` + +On its first invocation, the deployment script prompts for the Connection URL +and creates the service-specific secret. The generated A2UI form contains only +database username and password. Register this service in Gemini Enterprise +with end-user OAuth; selecting **Skip & Finish** causes its requests to fail +with HTTP 401. + +## Deploy fixed standalone + +```bash +gcloud/standalone/deploy.sh \ + --connection-mode fixed \ + --a2a-team ORACLE_AI_DATABASE_AGENT \ + --build +``` + +Fixed mode prompts for the ADB connect descriptor, username, and password. It +mounts all three from Secret Manager and creates the existing shared database +pool at server startup. The generated A2UI connection form is skipped. ### Optional: Autonomous Database mTLS wallet The Select AI SDK already supports `wallet_location` and `wallet_password`. -For Cloud Run, pass the path to the downloaded Autonomous Database wallet ZIP -on the first deployment (or when replacing it): +Wallet configuration is supported only in fixed mode. For Cloud Run, pass the +path to the downloaded Autonomous Database wallet ZIP on the first deployment +(or when replacing it): ```bash -gcloud/standalone/deploy.sh --wallet-archive /path/to/Wallet_database.zip +gcloud/standalone/deploy.sh \ + --connection-mode fixed \ + --a2a-team ORACLE_AI_DATABASE_AGENT \ + --wallet-archive /path/to/Wallet_database.zip ``` The script prompts for the wallet password, stores the ZIP and password as @@ -104,21 +234,26 @@ deploys private Cloud Run, sets the final public URL in the Agent Card, grants y gcloud identity and Gemini Enterprise Discovery Engine service agent the `run.routes.invoke` permission for this Cloud Run service. -The default Cloud Run service is `oracle-a2a-agent`. Its default Agent Team, -installed in Oracle Database, is `ORACLE_AI_DATABASE_AGENT`. Override either +The default Cloud Run services are `select-ai-a2a-standalone-dynamic` and +`select-ai-a2a-standalone-fixed`, selected by `--connection-mode`. Dynamic mode +has no default AI Agent. Fixed mode requires `--a2a-team` so all four +connection values are complete at startup. Fix either optional dynamic value with explicit options: ```bash -gcloud/standalone/deploy.sh --service sales-analyst-a2a --a2a-team SALES_ANALYST +gcloud/standalone/deploy.sh \ + --connection-mode dynamic \ + --service sales-analyst-a2a \ + --a2a-team SALES_ANALYST ``` -Use a distinct `--service` value for each A2A team. Each service gets distinct Secret -Manager secret names by default, so credentials remain attached to that A2A -server. +Use a distinct `--service` value for each fixed deployment profile. Each +service gets distinct Secret Manager secret names by default, so credentials +remain attached to that A2A server. -`--max-instances` controls the number of Cloud Run containers. Each container -can use up to 10 Oracle connections by default; change that limit with -`--pool-max-size`, for example `gcloud/standalone/deploy.sh --pool-max-size 20`. +`--max-instances` controls the number of Cloud Run containers in fixed mode. +Dynamic mode requires exactly one instance. A fixed-mode container can use up +to 10 Oracle connections by default; change that limit with `--pool-max-size`. ### Update the Select AI SDK or this repository @@ -127,20 +262,24 @@ and deploy the new image: ```bash git pull -gcloud/standalone/deploy.sh --build +gcloud/standalone/deploy.sh --connection-mode dynamic --build ``` `--build` creates a freshly tagged image from the current source; without it, -the existing image is reused. Existing database secrets are reused without -prompting. To rotate the ADB credentials, explicitly request it: +the existing image is reused. Existing explicitly selected database secrets +are reused without prompting. To configure or rotate the dynamic deployment's +Connection URL, explicitly request it: ```bash -gcloud/standalone/deploy.sh --rotate-db-credentials +gcloud/standalone/deploy.sh \ + --connection-mode dynamic \ + --rotate-db-config ``` ### What `cloudbuild.yaml` does -`gcloud/standalone/deploy.sh --build` uses `gcloud/standalone/cloudbuild.yaml` to tell Cloud Build to build +`gcloud/standalone/deploy.sh --connection-mode MODE --build` uses +`gcloud/standalone/cloudbuild.yaml` to tell Cloud Build to build `docker/Dockerfile` and push it to Artifact Registry. It is build configuration, not a command you run. The build context is the repository root, so the image can install the Select AI source from `pyproject.toml` and `src/`. diff --git a/gcloud/standalone/cloudbuild.yaml b/gcloud/standalone/cloudbuild.yaml index 3cedbda..213f0fc 100644 --- a/gcloud/standalone/cloudbuild.yaml +++ b/gcloud/standalone/cloudbuild.yaml @@ -5,7 +5,7 @@ # http://oss.oracle.com/licenses/upl. # ----------------------------------------------------------------------------- -# Build one reusable Select AI image for standalone servers, the gateway, and +# Build one reusable Select AI image for standalone and clustered servers and # GKE worker replicas. Runtime role selection is deployment configuration. steps: - name: gcr.io/cloud-builders/docker diff --git a/gcloud/standalone/deploy.sh b/gcloud/standalone/deploy.sh index 3ac7ff1..e964cfc 100755 --- a/gcloud/standalone/deploy.sh +++ b/gcloud/standalone/deploy.sh @@ -23,13 +23,15 @@ Options: --project PROJECT Google Cloud project (defaults to gcloud config project) --region REGION Cloud Run and Artifact Registry region (default: us-central1) --repository REPOSITORY Docker repository name (default: select-ai) - --service SERVICE Cloud Run service name (default: oracle-a2a-agent) - --a2a-team TEAM Agent Team installed in Oracle Database (default: ORACLE_AI_DATABASE_AGENT) + --connection-mode MODE Required: fixed or dynamic + --require-oauth Require end-user OAuth bearer authentication + --service SERVICE Cloud Run service name (defaults from connection mode) + --a2a-team TEAM Fix the AI Agent in the deployment --runtime-sa EMAIL Runtime service-account email --runtime-sa-name NAME Default runtime service-account name (default: oracle-a2a-runtime) - --db-user-secret NAME Secret name for the ADB user - --db-password-secret NAME Secret name for the ADB password - --db-dsn-secret NAME Secret name for the ADB connect descriptor + --db-user-secret NAME Secret name for the fixed-mode ADB user + --db-password-secret NAME Secret name for the fixed-mode ADB password + --db-dsn-secret NAME Fix the Connection URL using this secret --wallet-secret NAME Secret name for the wallet archive --wallet-password-secret NAME Secret name for the wallet password --wallet-archive PATH Wallet ZIP to upload or replace @@ -40,7 +42,7 @@ Options: --image-uri URI Deploy this container image --build Build the current checkout before deploying --image-tag TAG Tag for --build (default: git SHA plus UTC timestamp) - --rotate-db-credentials Prompt for and rotate ADB credentials + --rotate-db-config Prompt for and rotate the configured database values -h, --help Show this help EOF } @@ -49,14 +51,17 @@ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" project_id="" region="us-central1" repository="select-ai" -service="oracle-a2a-agent" -a2a_team="ORACLE_AI_DATABASE_AGENT" +connection_mode="" +require_oauth=false +service="" +a2a_team="" runtime_sa="" runtime_sa_name="oracle-a2a-runtime" runtime_sa_explicit=false db_user_secret="" db_password_secret="" db_dsn_secret="" +db_dsn_secret_explicit=false wallet_secret="" wallet_password_secret="" wallet_archive="" @@ -67,20 +72,22 @@ pool_max_size="10" image_uri="" image_tag="" build_image=false -rotate_db_credentials=false +rotate_db_config=false while [[ $# -gt 0 ]]; do case "$1" in --project) project_id="${2:?--project requires a value}"; shift 2 ;; --region) region="${2:?--region requires a value}"; shift 2 ;; --repository) repository="${2:?--repository requires a value}"; shift 2 ;; + --connection-mode) connection_mode="${2:?--connection-mode requires a value}"; shift 2 ;; + --require-oauth) require_oauth=true; shift ;; --service) service="${2:?--service requires a value}"; shift 2 ;; --a2a-team) a2a_team="${2:?--a2a-team requires a value}"; shift 2 ;; --runtime-sa) runtime_sa="${2:?--runtime-sa requires a value}"; runtime_sa_explicit=true; shift 2 ;; --runtime-sa-name) runtime_sa_name="${2:?--runtime-sa-name requires a value}"; shift 2 ;; --db-user-secret) db_user_secret="${2:?--db-user-secret requires a value}"; shift 2 ;; --db-password-secret) db_password_secret="${2:?--db-password-secret requires a value}"; shift 2 ;; - --db-dsn-secret) db_dsn_secret="${2:?--db-dsn-secret requires a value}"; shift 2 ;; + --db-dsn-secret) db_dsn_secret="${2:?--db-dsn-secret requires a value}"; db_dsn_secret_explicit=true; shift 2 ;; --wallet-secret) wallet_secret="${2:?--wallet-secret requires a value}"; shift 2 ;; --wallet-password-secret) wallet_password_secret="${2:?--wallet-password-secret requires a value}"; shift 2 ;; --wallet-archive) wallet_archive="${2:?--wallet-archive requires a value}"; shift 2 ;; @@ -91,7 +98,7 @@ while [[ $# -gt 0 ]]; do --image-uri) image_uri="${2:?--image-uri requires a value}"; shift 2 ;; --build) build_image=true; shift ;; --image-tag) image_tag="${2:?--image-tag requires a value}"; shift 2 ;; - --rotate-db-credentials) rotate_db_credentials=true; shift ;; + --rotate-db-config) rotate_db_config=true; shift ;; -h|--help) usage; exit 0 ;; *) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;; esac @@ -106,10 +113,56 @@ if [[ -z "$project_id" || "$project_id" == "(unset)" ]]; then exit 1 fi +case "$connection_mode" in + fixed) + service="${service:-select-ai-a2a-standalone-fixed}" + if [[ -z "$a2a_team" ]]; then + echo "Fixed standalone requires --a2a-team." >&2 + exit 2 + fi + ;; + dynamic) + service="${service:-select-ai-a2a-standalone-dynamic}" + if [[ "$max_instances" != "1" ]]; then + echo "Dynamic standalone requires --max-instances 1 because session routing is in memory." >&2 + exit 2 + fi + if [[ -n "$wallet_archive" ]]; then + echo "Dynamic standalone does not currently support --wallet-archive." >&2 + exit 2 + fi + ;; + *) + echo "--connection-mode must be fixed or dynamic." >&2 + exit 2 + ;; +esac + +form_summary="" +if [[ "$connection_mode" == "dynamic" ]]; then + append_form_field() { + if [[ -n "$form_summary" ]]; then + form_summary+=", " + fi + form_summary+="$1" + } + if [[ -z "$db_dsn_secret" && "$rotate_db_config" == false ]]; then + append_form_field "Connection URL" + fi + append_form_field "Database username" + append_form_field "Database password" + if [[ -z "$a2a_team" ]]; then + append_form_field "AI Agent" + fi +fi +form_summary="${form_summary:-none}" + runtime_sa="${runtime_sa:-${runtime_sa_name}@${project_id}.iam.gserviceaccount.com}" db_user_secret="${db_user_secret:-${service}-db-user}" db_password_secret="${db_password_secret:-${service}-db-password}" -db_dsn_secret="${db_dsn_secret:-${service}-db-connect-string}" +if [[ "$connection_mode" == "fixed" || "$db_dsn_secret_explicit" == true || "$rotate_db_config" == true ]]; then + db_dsn_secret="${db_dsn_secret:-${service}-db-connect-string}" +fi wallet_secret="${wallet_secret:-${service}-wallet}" wallet_password_secret="${wallet_password_secret:-${service}-wallet-password}" @@ -150,23 +203,30 @@ if gcloud run services describe "$service" --project="$project_id" --region="$re fi create_or_rotate_secrets=false -if [[ "$rotate_db_credentials" == true ]]; then +if [[ "$rotate_db_config" == true ]]; then create_or_rotate_secrets=true -else +elif [[ "$connection_mode" == "fixed" ]]; then for secret in "$db_user_secret" "$db_password_secret" "$db_dsn_secret"; do if ! gcloud secrets describe "$secret" --project="$project_id" >/dev/null 2>&1; then create_or_rotate_secrets=true break fi done +elif [[ -n "$db_dsn_secret" ]] \ + && ! gcloud secrets describe "$db_dsn_secret" --project="$project_id" >/dev/null 2>&1; then + create_or_rotate_secrets=true fi if [[ "$create_or_rotate_secrets" == true ]]; then - echo "Creating or rotating ADB credentials for Cloud Run service: $service" - read -r -p "ADB user: " db_user - read -r -s -p "ADB password: " db_password - echo + echo "Creating or rotating database configuration for Cloud Run service: $service" read -r -p "ADB connect descriptor: " db_dsn + db_user="" + db_password="" + if [[ "$connection_mode" == "fixed" ]]; then + read -r -p "ADB user: " db_user + read -r -s -p "ADB password: " db_password + echo + fi trap 'unset db_user db_password db_dsn' EXIT add_secret() { @@ -177,20 +237,33 @@ if [[ "$create_or_rotate_secrets" == true ]]; then else printf %s "$value" | gcloud secrets create "$name" --project="$project_id" --replication-policy=automatic --data-file=- >/dev/null fi - gcloud secrets add-iam-policy-binding "$name" --project="$project_id" \ - --member="serviceAccount:$runtime_sa" --role="roles/secretmanager.secretAccessor" >/dev/null } - add_secret "$db_user_secret" "$db_user" - add_secret "$db_password_secret" "$db_password" add_secret "$db_dsn_secret" "$db_dsn" + if [[ "$connection_mode" == "fixed" ]]; then + add_secret "$db_user_secret" "$db_user" + add_secret "$db_password_secret" "$db_password" + fi +fi + +if [[ "$connection_mode" == "fixed" ]]; then + for secret in "$db_user_secret" "$db_password_secret" "$db_dsn_secret"; do + gcloud secrets add-iam-policy-binding "$secret" --project="$project_id" \ + --member="serviceAccount:$runtime_sa" \ + --role="roles/secretmanager.secretAccessor" >/dev/null + done +elif [[ -n "$db_dsn_secret" ]]; then + gcloud secrets add-iam-policy-binding "$db_dsn_secret" --project="$project_id" \ + --member="serviceAccount:$runtime_sa" \ + --role="roles/secretmanager.secretAccessor" >/dev/null fi # An Oracle mTLS wallet is a ZIP archive containing several files, so it is # mounted as a Secret Manager volume rather than exposed as an environment # variable. Pass --wallet-archive to enable or replace this optional configuration. wallet_enabled=false -if gcloud secrets describe "$wallet_secret" --project="$project_id" >/dev/null 2>&1 \ +if [[ "$connection_mode" == "fixed" ]] \ + && gcloud secrets describe "$wallet_secret" --project="$project_id" >/dev/null 2>&1 \ && gcloud secrets describe "$wallet_password_secret" --project="$project_id" >/dev/null 2>&1; then wallet_enabled=true fi @@ -239,25 +312,44 @@ fi # Cloud Run needs a URL before the server can construct its Agent Card. Deploy # once with a placeholder, then update PUBLIC_URL with the assigned URL. -secret_mappings=( - "SELECT_AI_USER=$db_user_secret:latest" - "SELECT_AI_PASSWORD=$db_password_secret:latest" - "SELECT_AI_DB_CONNECT_STRING=$db_dsn_secret:latest" -) +secret_mappings_csv="" +append_secret_mapping() { + if [[ -n "$secret_mappings_csv" ]]; then + secret_mappings_csv+="," + fi + secret_mappings_csv+="$1" +} +if [[ -n "$db_dsn_secret" ]]; then + append_secret_mapping "SELECT_AI_DB_CONNECT_STRING=$db_dsn_secret:latest" +fi +if [[ "$connection_mode" == "fixed" ]]; then + append_secret_mapping "SELECT_AI_USER=$db_user_secret:latest" + append_secret_mapping "SELECT_AI_PASSWORD=$db_password_secret:latest" +fi if [[ "$wallet_enabled" == true ]]; then - secret_mappings+=( - "/var/run/secrets/select-ai-wallet/wallet.zip=$wallet_secret:latest" - "SELECT_AI_WALLET_PASSWORD=$wallet_password_secret:latest" - ) + append_secret_mapping "/var/run/secrets/select-ai-wallet/wallet.zip=$wallet_secret:latest" + append_secret_mapping "SELECT_AI_WALLET_PASSWORD=$wallet_password_secret:latest" +fi +secret_option="--clear-secrets" +if [[ -n "$secret_mappings_csv" ]]; then + secret_option="--set-secrets=$secret_mappings_csv" +fi + +env_vars_csv="PUBLIC_URL=https://pending.invalid,SELECT_AI_POOL_MAX_SIZE=$pool_max_size" +if [[ -n "$a2a_team" ]]; then + env_vars_csv+=",SELECT_AI_A2A_TEAM=$a2a_team" +fi +if [[ "$require_oauth" == true ]]; then + env_vars_csv+=",SELECT_AI_A2A_REQUIRE_OAUTH=true" fi -secret_mappings_csv="$(IFS=,; echo "${secret_mappings[*]}")" gcloud run deploy "$service" --image="$image_uri" --project="$project_id" --region="$region" \ --service-account="$runtime_sa" --no-allow-unauthenticated --port=8080 \ --command="/app/docker/a2a-entrypoint.sh" \ - --memory="$memory" --timeout="$timeout" --max-instances="$max_instances" \ - --set-env-vars="SELECT_AI_A2A_TEAM=$a2a_team,PUBLIC_URL=https://pending.invalid,SELECT_AI_POOL_MAX_SIZE=$pool_max_size" \ - --update-secrets="$secret_mappings_csv" + --memory="$memory" --timeout="$timeout" --min-instances=1 \ + --max-instances="$max_instances" \ + --set-env-vars="$env_vars_csv" \ + "$secret_option" service_url="$(gcloud run services describe "$service" --project="$project_id" --region="$region" --format='value(status.url)')" gcloud run services update "$service" --project="$project_id" --region="$region" --update-env-vars="PUBLIC_URL=$service_url" @@ -301,7 +393,26 @@ Deployment complete. Cloud Run URL: $service_url Gemini Enterprise invoker: $gemini_sa +Connection mode: $connection_mode +End-user OAuth required: $require_oauth +A2UI connection fields: $form_summary + +Agent Card endpoint: + $service_url/.well-known/agent-card.json -Paste this A2A v0.3 Agent Card into Gemini Enterprise: - select-ai a2a agent-card --team "$a2a_team" --public-url "$service_url" EOF +if [[ "$require_oauth" == true ]]; then + cat <<'EOF' + +Gemini Enterprise must be configured with end-user OAuth so it sends an +Authorization bearer token in addition to its automatic +X-Serverless-Authorization Cloud Run identity token. +EOF +else + cat <<'EOF' + +End-user OAuth is not required. Cloud Run IAM authenticates Gemini Enterprise, +and database sessions are separated by A2A conversation rather than by a +verified human-user identity. +EOF +fi diff --git a/samples/a2a/dynamic/README.md b/samples/a2a/dynamic/README.md index 34eca84..7a52e1f 100644 --- a/samples/a2a/dynamic/README.md +++ b/samples/a2a/dynamic/README.md @@ -51,7 +51,6 @@ env -u SELECT_AI_USER -u SELECT_AI_PASSWORD \ --host 127.0.0.1 \ --port 8000 \ --public-url http://127.0.0.1:8000 \ - --allow-unauthenticated \ --dsn '' \ --team ORACLE_AI_DATABASE_AGENT ``` @@ -93,7 +92,6 @@ select-ai a2a serve \ --host 127.0.0.1 \ --port 8000 \ --public-url http://127.0.0.1:8000 \ - --allow-unauthenticated \ --consul-url http://127.0.0.1:8500 ``` @@ -112,11 +110,11 @@ For a TNS-alias DSN, set `TNS_ADMIN` in the worker terminal before starting the clustered worker, or in the standalone server terminal. Wallet-based Oracle Database mTLS is not currently supported by the dynamic session path. -To call an authenticated server, set its bearer JWT without changing the -scripts: +To require OAuth, start the server with `--require-oauth`. Then set its bearer +token without changing the scripts: ```bash -export SELECT_AI_A2A_BEARER_TOKEN='' +export SELECT_AI_A2A_BEARER_TOKEN='' ``` Override the default endpoint with `SELECT_AI_A2A_ENDPOINT`. diff --git a/samples/a2a/dynamic/_common.py b/samples/a2a/dynamic/_common.py index 8958698..2d3d9ab 100644 --- a/samples/a2a/dynamic/_common.py +++ b/samples/a2a/dynamic/_common.py @@ -18,10 +18,10 @@ ) TEAM_NAME = os.environ.get("SELECT_AI_A2A_TEAM", "ORACLE_AI_DATABASE_AGENT") _CONNECTION_ENV = { - "dsn": "SELECT_AI_DB_CONNECT_STRING", + "connection_url": "SELECT_AI_DB_CONNECT_STRING", "username": "SELECT_AI_USER", "password": "SELECT_AI_PASSWORD", - "team_name": "SELECT_AI_A2A_TEAM", + "ai_agent": "SELECT_AI_A2A_TEAM", } @@ -68,7 +68,7 @@ def connect(prompt: str) -> str: for field in fields: env_name = _CONNECTION_ENV[field] value = os.environ.get(env_name) - if field == "team_name" and not value: + if field == "ai_agent" and not value: value = TEAM_NAME if not value: raise RuntimeError( diff --git a/src/select_ai/agent/a2a/auth.py b/src/select_ai/agent/a2a/auth.py index 2c97363..44265f7 100644 --- a/src/select_ai/agent/a2a/auth.py +++ b/src/select_ai/agent/a2a/auth.py @@ -32,11 +32,11 @@ class TrustedBearerAuthenticationBackend(AuthenticationBackend): - """Map an upstream-validated bearer JWT's ``sub`` to a Starlette user. + """Map an upstream-validated OAuth bearer token to a Starlette user. Signature and claims validation belongs to the deployment's authenticating - proxy (for example Gemini Enterprise plus Cloud Run IAM). This backend only - establishes the SDK user object from the already trusted token. + proxy (for example Gemini Enterprise). This backend only establishes the + SDK user object from the already trusted token. """ async def authenticate(self, connection): @@ -46,22 +46,20 @@ async def authenticate(self, connection): scheme, separator, token = header.partition(" ") if separator != " " or scheme.lower() != "bearer" or not token: raise AuthenticationError("Authorization must use Bearer syntax.") - owner = _jwt_owner(token) + owner = _bearer_owner(token) return AuthCredentials(["authenticated"]), SimpleUser(owner) class RequireA2AAuthenticationMiddleware: """Require an SDK-visible user on the JSON-RPC endpoint.""" - def __init__(self, app, allow_unauthenticated: bool = False) -> None: + def __init__(self, app) -> None: self.app = app - self.allow_unauthenticated = allow_unauthenticated async def __call__(self, scope, receive, send) -> None: if ( scope["type"] == "http" and scope.get("path", "").rstrip("/") == "/a2a/jsonrpc" - and not self.allow_unauthenticated and not scope["user"].is_authenticated ): response = JSONResponse( @@ -74,10 +72,10 @@ async def __call__(self, scope, receive, send) -> None: await self.app(scope, receive, send) -def authentication_middleware( - allow_unauthenticated: bool, -) -> list[Middleware]: - """Return middleware ordered so authentication precedes enforcement.""" +def authentication_middleware(require_oauth: bool) -> list[Middleware]: + """Enable bearer authentication only when explicitly requested.""" + if not require_oauth: + return [] return [ Middleware( AuthenticationMiddleware, @@ -86,7 +84,6 @@ def authentication_middleware( ), Middleware( RequireA2AAuthenticationMiddleware, - allow_unauthenticated=allow_unauthenticated, ), ] @@ -96,9 +93,11 @@ def add_bearer_security(agent_card: AgentCard) -> None: agent_card.security_schemes["bearer"].CopyFrom( SecurityScheme( http_auth_security_scheme=HTTPAuthSecurityScheme( - description="Upstream-validated user identity token.", + description=( + "Upstream-validated end-user OAuth 2.0 access token or " + "OpenID Connect ID token." + ), scheme="bearer", - bearer_format="JWT", ) ) ) @@ -114,11 +113,20 @@ def _authentication_error(_connection, error): ) -def _jwt_owner(token: str) -> str: - """Derive a stable opaque owner from an upstream-validated JWT.""" +def _bearer_owner(token: str) -> str: + """Derive an opaque owner from a trusted JWT or OAuth access token.""" + jwt_owner = _jwt_owner(token) + if jwt_owner is not None: + return jwt_owner + digest = hashlib.sha256(token.encode("utf-8")).hexdigest() + return f"oauth:{digest}" + + +def _jwt_owner(token: str) -> str | None: + """Return the issuer/subject owner when the bearer value is a JWT.""" parts = token.split(".") if len(parts) != 3: - raise AuthenticationError("Bearer token must be a JWT.") + return None try: encoded = parts[1] + "=" * (-len(parts[1]) % 4) payload = json.loads(base64.urlsafe_b64decode(encoded)) @@ -127,15 +135,13 @@ def _jwt_owner(token: str) -> str: UnicodeDecodeError, ValueError, TypeError, - ) as error: - raise AuthenticationError( - "Bearer token has an invalid payload." - ) from error + ): + return None subject = payload.get("sub") if isinstance(payload, dict) else None issuer = payload.get("iss") if isinstance(payload, dict) else None if not isinstance(subject, str) or not subject: - raise AuthenticationError("Bearer token has no subject.") + return None if not isinstance(issuer, str) or not issuer: - raise AuthenticationError("Bearer token has no issuer.") + return None digest = hashlib.sha256(f"{issuer}\0{subject}".encode("utf-8")).hexdigest() return f"jwt:{digest}" diff --git a/src/select_ai/agent/a2a/embedded.py b/src/select_ai/agent/a2a/embedded.py index f8918a2..105ba7d 100644 --- a/src/select_ai/agent/a2a/embedded.py +++ b/src/select_ai/agent/a2a/embedded.py @@ -287,7 +287,7 @@ async def lifespan(_app): session_client=client, connection=settings.connection, connection_form_template=form_template, - allow_unauthenticated=settings.allow_unauthenticated, + require_oauth=settings.require_oauth, description=settings.description, lifespan=lifespan, ) diff --git a/src/select_ai/agent/a2a/forms.py b/src/select_ai/agent/a2a/forms.py index c05472a..7593d06 100644 --- a/src/select_ai/agent/a2a/forms.py +++ b/src/select_ai/agent/a2a/forms.py @@ -16,14 +16,14 @@ from uuid import uuid4 from select_ai.agent.a2a.a2ui import A2UI_CATALOG_ID, A2UI_VERSION -from select_ai.agent.a2a.models import CONNECTION_FIELDS +from select_ai.agent.a2a.models import CONNECTION_FORM_FIELDS _ACTION_NAME = "submit_database_connection" _FIELD_COMPONENTS = { - "dsn": ("Database DSN", "shortText"), + "connection_url": ("Connection URL", "shortText"), "username": ("Database username", "shortText"), "password": ("Database password", "obscured"), - "team_name": ("Select AI team name", "shortText"), + "ai_agent": ("AI Agent", "shortText"), } @@ -34,13 +34,13 @@ def connection_form( ) -> list[dict]: """Return a fresh form containing only missing connection properties.""" fields = ( - tuple(CONNECTION_FIELDS) + tuple(CONNECTION_FORM_FIELDS) if missing_fields is None else tuple(missing_fields) ) if not fields: return [] - if set(fields) - set(CONNECTION_FIELDS): + if set(fields) - set(CONNECTION_FORM_FIELDS): raise ValueError("Connection form contains unsupported fields.") surface_id = surface_id or f"db-connect-{uuid4().hex}" if template is not None: diff --git a/src/select_ai/agent/a2a/gateway.py b/src/select_ai/agent/a2a/gateway.py index 33ecb7a..a511319 100644 --- a/src/select_ai/agent/a2a/gateway.py +++ b/src/select_ai/agent/a2a/gateway.py @@ -108,13 +108,13 @@ def __init__( agent_card: AgentCard, connection: ConnectionConfig | None = None, connection_form_template: tuple[dict, ...] | None = None, - allow_unauthenticated: bool = False, + require_oauth: bool = False, ): self.worker_client = worker_client self.agent_card = agent_card self.connection = connection or ConnectionConfig() self.connection_form_template = connection_form_template - self.allow_unauthenticated = allow_unauthenticated + self.require_oauth = require_oauth # RequestHandler requires every operation even when the Agent Card does # not advertise streaming or push notifications. @@ -479,12 +479,12 @@ def _form_template(self) -> tuple[dict, ...] | None: return getattr(self, "connection_form_template", None) def _owner(self, context: ServerCallContext) -> str: - """Resolve the SDK owner, allowing one explicit local-only scope.""" + """Resolve an OAuth owner or the conversation-only session scope.""" owner = resolve_user_scope(context) if owner: return owner - if getattr(self, "allow_unauthenticated", True): - return "local-development" + if not getattr(self, "require_oauth", False): + return "a2a-conversation" raise PermissionError("Bearer authentication is required.") @@ -516,7 +516,7 @@ def create_gateway_app(settings: GatewaySettings) -> Starlette: session_client=WorkerClient(settings), connection=settings.connection, connection_form_template=form_template, - allow_unauthenticated=settings.allow_unauthenticated, + require_oauth=settings.require_oauth, description=settings.description, ) @@ -527,7 +527,7 @@ def create_session_app( session_client, connection: ConnectionConfig, connection_form_template: tuple[dict, ...] | None, - allow_unauthenticated: bool, + require_oauth: bool, description: str | None = None, lifespan=None, ) -> Starlette: @@ -570,14 +570,14 @@ def create_session_app( ) ], ) - if not allow_unauthenticated: + if require_oauth: add_bearer_security(card) handler = GatewayRequestHandler( session_client, card, connection, connection_form_template, - allow_unauthenticated, + require_oauth, ) compat_card = to_compat_agent_card(card).model_dump( by_alias=True, @@ -599,6 +599,6 @@ async def get_agent_card(_request): ) return Starlette( routes=routes, - middleware=authentication_middleware(allow_unauthenticated), + middleware=authentication_middleware(require_oauth), lifespan=lifespan, ) diff --git a/src/select_ai/agent/a2a/models.py b/src/select_ai/agent/a2a/models.py index 8a3ca36..57eca76 100644 --- a/src/select_ai/agent/a2a/models.py +++ b/src/select_ai/agent/a2a/models.py @@ -10,6 +10,15 @@ from dataclasses import dataclass, field CONNECTION_FIELDS = ("dsn", "username", "password", "team_name") +CONNECTION_FORM_FIELDS = ( + "connection_url", + "username", + "password", + "ai_agent", +) +FORM_FIELD_TO_CONNECTION_FIELD = dict( + zip(CONNECTION_FORM_FIELDS, CONNECTION_FIELDS) +) @dataclass(frozen=True) @@ -29,30 +38,33 @@ def __post_init__(self) -> None: @property def missing_fields(self) -> tuple[str, ...]: - """Return canonical properties that must come from the form.""" + """Return canonical A2UI properties that must come from the form.""" return tuple( - name for name in CONNECTION_FIELDS if getattr(self, name) is None + form_name + for form_name, connection_name in FORM_FIELD_TO_CONNECTION_FIELD.items() + if getattr(self, connection_name) is None ) def resolve(self, submitted: dict) -> "SessionInfo": """Merge validated submitted values with immutable server values.""" if not isinstance(submitted, dict): raise ValueError("Connection form context must be an object.") - unknown = set(submitted) - set(CONNECTION_FIELDS) + unknown = set(submitted) - set(CONNECTION_FORM_FIELDS) if unknown: raise ValueError("Connection form contains unsupported fields.") configured = { - name - for name in CONNECTION_FIELDS - if getattr(self, name) is not None + form_name + for form_name, connection_name in FORM_FIELD_TO_CONNECTION_FIELD.items() + if getattr(self, connection_name) is not None } if configured.intersection(submitted): raise ValueError( "Configured connection fields cannot be overridden." ) values = { - name: getattr(self, name, None) or submitted.get(name) - for name in CONNECTION_FIELDS + connection_name: getattr(self, connection_name, None) + or submitted.get(form_name) + for form_name, connection_name in FORM_FIELD_TO_CONNECTION_FIELD.items() } return SessionInfo.from_values(values) @@ -101,7 +113,7 @@ class GatewaySettings: description: str | None = None connection: ConnectionConfig = field(default_factory=ConnectionConfig) connection_form_template: tuple[dict, ...] | None = None - allow_unauthenticated: bool = False + require_oauth: bool = False def __post_init__(self) -> None: if self.session_ttl_seconds < 1: @@ -135,7 +147,7 @@ class StandaloneSessionSettings: description: str | None = None connection: ConnectionConfig = field(default_factory=ConnectionConfig) connection_form_template: tuple[dict, ...] | None = None - allow_unauthenticated: bool = False + require_oauth: bool = False def __post_init__(self) -> None: if self.session_ttl_seconds < 1: diff --git a/src/select_ai/agent/a2a/server.py b/src/select_ai/agent/a2a/server.py index ebf2bc1..334c4d3 100644 --- a/src/select_ai/agent/a2a/server.py +++ b/src/select_ai/agent/a2a/server.py @@ -87,14 +87,14 @@ def create_app( # noqa: PLR0913 wallet_password: Optional[str] = None, description: Optional[str] = None, pool_max_size: int = 10, - allow_unauthenticated: bool = False, + require_oauth: bool = False, ) -> Starlette: """Build an A2A JSON-RPC application for one database AI Agent Team.""" if pool_max_size < 1: raise ValueError("pool_max_size must be at least 1") agent_card = _build_agent_card(team_name, public_url, description) - if not allow_unauthenticated: + if require_oauth: add_bearer_security(agent_card) compat_agent_card = _build_v03_agent_card(agent_card) task_store = OracleTaskStore() @@ -148,7 +148,7 @@ async def get_agent_card(request): return Starlette( routes=routes, lifespan=lifespan, - middleware=authentication_middleware(allow_unauthenticated), + middleware=authentication_middleware(require_oauth), ) diff --git a/src/select_ai/cli/a2a.py b/src/select_ai/cli/a2a.py index 84bd964..8e0d622 100644 --- a/src/select_ai/cli/a2a.py +++ b/src/select_ai/cli/a2a.py @@ -5,7 +5,6 @@ # http://oss.oracle.com/licenses/upl. # ----------------------------------------------------------------------------- -import ipaddress import json import os import socket @@ -110,9 +109,13 @@ def a2a(): help="Custom A2UI connection-form JSON file.", ) @click.option( - "--allow-unauthenticated", + "--require-oauth", is_flag=True, - help="Allow unauthenticated requests for local development only.", + envvar="SELECT_AI_A2A_REQUIRE_OAUTH", + help=( + "Require an end-user OAuth bearer token. Without this option, " + "sessions are separated only by the A2A conversation." + ), ) @connection_options def serve( @@ -130,7 +133,7 @@ def serve( worker_tls_cert_file, worker_tls_key_file, a2ui_form, - allow_unauthenticated, + require_oauth, user, password, dsn, @@ -146,11 +149,6 @@ def serve( if public_url is None: public_url = f"http://{host}:{port}" - if allow_unauthenticated and not _is_loopback_host(host): - raise click.ClickException( - "--allow-unauthenticated requires a loopback --host " - "(127.0.0.1, ::1, or localhost)" - ) try: from select_ai.agent.a2a import ( @@ -199,7 +197,7 @@ def serve( wallet_password=wallet_password, description=description, pool_max_size=pool_max_size, - allow_unauthenticated=allow_unauthenticated, + require_oauth=require_oauth, ) elif deployment == "standalone": if os.environ.get("WEB_CONCURRENCY", "1") != "1": @@ -212,7 +210,7 @@ def serve( description=description, connection=connection, connection_form_template=form_template, - allow_unauthenticated=allow_unauthenticated, + require_oauth=require_oauth, ) app = create_embedded_session_app(settings) else: @@ -227,7 +225,7 @@ def serve( description=description, connection=connection, connection_form_template=form_template, - allow_unauthenticated=allow_unauthenticated, + require_oauth=require_oauth, ) app = create_gateway_app(settings) @@ -237,16 +235,6 @@ def serve( uvicorn.run(app, host=host, port=port) -def _is_loopback_host(host: str) -> bool: - """Return whether a bind host is safe for unauthenticated development.""" - if host.lower() == "localhost": - return True - try: - return ipaddress.ip_address(host).is_loopback - except ValueError: - return False - - @a2a.command("worker") @click.option("--host", default="0.0.0.0", show_default=True) @click.option( diff --git a/tests/a2a/test_worker_runtime.py b/tests/a2a/test_worker_runtime.py index 0eab777..4c26b79 100644 --- a/tests/a2a/test_worker_runtime.py +++ b/tests/a2a/test_worker_runtime.py @@ -678,7 +678,7 @@ def test_connection_action_accepts_gemini_unmarked_data_part(): "version": "v0.9", "action": { "name": "submit_database_connection", - "context": {"team_name": "TEAM"}, + "context": {"ai_agent": "TEAM"}, }, } ) @@ -689,7 +689,7 @@ def test_connection_action_accepts_gemini_unmarked_data_part(): assert action == { "name": "submit_database_connection", - "context": {"team_name": "TEAM"}, + "context": {"ai_agent": "TEAM"}, } @@ -706,10 +706,10 @@ def open_session(_owner, _context_id, _session_info): session_id = asyncio.run( handler._open_session( { - "dsn": "database", + "connection_url": "database", "username": "user", "password": "password", - "team_name": "TEAM", + "ai_agent": "TEAM", }, "owner-1", "context-1", @@ -729,7 +729,7 @@ def session_exists(self, _owner, _context_id): return self.opened def open_session(self, owner, context_id, session_info): - assert owner == "local-development" + assert owner == "a2a-conversation" assert context_id == "context-1" assert session_info.team_name == "TEAM" self.opened = True @@ -769,10 +769,10 @@ def open_session(self, owner, context_id, session_info): "action": { "name": "submit_database_connection", "context": { - "dsn": "database", + "connection_url": "database", "username": "user", "password": "password", - "team_name": "TEAM", + "ai_agent": "TEAM", }, }, }

2lOmhaY^o`wKqDO=How8h~s_zZ3bq-!v7>NnPK$Q$VG+;hSLeBGm$Kl=gy6MnX9|FW9!atcGPlQrNw;r2F9?Rkpr~Z0d+y%Anu?$DtIGLj6*b0`#JzkOX9Kr=b_?J|(hb7P# zz?>A^iap)-I%4=g>k(KKYhP{lbejq(OS+7{#++EHI3wS{uBNJ9fkM^Vb@mPSHbHbl z7A*fWmGQ?uJx*n{D+eq@21I zx5dks#jaeu3>dDcbzib}ewxW~VU^-B1y0^cU`8B++lsAzXGKkl!XRV(i;=&!G9B*w#}9M8K-4M$JJz z$;v#3-%D#-Te(13SQXmYh1D+hvRLb-#!FdG|2d_iU8`>b(_z?{*t`#?ftZM<-8t4U zm_L2-f(tPadHI-_mAy8g zm#5p~SXG@v2DrHkZo>waVbpzHmG6B`pJ$96bR`sAKQ12kzu1E0!rKrbH8jHgT-??U z8NbckO19}bW(v(V;c-GH*%|T&#l;)-O$dp=IT zi1j&^hJ8cp0ZR|?k(}(DWBN%$iGj8W<n&Re33drCsT+mvu&~QJ&)KGB=I4vDusHDYlzc7iYiQxW@e|)L zT$CpWkCLM7KXMv)k?F^Xt^OVU^UsgW%m3B4{OeZye^s^fUrzRa7W@Bth1Gwb)qkJW u@g4ZDSmb|HhW{@UN&a^W|39~dk0?W!DCX$u93qeZ#zR>^pYFYU`+oq7+6kio literal 0 HcmV?d00001 diff --git a/doc/source/image/profile_object_model.svg b/doc/source/image/profile_object_model.svg new file mode 100644 index 0000000..345a552 --- /dev/null +++ b/doc/source/image/profile_object_model.svg @@ -0,0 +1,107 @@ + + Select AI Profile object model + Profile and AsyncProfile use ProfileAttributes, which contain a provider and profile configuration. + + + + + + + + + Profile object model + A Profile or AsyncProfile is a database-backed interface to a configured AI provider. + + + Profile / AsyncProfile + Synchronous and asynchronous APIs + + Identity + • profile_name, description, owner + • qualified_name for shared profiles + Lifecycle + create, fetch, list, enable, disable + AI actions + generate, chat, show_sql, run_sql + narrate, summarize, translate + + + ProfileAttributes + Configuration persisted with the profile + + Provider and access + • provider, credential_name + • object_list, object_list_mode + Generation + • max_tokens, temperature, seed + • streaming, stop_tokens, conversation + RAG and translation + • vector_index_name, source_language + • target_language, additional_instructions + + + Provider + Provider-specific connection and model settings + + Common provider fields + • provider_name, model + • embedding_model, provider_endpoint + • region + Concrete providers + + OCI Gen AI + + OpenAI / Azure + + AWS / Google + + Cohere / others + + + has + + uses + + Related concepts + + + Database profile + DBMS_CLOUD_AI profile object + Profile metadata and attributes are + created, fetched, and updated here. + The Python objects are lightweight proxies. + + + Request-level overrides + generate, chat, narrate, SQL helpers + streaming, sessions, and pipelines + Temporary fields include: + • seed, language, additional_instructions + + + Typical flow + Profile → ProfileAttributes → Provider + → Select AI action → database / model + Use AsyncProfile with async_connect() + for asynchronous workflows. + + Use the object model as a map: profiles hold configuration, providers hold model settings, and actions use both. + diff --git a/doc/source/image/vector_index_object_model.png b/doc/source/image/vector_index_object_model.png new file mode 100644 index 0000000000000000000000000000000000000000..7090fe27a9c0f509dbd9b74e7f973b3b080fbc33 GIT binary patch literal 272728 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