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 0000000..bc9da1a Binary files /dev/null and b/doc/source/image/a2a_architecture.png differ 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 0000000..8d45cfe Binary files /dev/null and b/doc/source/image/conversation_object_model.png differ 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 0000000..6caa418 Binary files /dev/null and b/doc/source/image/profile_object_model.png differ 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 0000000..7090fe2 Binary files /dev/null and b/doc/source/image/vector_index_object_model.png differ diff --git a/doc/source/image/vector_index_object_model.svg b/doc/source/image/vector_index_object_model.svg new file mode 100644 index 0000000..a9e47eb --- /dev/null +++ b/doc/source/image/vector_index_object_model.svg @@ -0,0 +1,101 @@ + + 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)