diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 2dd3c6b..4c1ddac 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -50,7 +50,7 @@ jobs: run: | python -m pip install --upgrade pip setuptools pip install pytest anyio - pip install -e ".[cli]" + pip install -e ".[cli,a2a]" - name: Wait for ADB Free Container run: | diff --git a/README.md b/README.md index a1463d2..0cf5bff 100644 --- a/README.md +++ b/README.md @@ -36,11 +36,22 @@ Install the optional command line interface: python3 -m pip install 'select_ai[cli]' ``` -The CLI extra includes A2A server support. +The `cli` extra installs the command line interface. Install the separate +`a2a` extra to enable the A2A server and worker commands: + +```bash +python3 -m pip install 'select_ai[a2a]' +``` + +Without the A2A dependencies, the other `select-ai` commands remain available; +invoking `select-ai a2a` or an A2A server or worker command prints the A2A +installation instructions. ## Documentation -See [Select AI for Python documentation][documentation] +See [Select AI for Python documentation][documentation], the +[complete first-query walkthrough](doc/source/user_guide/quickstart.rst), and +[1.5.0 release notes](doc/source/release_notes.rst). ## Getting Started @@ -119,6 +130,10 @@ directory containing the unzipped wallet and set `SELECT_AI_WALLET_PASSWORD`. The CLI passes both values to the Select AI SDK as `wallet_location` and `wallet_password`. +For a database using a server certificate signed by a private CA, pass the PEM +certificate file with `--database-tls-ca-file` or set +`SELECT_AI_DATABASE_TLS_CA_FILE`. The CLI reads the certificate from the file. + The server accepts both A2A 1.x and the A2A v0.3 JSON-RPC streaming protocol for compatibility with Gemini Enterprise. diff --git a/doc/README.md b/doc/README.md index 9b3d4ce..f33563a 100644 --- a/doc/README.md +++ b/doc/README.md @@ -1,24 +1,31 @@ -Sphinx is used to generate documentation +# Building the documentation -```text -python -m pip install -r requirements.txt -``` - -For more information on Sphinx, please visit this page: - -http://www.sphinx-doc.org +Run these commands from the repository root in your virtual environment: -Once Sphinx is installed, the supplied Makefile can be used to build the -different targets, for example to build the HTML documentation, run:: +```bash +python -m pip install -e . +python -m pip install -r doc/requirements.txt +python -m sphinx -b html -n -W --keep-going doc/source doc/build/html +``` - make +The strict build checks cross-references and treats warnings as errors. +Intersphinx downloads reference inventories from the Python and pandas +documentation sites, so the first build requires network access. -To make ePub documentation, run:: +Open `doc/build/html/index.html` to review the local site. To refresh the +checked-in HTML under `docs/`, use a separate doctree cache: - make epub +```bash +python -m sphinx -b html -n -W --keep-going -d doc/build/doctrees doc/source docs +``` -To make PDF documentation, run:: +The Makefile targets run from `doc/`: - make pdf +```bash +make -C doc html +make -C doc epub +make -C doc pdf +``` -The program ``latexmk`` may be required by Sphinx to generate PDF output. +PDF generation additionally requires a LaTeX installation with `latexmk`. +See [Sphinx](https://www.sphinx-doc.org/) for builder options. diff --git a/doc/requirements.txt b/doc/requirements.txt index 1e15c01..3c0ffe3 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,4 +1,4 @@ -sphinx +sphinx==9.1.0 sphinx-rtd-theme sphinx_toolbox python_docs_theme diff --git a/doc/source/conf.py b/doc/source/conf.py index 07aacd6..cc98cd0 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -11,10 +11,16 @@ import sphinx_prompt sys.modules["sphinx-prompt"] = sphinx_prompt +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -sys.path.insert(0, os.path.join("..", "..", "src", "select_ai")) +sys.path.insert( + 0, + os.path.abspath( + os.path.join(os.path.dirname(__file__), "..", "..", "src") + ), +) -autodocs_default_options = { +autodoc_default_options = { "members": True, "inherited-members": True, "undoc-members": True, @@ -29,12 +35,20 @@ extensions = [ "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", "sphinx_toolbox.latex", + "sphinx_toolbox_latex_compat", "sphinx.ext.autosectionlabel", ] autosectionlabel_prefix_document = True +# Resolve standard-library and DataFrame API types to their own references. +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), + "pandas": ("https://pandas.pydata.org/docs", None), +} + # The suffix of source filenames. source_suffix = ".rst" @@ -47,7 +61,11 @@ global_vars = {} local_vars = {} -version_file_name = os.path.join("..", "..", "src", "select_ai", "version.py") +version_file_name = os.path.abspath( + os.path.join( + os.path.dirname(__file__), "..", "..", "src", "select_ai", "version.py" + ) +) with open(version_file_name) as f: exec(f.read(), global_vars, local_vars) version = ".".join(local_vars["__version__"].split(".")[:2]) @@ -59,7 +77,11 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output html_theme = "python_docs_theme" -html_static_path = ["_static"] +html_static_path = ( + ["_static"] + if os.path.isdir(os.path.join(os.path.dirname(__file__), "_static")) + else [] +) pygments_style = "sphinx" diff --git a/doc/source/index.rst b/doc/source/index.rst index 767ab0f..8241b5d 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -21,6 +21,9 @@ Getting Started user_guide/introduction.rst user_guide/installation.rst user_guide/connection.rst + user_guide/quickstart.rst + user_guide/compatibility.rst + release_notes.rst Actions @@ -187,3 +190,11 @@ Concurrent Prompt Processing :maxdepth: 3 user_guide/concurrent_prompt_processing.rst + +API Reference +============= + +.. toctree:: + :maxdepth: 3 + + user_guide/api_reference.rst diff --git a/doc/source/release_notes.rst b/doc/source/release_notes.rst new file mode 100644 index 0000000..105e805 --- /dev/null +++ b/doc/source/release_notes.rst @@ -0,0 +1,63 @@ +.. _release-1-5-0: + +******************* +What's new in 1.5.0 +******************* + +Version 1.5.0 adds A2A integration, object sharing, request-level profile +overrides, and typed agent history APIs. The changes below are relative to +1.4.1. + +New and expanded APIs +===================== + +* Expose an agent team with the optional :ref:`A2A server `. Choose fixed + or dynamic connections and standalone or clustered deployment. A2A includes + Oracle-backed task/context storage, task polling, A2UI connection forms, and + A2A 1.0/v0.3 JSON-RPC compatibility. +* :ref:`Share profiles, vector indexes, teams, and credentials ` + with database users or roles. Profile and vector-index fetch/list APIs + accept ``owner`` and expose ``qualified_name``. +* Pass an ``attributes`` mapping to profile and session actions for + :ref:`request-only overrides `. Async pipelines + can apply the same override mapping to every operation. +* Inspect agent execution with :ref:`TeamHistory, TaskHistory, and ToolHistory + ` and their async equivalents. +* Enable and disable profiles without deleting them. Configure + ``additional_instructions``, integer ``seed``, and translation language + defaults with ``ProfileAttributes``. Per-call translation languages are + optional and can use the saved defaults. +* Configure supervised teams with ``AgentAttributes.supervisor`` and + ``TeamAttributes.supervisor_agent``; inspect the database-generated + ``supervisor_task`` after creation. + +Upgrade considerations +====================== + +* **Python 3.11 is now the minimum.** Python 3.9 and 3.10 are no longer + supported. Supported versions are 3.11 through 3.14. +* **pandas is now pinned to 3.0.3**, replacing 2.2.3. Check applications that + consume DataFrames for pandas compatibility when upgrading. +* ``ProfileAttributes.seed`` is now typed as an integer. Supply an integer + rather than a numeric string. +* Unspecified ``SyntheticDataParams`` values are omitted so the database can + apply its defaults. Set values explicitly if your application relies on + particular generation settings. +* Install ``select_ai[a2a]`` to use A2A commands; ``select_ai[cli]`` covers + the other CLI commands. Prepare A2A database storage privileges as described + in :ref:`A2A prerequisites `. +* ``--require-oauth`` requires a bearer token and derives an owner scope. + Token signatures and claims must be validated by a trusted upstream service; + the SDK flag alone does not verify identity. See :ref:`A2A authentication + `. + +Upgrade in your application's virtual environment: + +.. code-block:: bash + + python -m pip install --upgrade 'select_ai==1.5.0' + # If you use A2A: + python -m pip install --upgrade 'select_ai[a2a]==1.5.0' + +See :ref:`Compatibility ` for the feature requirements and +:ref:`Run your first SQL query ` for a complete example. diff --git a/doc/source/sphinx_toolbox_latex_compat.py b/doc/source/sphinx_toolbox_latex_compat.py new file mode 100644 index 0000000..38cb290 --- /dev/null +++ b/doc/source/sphinx_toolbox_latex_compat.py @@ -0,0 +1,15 @@ +"""Keep the Sphinx LaTeX support files when sphinx-toolbox writes documents.""" + + +def setup(app): + """Restore the asset-copy step omitted by sphinx-toolbox's LaTeX builder.""" + from sphinx_toolbox.latex import PatchedLaTeXBuilder + + original_write = PatchedLaTeXBuilder.write + + def write_with_assets(self, *args, **kwargs): + original_write(self, *args, **kwargs) + self.copy_assets() + + PatchedLaTeXBuilder.write = write_with_assets + return {"version": "1"} diff --git a/doc/source/user_guide/a2a.rst b/doc/source/user_guide/a2a.rst index f4a83ca..d984f03 100644 --- a/doc/source/user_guide/a2a.rst +++ b/doc/source/user_guide/a2a.rst @@ -9,33 +9,141 @@ Select AI for Python can expose an Oracle Database AI Agent Team through the 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: +Make two independent choices: **where the server runs** (topology) and +**when database connection values are supplied** (fixed or dynamic). +For example, a clustered deployment can use fixed credentials, and a +standalone deployment can ask each client for credentials. -* **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. -* **Clustered A2A server**: the public server asks the client for any database - DSN, username, password, and team values not fixed at deployment time. It - opens a temporary isolated worker session for that selection. This mode - supports task polling, but does not advertise or implement streaming. +.. only:: html + + .. figure:: /image/select_ai_a2a_server_demo.gif + :alt: Select AI A2A server demo from the project README + :width: 100% + + A Select AI agent team accessed through an A2A client. + +Choose a topology +================= + +* **Standalone:** run one public ``select-ai a2a serve`` process. It handles + A2A requests and database work locally. Start here for a single server; + Consul and a separate worker are not required. +* **Clustered:** run a public gateway with + ``select-ai a2a serve --deployment clustered`` and one or more + ``select-ai a2a worker`` processes. Consul lets the gateway discover healthy + workers and route each session back to its worker. Use this when database + sessions need to run across multiple workers. See + :ref:`Why clustered deployments use Consul ` for the role of + this additional service. + +.. _a2a-why-consul: + +Why clustered deployments use Consul +==================================== + +Consul is a separate service that keeps a directory of running workers and +records which worker owns each A2A session. The gateway consults this directory +to answer two questions: "Where can I start a new session?" and "Where should I +send the next request for an existing session?" + +This matters because each database session runs in a child process on one +worker. That process holds the session's database connection and credentials. +Later prompts, task-status requests, and cancellation requests must reach that +same worker. A different worker cannot continue using its live connection. + +Consul provides three things for the gateway: + +* **Worker discovery:** each worker registers its address when it starts. The + gateway can find workers without a manually maintained list of addresses. +* **Worker health:** workers send regular heartbeats to Consul to report that + they are running. The gateway selects a worker whose health check is passing + when opening a new session. +* **Session and task routing:** the gateway records the worker assigned to a + session and the session associated with each task. Subsequent requests can + reach the worker that owns them, even when other workers are available. + +For example, suppose two workers, A and B, are running. The gateway opens your +session on worker A and records that assignment in Consul. Your next prompt +and a request to check your task's status both go to A. Another client's new +session can run on B. If A stops, the gateway cannot move its live database +connection to B; the client must reconnect to open a new session. See +:ref:`Session isolation, routing, and cleanup ` +for expiry and reconnection behavior. + +Consul stores worker addresses and temporary routing records. Database +passwords are not stored in Consul; task results and conversation history are +stored in Oracle Database. See :ref:`Persistent Oracle-backed state ` +for the database storage details. + +**When do you need it?** The current clustered implementation requires Consul, +even with only one worker or fixed database credentials. Standalone deployment +handles sessions locally and does not require Consul. For a first deployment, +use standalone unless you need separate worker processes across machines. + +Choose how connections are configured +===================================== + +Each session needs four values: a database DSN, username, password, and an +existing AI Agent Team name. Configure these on ``a2a serve`` with ``--dsn``, +``--user``, ``--password``, and ``--team``, or their environment fallbacks. + +* **Fixed:** configure all four values before starting the server. Clients + send prompts without completing a connection form. In standalone topology, + the server uses a shared connection pool. In clustered topology, the gateway + automatically opens a session on a worker using the configured values. +* **Dynamic:** leave one or more values unset. The client's first message + receives an A2UI connection form asking for those missing values. After the + client submits the form, the server opens an isolated database session and + the client can send prompts using that A2A context ID. The client must + support the A2UI handshake; see the runnable samples below. + +For example, configure ``--dsn`` and ``--team`` for an application that always +uses one database and one team, but lets each user connect with their own +username and password. The form then asks only for username and password. +Leave all four values unset to let the client choose the database and team +as well. Values configured on the server cannot be changed by the client. + +"Dynamic" describes collecting connection values when a session starts. +Changing a database or user requires a new session. It does not switch the +connection underneath an existing session. + +.. list-table:: Supported combinations + :header-rows: 1 + :widths: 25 37 38 + + * - Topology + - Fixed connection + - Dynamic connection + * - Standalone + - Shared async pool; streaming supported. + - Local child process per session; task polling supported. + * - Clustered + - Worker session opened automatically; task polling supported. + - Worker session opened after form submission; task polling supported. + +Dynamic standalone and all clustered sessions do not support streaming. +Standalone dynamic mode requires one Uvicorn process. Clustered mode requires +Consul and at least one worker even when the connection is fixed. +See :ref:`A2A CLI examples ` for commands and +:ref:`Command Line Interface ` for options. .. only:: html .. figure:: /image/a2a_architecture.svg - :alt: Architecture comparison of the standalone A2A server and dynamic A2A gateway + :alt: Standalone A2A server and clustered gateway-worker architecture :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. + Select AI A2A topologies. Dynamic sessions can run inside the standalone + server or through the clustered gateway and worker processes. .. only:: latex .. figure:: /image/a2a_architecture.png - :alt: Architecture comparison of the standalone A2A server and dynamic A2A gateway + :alt: Standalone A2A server and clustered gateway-worker architecture :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. + Select AI A2A topologies. Dynamic sessions can run inside the standalone + server or through the clustered gateway and worker processes. CLI-based deployment on any cloud ================================== @@ -43,20 +151,21 @@ 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: +that can reach the required Oracle Database. Clustered deployments also need +Consul; standalone deployments do not. -* ``select-ai a2a serve`` runs the standalone server. -* ``select-ai a2a serve --deployment clustered`` runs the public dynamic - server. -* ``select-ai a2a worker`` runs a database-bearing dynamic worker. +The ``--deployment`` option defaults to ``standalone``. Set it to +``clustered`` to use a gateway and workers. There is no separate connection-mode +CLI flag: the connection is fixed when all four connection values are supplied +and dynamic when any are missing. Environment values count as supplied values. +See :ref:`A2A CLI examples ` for complete commands. 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: +The public routes are the same in all supported combinations: ``/.well-known/agent-card.json`` Agent Card discovery endpoint. @@ -65,39 +174,15 @@ The public routes are the same in both modes: 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 serve --deployment clustered`` - * - 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: +.. raw:: latex + + \Needspace{5\baselineskip} + .. code-block:: bash python -m pip install 'select_ai[a2a]' @@ -108,9 +193,21 @@ All modes require: * 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. +The database user also needs ``CREATE TABLE``, ``CREATE VIEW``, and sufficient +quota in its default tablespace for A2A task and context storage. Ask a database +administrator to grant these privileges to each database user that will open +an A2A session. The SDK's ``grant_privileges()`` helper grants package execute +privileges; it does not grant these storage privileges or tablespace quota. + +At initialization, the runtime creates missing internal tables and executes +``CREATE OR REPLACE VIEW`` for its read-only views. ``CREATE VIEW`` is therefore +required even when the tables already exist. See +:ref:`Persistent Oracle-backed state `. + +The fixed standalone server needs all database values at startup. Dynamic +standalone and clustered sessions can collect missing values through A2UI. +Clustered deployment additionally requires Consul and at least one worker; +workers must be able to reach both Consul and Oracle Database. Protocol compatibility ====================== @@ -151,29 +248,44 @@ 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: +Fixed database connection +~~~~~~~~~~~~~~~~~~~~~~~~~ -.. code-block:: bash +When all four connection values are configured at startup, the standalone +server uses one shared asynchronous connection pool for the selected team. +This mode supports streaming and does not need Consul or a separate worker. +Replace the example values with your database username, password, host, port, +service name, team, and CA-file path. Supply all four connection values to +use fixed standalone mode; omitting any value selects dynamic standalone mode. - export SELECT_AI_USER=select_ai_user - export SELECT_AI_PASSWORD='database-password' - export SELECT_AI_DB_CONNECT_STRING='db2025adb_medium' +.. raw:: latex + + \Needspace{14\baselineskip} + +.. code-block:: bash select-ai a2a serve \ + --deployment standalone \ --team ORACLE_AI_DATABASE_AGENT \ + --user DB_USERNAME \ + --password 'DB_PASSWORD' \ + --dsn 'dbhost.example.com:1521/db_service' \ + --database-tls-ca-file /run/secrets/database-ca.pem \ --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 ``--database-tls-ca-file`` option is only needed when the database +certificate is signed by a CA not already trusted by the system. The +``--public-url`` value 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: +.. raw:: latex + + \Needspace{6\baselineskip} + .. code-block:: text A2A Agent Card: https://a2a.example.com/.well-known/agent-card.json @@ -181,31 +293,80 @@ The server prints the discovery URL when it starts: The exact Uvicorn startup lines vary by version and configuration. +Dynamic standalone sessions +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If one or more of ``--dsn``, ``--user``, ``--password``, and ``--team`` are +omitted, the standalone server asks the A2A client for those values through an +A2UI form. The server creates an isolated child process for each session. This +is still standalone: it needs no Consul service and no ``a2a worker`` process. +Run one Uvicorn process for this mode. Dynamic standalone sessions support +task polling but do not support streaming. + +.. raw:: latex + + \Needspace{9\baselineskip} + +.. code-block:: bash + + select-ai a2a serve \ + --deployment standalone \ + --host 0.0.0.0 \ + --port 8000 \ + --public-url https://a2a.example.com + +If you set some connection values at startup, the form asks only for the +remaining values. See :ref:`A2A CLI examples ` for the +clustered gateway and worker commands. + End-user OAuth and session ownership ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ By default, ``select-ai a2a serve`` does not require an application OAuth -token. It gives each A2A conversation its own database session. A hosting -platform such as private Cloud Run can still authenticate the calling service, -but the A2A server does not verify which human user owns the conversation. +token. Each A2A context maps to its own Select AI conversation, while records +use a shared owner scope (``anonymous`` in fixed standalone mode and +``a2a-conversation`` in session-based modes). A hosting platform such as +private Cloud Run can authenticate the calling service, but the A2A server +does not verify which human user owns the conversation. -Add ``--require-oauth`` when verified end-user ownership is required: +Add ``--require-oauth`` to require a bearer token and derive the owner scope +from it. Configure a trusted upstream service to validate the token first: + +.. raw:: latex + + \Needspace{15\baselineskip} .. code-block:: bash select-ai a2a serve \ + --deployment standalone \ --require-oauth \ --team ORACLE_AI_DATABASE_AGENT \ + --user DB_USERNAME \ + --password 'DB_PASSWORD' \ + --dsn 'dbhost.example.com:1521/db_service' \ + --database-tls-ca-file /run/secrets/database-ca.pem \ --host 0.0.0.0 \ --port 8000 \ --public-url https://a2a.example.com -Every A2A request must then contain ``Authorization: Bearer ...``. The server -uses both the authenticated owner and the A2A conversation to isolate -sessions, tasks, and conversation state. It accepts an OpenID Connect ID token -or an opaque OAuth 2.0 access token. ID-token ownership remains stable across -refreshes through the ``iss`` and ``sub`` claims; refreshing an opaque access -token starts a new owner and session scope. Tokens are never stored or logged. +Every request to the A2A JSON-RPC endpoint must then contain +``Authorization: Bearer ...``. Agent Card discovery remains public. The SDK +requires bearer syntax and derives an owner scope, but does not validate token +signatures, expiration, issuer, or audience. It accepts arbitrary nonempty +bearer values. This flag alone does not establish a verified end-user identity. + +The deployment must validate tokens in a trusted authenticating proxy or +service before forwarding them, and prevent callers from reaching the A2A +listener through a path that bypasses that validation. Cloud Run IAM validation +of a calling service is separate from validation of the end-user token. + +The server uses the derived owner and A2A context to scope task and context +records and the Select AI conversation. For an upstream-validated JWT with +``iss`` and ``sub`` claims, ownership remains stable across token refreshes. +For an opaque access token, ownership is derived from a hash of the token; +refreshing it starts a new owner scope. Tokens are not persisted by this +authentication backend. The Agent Card advertises bearer security only with ``--require-oauth``. @@ -214,6 +375,10 @@ Agent Card discovery Fetch the card after starting the server: +.. raw:: latex + + \Needspace{5\baselineskip} + .. code-block:: bash curl -sS https://a2a.example.com/.well-known/agent-card.json | jq @@ -226,6 +391,10 @@ 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: +.. raw:: latex + + \Needspace{7\baselineskip} + .. code-block:: bash select-ai a2a agent-card \ @@ -234,6 +403,10 @@ without starting the server: This command prints JSON similar to: +.. raw:: latex + + \Needspace{12\baselineskip} + .. code-block:: json { @@ -251,7 +424,7 @@ actual output. Tasks, polling, streaming, and cancellation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The standalone server supports these execution patterns: +The fixed 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 @@ -273,11 +446,17 @@ The repository includes the and the `task_poll.py sample `__: +.. latex:clearpage:: + .. literalinclude:: ../../../samples/a2a/blocking_task.py :language: python - :lines: 8- + :start-at: import json + +The blocking sample omits ``configuration``. Example output (task IDs and model responses vary): -The blocking sample omits ``configuration``. Representative output is: +.. raw:: latex + + \Needspace{10\baselineskip} .. code-block:: text @@ -290,12 +469,29 @@ The blocking sample omits ``configuration``. Representative output is: The task ID, timestamp, and database answer vary for each run. +.. latex:clearpage:: + +First, define the endpoint, prompt, terminal states, and JSON-RPC helper: + +.. literalinclude:: ../../../samples/a2a/task_poll.py + :language: python + :start-at: import json + :end-before: # blocking=False + +.. latex:clearpage:: + +Append the send-and-poll loop to that setup: + .. literalinclude:: ../../../samples/a2a/task_poll.py :language: python - :lines: 8- + :start-at: # blocking=False The polling sample sends a non-blocking request and then calls ``tasks/get``: +.. raw:: latex + + \Needspace{12\baselineskip} + .. code-block:: text Task 42b...: submitted @@ -312,14 +508,15 @@ The polling sample sends a non-blocking request and then calls ``tasks/get``: 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 clustered mode, +Both standalone and clustered deployments use Oracle implementations of the +A2A task and context stores. In fixed standalone mode, the server initializes +these stores when the application starts. In dynamic standalone mode, each +session child initializes the stores after connecting. In clustered mode, ``select-ai a2a serve --deployment clustered`` does not connect to Oracle -itself; the ``select-ai a2a worker`` command starts -the internal worker, and each connected worker session initializes the stores -after it opens its supplied database connection. On first initialization, the -stores create these tables if they do not already exist: +itself; the ``select-ai a2a worker`` command starts the internal worker, and +each connected worker session initializes the stores after it opens its +database connection. On first initialization, the stores create these tables +if they do not already exist: ``DBMS_AI_A2A_TASKS$`` and ``DBMS_AI_A2A_TASKS`` Stores the task ID, context ID, serialized task JSON, owner, and update @@ -335,11 +532,13 @@ stores create these tables if they do not already exist: 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. +conversation history. In standalone modes, task and context ownership is +resolved from the A2A request context in fixed standalone mode; an absent owner +is stored as ``anonymous``. Dynamic standalone and clustered runtimes pass the +gateway's derived owner scope to the child runtime, using ``a2a-conversation`` +when OAuth is not required. Routing binds the owner and A2A context ID to an +internal session ID. That internal ID identifies the child process; it is +separate from the client-visible A2A context ID. The tables hold task and context metadata, while the conversation prompts and responses remain in the regular Select AI conversation storage. This means a @@ -355,22 +554,28 @@ 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``: +Wallet-based connections apply to the fixed standalone +``select-ai a2a serve`` path. Dynamic standalone sessions and clustered worker +sessions do not currently accept wallet settings. Pass the unzipped wallet +directory and password with CLI options: -.. code-block:: bash +.. raw:: latex + + \Needspace{15\baselineskip} - export SELECT_AI_WALLET_LOCATION=/secure/wallet/Wallet_database - export SELECT_AI_WALLET_PASSWORD='wallet-password' - export SELECT_AI_DB_CONNECT_STRING=db2025adb_medium +.. code-block:: bash select-ai a2a serve \ + --deployment standalone \ --team ORACLE_AI_DATABASE_AGENT \ - --user SELECT_AI_USER \ - --dsn db2025adb_medium + --user DB_USERNAME \ + --password 'DB_PASSWORD' \ + --dsn 'dbhost.example.com:1521/db_service' \ + --wallet-location /secure/wallet/Wallet_database \ + --wallet-password 'WALLET_PASSWORD' \ + --host 0.0.0.0 \ + --port 8000 \ + --public-url https://a2a.example.com ``--wallet-location`` is used as both the wallet location and the Oracle configuration directory. Keep wallet files and passwords outside the source @@ -380,15 +585,37 @@ 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 -=================== +Private database CA certificates +================================= + +For a database whose server certificate is signed by a private CA, add +``--database-tls-ca-file /path/to/database-ca.pem`` to the relevant +``select-ai a2a serve`` command. The CLI reads PEM from that file and uses it +to validate Oracle Database TLS in fixed standalone, dynamic standalone, and +clustered sessions. In clustered mode, the gateway sends the certificate to +the worker when it opens a session. The separate ``--worker-tls-ca-file`` +option validates the worker certificate on the gateway-to-worker connection. + +Database call timeout +===================== -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 +Use ``--database-call-timeout-seconds`` to configure the maximum duration of +an Oracle Database call. It defaults to 120 seconds and sets the timeout for +fixed standalone connections and dynamic standalone or clustered worker +sessions. In clustered mode, the gateway uses this value to derive its request +deadline, so set the same value on the gateway and worker. + +Clustered A2A gateway and worker +================================ + +The clustered topology separates the public A2A protocol endpoint from the +database-bearing runtime. The gateway can start without database credentials; +an A2UI form collects them from the client. Values configured on the gateway +are prefilled and the form asks only for the missing values. A worker registers +with Consul and waits for the gateway to assign sessions. Each worker session +uses the persistent Oracle-backed task, context, and conversation state +described in :ref:`Persistent Oracle-backed state +`; Consul stores only temporary routing metadata. Consul provides the gateway's service-discovery and lightweight routing @@ -404,6 +631,10 @@ instructions in the `Consul installation guide `__. After installation, verify that the binary is on ``PATH``: +.. raw:: latex + + \Needspace{5\baselineskip} + .. code-block:: bash consul version @@ -416,6 +647,10 @@ Consul in GKE from the Start the three local components in separate terminals: +.. raw:: latex + + \Needspace{22\baselineskip} + .. code-block:: bash # Terminal 1: Consul @@ -460,6 +695,46 @@ 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. +.. _a2a-clustered-fixed: + +Clustered deployment with a fixed connection +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Yes, a clustered deployment can use fixed connection information. Start +Consul and the worker as shown above, then run this gateway command in place +of the dynamic gateway command. Supply the connection values on the gateway; +the gateway passes them to the selected worker when opening each session: + +.. raw:: latex + + \Needspace{14\baselineskip} + +.. code-block:: bash + + select-ai a2a serve \ + --deployment clustered \ + --host 127.0.0.1 \ + --port 8000 \ + --public-url http://127.0.0.1:8000 \ + --consul-url http://127.0.0.1:8500 \ + --dsn 'dbhost.example.com:1521/db_service' \ + --user APP_USER \ + --password "$SELECT_AI_PASSWORD" \ + --team ORACLE_AI_DATABASE_AGENT + +Set ``SELECT_AI_PASSWORD`` to the database password before running the +command. The client sends an ordinary A2A prompt; the gateway skips the form +and creates the worker session automatically. Workers still isolate sessions +in child processes. All clients use the configured database identity and +team. Fixed clustered connections retain the clustered restrictions: no +streaming and no wallet parameters for the database session. + +To collect each user's credentials instead, remove ``--user`` and +``--password`` and unset their environment fallbacks ``SELECT_AI_USER`` and +``SELECT_AI_PASSWORD`` in the gateway environment. Keep ``--dsn`` and +``--team``. The same gateway and workers now serve dynamic sessions whose +forms ask only for username and password. + A2UI database-connection form ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -503,8 +778,9 @@ template must not contain a password default. The server validates the file at startup and replaces its template surface ID with a fresh ID every time the form is displayed. -After the action is submitted, the gateway selects a healthy worker through -Consul and opens a session. The worker starts a child process, calls +After the action is submitted in clustered mode, the gateway selects a healthy +worker through Consul and opens a session. In dynamic standalone mode, the +server opens the session locally. 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 @@ -554,12 +830,20 @@ The examples below are the and the `task_poll.py sample `__. +.. raw:: latex + + \Needspace{18\baselineskip} + .. literalinclude:: ../../../samples/a2a/dynamic/blocking_task.py :language: python - :lines: 8- + :start-at: from _common Set the database values before running the sample: +.. raw:: latex + + \Needspace{8\baselineskip} + .. code-block:: bash export SELECT_AI_DB_CONNECT_STRING='database-dsn' @@ -569,11 +853,19 @@ Set the database values before running the sample: Run it with: +.. raw:: latex + + \Needspace{5\baselineskip} + .. code-block:: bash python samples/a2a/dynamic/blocking_task.py -Representative output is: +Example output (task IDs and model responses vary): + +.. raw:: latex + + \Needspace{7\baselineskip} .. code-block:: text @@ -584,9 +876,17 @@ Representative output is: The dynamic polling sample uses the same connection-form handshake, then passes ``configuration.blocking: false`` and polls ``tasks/get``: +.. raw:: latex + + \Needspace{30\baselineskip} + .. literalinclude:: ../../../samples/a2a/dynamic/task_poll.py :language: python - :lines: 8- + :start-at: import time + +.. raw:: latex + + \Needspace{9\baselineskip} .. code-block:: text @@ -599,8 +899,9 @@ passes ``configuration.blocking: false`` and polls ``tasks/get``: 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 +Each successful connection action binds the owner and A2A context ID to one +worker session with its own internal session 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 @@ -627,23 +928,98 @@ 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 -~~~~~~~~~~~~~~~~~~~~~~ +Public gateway HTTPS +~~~~~~~~~~~~~~~~~~~~ + +The ``--tls-*`` options on ``select-ai a2a serve`` configure the public A2A +listener. They do not configure gateway-to-worker traffic. + +* With no TLS options, Uvicorn listens over HTTP. +* Supply ``--tls-cert-file`` and ``--tls-key-file`` together to serve HTTPS. + Clients verify the gateway certificate using their configured trust store. +* Add ``--tls-ca-file`` to require clients to present a certificate signed by + that CA. This enables mTLS on the public connection. The CA file requires + the certificate and key. + +For example, this command serves HTTPS without requiring client certificates: + +.. code-block:: bash + + select-ai a2a serve \ + --team ORACLE_AI_DATABASE_AGENT \ + --user DB_USERNAME \ + --password DB_PASSWORD \ + --dsn 'dbhost.example.com:1521/db_service' \ + --host 0.0.0.0 \ + --port 8000 \ + --public-url https://a2a.example.com \ + --tls-cert-file /run/secrets/gateway.crt \ + --tls-key-file /run/secrets/gateway.key + +Add ``--tls-ca-file /run/secrets/client-ca.pem`` when public clients must +authenticate with client certificates. ``--public-url`` controls the URL +advertised in the Agent Card; the TLS options control Uvicorn's listener. + +Gateway-to-worker HTTP and TLS +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The gateway-to-worker connection is independent of the public A2A listener. +The public listener can use HTTPS while workers use HTTP, or the gateway can +use HTTPS to workers while its public listener uses HTTP. + +.. list-table:: Gateway-to-worker TLS options + :header-rows: 1 + :widths: 28 36 36 -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: + * - Option + - Process + - Purpose + * - ``--tls-cert-file`` and ``--tls-key-file`` + - Worker + - Enable HTTPS on the worker. Supply both together. + * - ``--tls-ca-file`` + - Worker + - Optional CA for requiring and verifying gateway client certificates. + Supplying it enables the worker side of mTLS. + * - ``--worker-tls-ca-file`` + - Gateway + - Optional CA bundle for verifying the worker's server certificate. + Supply it alone for HTTPS with a private CA and no mTLS. If omitted, + Requests uses its default CA bundle (which can be overridden with + ``REQUESTS_CA_BUNDLE``). + * - ``--worker-tls-cert-file`` and ``--worker-tls-key-file`` + - Gateway + - Optional gateway client certificate and key. Supply both together; + the worker must require and trust this client certificate for mTLS. + +Whenever a gateway ``--worker-tls-*`` option is supplied, register the worker +with an HTTPS URL using ``--worker-endpoint``. If no gateway worker TLS option +is supplied, Requests still verifies an HTTPS worker certificate against its +default CA bundle. When the worker has no advertised endpoint, the gateway +builds an HTTP URL from the worker address and port in the Consul record. + +For HTTPS without mTLS, configure the worker's certificate and key, then +advertise its HTTPS URL. If the certificate uses a private CA, give that CA +to the gateway with ``--worker-tls-ca-file``. Do not set the gateway client +certificate or the worker's ``--tls-ca-file`` in this mode: .. code-block:: bash + select-ai a2a worker \ + --worker-endpoint https://worker.example.com:8443 \ + --tls-cert-file /run/secrets/worker.crt \ + --tls-key-file /run/secrets/worker.key + select-ai a2a serve \ --deployment clustered \ - --public-url https://gateway.example.com \ - --worker-tls-ca-file /run/secrets/worker-ca.pem \ - --worker-tls-cert-file /run/secrets/gateway-client.crt \ - --worker-tls-key-file /run/secrets/gateway-client.key + --worker-tls-ca-file /run/secrets/private-worker-ca.pem -Start the worker with the matching server certificate, key, and CA: +For mTLS, the worker requires a client certificate with ``--tls-ca-file``. +The gateway presents its certificate and key with ``--worker-tls-cert-file`` +and ``--worker-tls-key-file``. The gateway also needs +``--worker-tls-ca-file`` when the worker certificate is signed by a private +CA; omit it when the Requests default CA bundle trusts the worker certificate. +The two directions can use the same CA or different CAs: .. code-block:: bash @@ -653,11 +1029,16 @@ Start the worker with the matching server certificate, key, and CA: --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. + select-ai a2a serve \ + --deployment clustered \ + --worker-tls-ca-file /run/secrets/worker-server-ca.pem \ + --worker-tls-cert-file /run/secrets/gateway-client.crt \ + --worker-tls-key-file /run/secrets/gateway-client.key + +The worker CA validates the gateway's client certificate. The gateway CA +validates the worker's server certificate. This protects only the +gateway-to-worker connection; it does not configure TLS for the public A2A +listener or Oracle Database connections. Google Cloud deployment ======================= @@ -675,6 +1056,10 @@ Standalone Google Cloud deployment deploys one private Cloud Run service. Select its connection provisioning explicitly: +.. raw:: latex + + \Needspace{8\baselineskip} + .. code-block:: bash gcloud/standalone/deploy.sh \ @@ -684,8 +1069,9 @@ explicitly: The script creates or reuses an Artifact Registry repository, runtime service account, any requested Secret Manager secrets, and Cloud Run service. With no -database options, dynamic mode fixes neither the Connection URL nor AI Agent; -its A2UI form requests Connection URL, username, password, and AI Agent. Pass +database options, dynamic standalone mode fixes neither the Connection URL nor +AI Agent; its A2UI form requests Connection URL, username, password, and AI +Agent. Pass ``--db-dsn-secret`` or ``--a2a-team`` to fix either value independently, or ``--rotate-db-config`` to configure the deployment's Connection URL. Fixed mode prompts for Connection URL, username, and password, requires @@ -708,10 +1094,12 @@ Important standalone options include: * ``--service``: Cloud Run service name; use a different service for each deployment profile. * ``--a2a-team``: fix the AI Agent in the deployment; required in fixed mode. -* ``--require-oauth``: require a Gemini Enterprise end-user OAuth bearer token - and scope state by verified owner as well as A2A conversation. +* ``--require-oauth``: require an upstream-validated end-user bearer token + and derive an owner scope as well as the A2A conversation scope. The SDK + does not validate the token itself; see `End-user OAuth and session ownership`_. * ``--pool-max-size``: maximum Oracle connections per Cloud Run instance. -* ``--max-instances``: Cloud Run instance limit; dynamic mode requires one. +* ``--max-instances``: Cloud Run instance limit; dynamic standalone mode + requires one. * ``--wallet-archive``: upload or replace an Autonomous Database wallet ZIP in fixed mode. * ``--db-dsn-secret``: fix the Connection URL using an existing Secret Manager @@ -732,8 +1120,9 @@ Run URL and the client can invoke the private service. The Google Cloud scripts do not require end-user OAuth by default. Cloud Run IAM authenticates Gemini Enterprise, and ``a2a serve`` separates database sessions by A2A conversation without asserting a verified human owner. Add -``--require-oauth`` to the deployment command when verified user ownership is -required. Gemini Enterprise must then be configured with end-user OAuth rather +``--require-oauth`` to the deployment command when user ownership is required, +and ensure a trusted upstream service validates the end-user token. The flag +alone does not verify identity. Gemini Enterprise must then be configured with end-user OAuth rather than **Skip & Finish**. It uses ``X-Serverless-Authorization`` for Cloud Run IAM and sends the user token separately in ``Authorization``. @@ -745,6 +1134,10 @@ deploys the clustered topology: Cloud Run for the public server, GKE Autopilot for worker replicas, and Consul for discovery and routing. +.. raw:: latex + + \Needspace{8\baselineskip} + .. code-block:: bash gcloud/cluster/deploy.sh \ @@ -780,6 +1173,8 @@ Important clustered options include: internal worker infrastructure. * ``--worker-replicas``: number of worker runtimes available for new sessions. * ``--session-ttl-seconds``: lifetime of temporary database sessions. +* ``--conversation-retention-days``: number of days to retain Oracle + conversations (default 7; use 0 to disable automatic removal). * ``--enable-worker-mtls``: create and use server-to-worker certificates. * ``--rotate-worker-mtls``: replace the existing test certificates and restart the worker workload. diff --git a/doc/source/user_guide/agent.rst b/doc/source/user_guide/agent.rst index 309d43d..98b57e2 100644 --- a/doc/source/user_guide/agent.rst +++ b/doc/source/user_guide/agent.rst @@ -366,6 +366,9 @@ read the generated value from ``fetched.attributes.supervisor_task``. .. code-block:: python + from select_ai.agent import Agent, AgentAttributes, Team, TeamAttributes + + # MOVIE_ANALYST and ANALYZE_MOVIE_TASK must already exist. supervisor = Agent( agent_name="MOVIE_SUPERVISOR", attributes=AgentAttributes( @@ -375,6 +378,8 @@ read the generated value from ``fetched.attributes.supervisor_task``. ), ) + supervisor.create() + team = Team( team_name="MOVIE_AGENT_TEAM", attributes=TeamAttributes( @@ -385,18 +390,50 @@ read the generated value from ``fetched.attributes.supervisor_task``. supervisor_agent=supervisor.agent_name, ), ) + team.create() + +Agent and team constructors build Python objects; ``create()`` persists them +in Oracle Database. The example uses new names and raises an error if those +objects already exist. Use ``replace=True`` on ``create()`` only when you +intend to replace existing definitions. 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. +Set the common database connection variables and ``SELECT_AI_PROFILE_NAME`` +(default ``oci_ai_profile``) to an existing profile. Run +``python samples/agent/team_supervisor_inspect.py`` from the repository root. +The script removes the objects and conversation it creates during cleanup. + +.. latex:clearpage:: + +First, configure the connection and define the task and worker agent: + .. literalinclude:: ../../../samples/agent/team_supervisor_inspect.py :language: python - :lines: 14- + :start-at: import os + :end-before: supervisor = Agent( -The generated names and returned JSON metadata vary between runs. Representative -output is: +.. latex:clearpage:: + +Append the supervisor, team, and conversation definitions: + +.. literalinclude:: ../../../samples/agent/team_supervisor_inspect.py + :language: python + :start-at: supervisor = Agent( + :end-before: created_objects = [] + +.. latex:clearpage:: + +Finally, create the objects, run the team, inspect its metadata, and clean up: + +.. literalinclude:: ../../../samples/agent/team_supervisor_inspect.py + :language: python + :start-at: created_objects = [] + +Example output (generated names, model responses, and JSON metadata vary): output:: @@ -585,7 +622,7 @@ async definition samples for a complete create, inspect, and cleanup flow. :lines: 14- The task name and exact PL/SQL formatting vary between runs and database -versions. Representative output is: +versions. Example output (generated IDs and model responses vary): output:: diff --git a/doc/source/user_guide/api_reference.rst b/doc/source/user_guide/api_reference.rst new file mode 100644 index 0000000..efea259 --- /dev/null +++ b/doc/source/user_guide/api_reference.rst @@ -0,0 +1,86 @@ +.. _error-handling: + +************************************ +Errors, sessions, and result objects +************************************ + +Error handling +============== + +SDK validation and lookup errors are separate from Oracle driver errors. +Catch a specific SDK exception when the application can recover from it. +Database and provider failures surfaced by PL/SQL can raise +``oracledb.DatabaseError``; inspect the driver error code when deciding what +the application should do. Avoid retrying authorization or configuration +failures without correcting their cause. + +.. code-block:: python + + import oracledb + import select_ai + + # Assumes an open connection. Missing profiles are not created here. + try: + profile = select_ai.Profile.fetch("APP_PROFILE") + answer = profile.chat("Explain retrieval augmented generation.") + except select_ai.ProfileNotFoundError: + print("Create APP_PROFILE or choose an existing profile.") + except select_ai.DatabaseNotConnectedError: + print("Establish a connection before using the profile.") + except oracledb.DatabaseError as exc: + error, = exc.args + print(f"Database operation failed (code {error.code}).") + raise + +Do not expose raw database errors or connection credentials in public HTTP +responses. For web applications, translate expected failures into your +application's error response and close pools in the application shutdown hook. + +.. automodule:: select_ai.errors + :members: + +Session objects +=============== + +Obtain sessions through ``Profile.chat_session()`` or +``AsyncProfile.chat_session()``. A session holds a connection for the duration +of its context. Consume and close streaming iterators inside that context. + +.. autoclass:: select_ai.profile.Session + :members: + +.. autoclass:: select_ai.async_profile.AsyncSession + :members: + +Agent history results +===================== + +History APIs yield these typed event objects. Availability and values depend +on the current user's database history views. + +.. autoclass:: select_ai.agent.history.TeamHistoryEvent + :members: + +.. autoclass:: select_ai.agent.history.TaskHistoryEvent + :members: + +.. autoclass:: select_ai.agent.history.ToolHistoryEvent + :members: + +Enumerations +============ + +.. autoclass:: select_ai.action.Action + :members: + +.. autoclass:: select_ai.agent.tool.ToolType + :members: + +.. autoclass:: select_ai.agent.tool.NotificationType + :members: + +.. autoclass:: select_ai.vector_index.VectorDistanceMetric + :members: + +.. autoclass:: select_ai.vector_index.VectorDBProvider + :members: diff --git a/doc/source/user_guide/async_agent.rst b/doc/source/user_guide/async_agent.rst index 06377d5..2e2edc1 100644 --- a/doc/source/user_guide/async_agent.rst +++ b/doc/source/user_guide/async_agent.rst @@ -311,6 +311,12 @@ after creation to read the generated value from .. code-block:: python + from select_ai.agent import ( + AgentAttributes, AsyncAgent, AsyncTeam, TeamAttributes, + ) + + # Inside an async function, with an open async connection. + # MOVIE_ANALYST and ANALYZE_MOVIE_TASK must already exist. supervisor = AsyncAgent( agent_name="MOVIE_SUPERVISOR", attributes=AgentAttributes( @@ -320,6 +326,8 @@ after creation to read the generated value from ), ) + await supervisor.create() + team = AsyncTeam( team_name="MOVIE_AGENT_TEAM", attributes=TeamAttributes( @@ -330,18 +338,50 @@ after creation to read the generated value from supervisor_agent=supervisor.agent_name, ), ) + await team.create() + +Constructors build Python objects. Await ``create()`` to persist each object +in Oracle Database. The example expects new supervisor and team names; pass +``replace=True`` to ``create()`` only when replacement is intended. 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. +Set the common database connection variables and ``SELECT_AI_PROFILE_NAME`` +(default ``oci_ai_profile``) to an existing profile. Run +``python samples/agent/async/team_supervisor_inspect.py`` from the repository +root. The script removes its generated objects and conversation during cleanup. + +.. latex:clearpage:: + +Configure the imports and connection values: + .. literalinclude:: ../../../samples/agent/async/team_supervisor_inspect.py :language: python - :lines: 14- + :start-at: import asyncio + :end-before: async def main(): -The generated names and returned JSON metadata vary between runs. Representative -output is: +.. latex:clearpage:: + +Append the async function and its task, agent, and team definitions: + +.. literalinclude:: ../../../samples/agent/async/team_supervisor_inspect.py + :language: python + :start-at: async def main(): + :end-before: created_objects = [] + +.. latex:clearpage:: + +Continue inside ``main()`` with creation, execution, inspection, and cleanup. +Keep this block indented as part of the function, then add the entry point: + +.. literalinclude:: ../../../samples/agent/async/team_supervisor_inspect.py + :language: python + :start-at: created_objects = [] + +Example output (generated names, model responses, and JSON metadata vary): output:: diff --git a/doc/source/user_guide/async_profile.rst b/doc/source/user_guide/async_profile.rst index 9642526..9d737a1 100644 --- a/doc/source/user_guide/async_profile.rst +++ b/doc/source/user_guide/async_profile.rst @@ -377,6 +377,22 @@ 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``. +The SDK awaits the database function before reading its returned CLOB in +chunks. The first chunk is available after that function returns; this does +not guarantee token delivery while the model is generating. A partially +consumed iterator retains its cursor and connection. Use ``aclosing()`` when +iteration can stop early, and consume session streams before leaving the +``chat_session()`` context: + +.. code-block:: python + + from contextlib import aclosing + + chunks = await async_profile.chat("Explain connection pooling.", stream=True) + async with aclosing(chunks): + async for chunk in chunks: + print(chunk, end="") + .. _async-request-profile-attributes: *********************************************** @@ -551,6 +567,43 @@ output:: Async pipeline ********************* +.. _pipeline-compatibility: + +Pipeline compatibility and results +++++++++++++++++++++++++++++++++++ + +``run_pipeline()`` requires ``python-oracledb>=2.4.0``, which provides +``create_pipeline()`` and ``AsyncConnection.run_pipeline()``. If an older +driver lacks these APIs, upgrade it with ``python -m pip install --upgrade +oracledb``. Async operations use Thin mode. + +The driver documents true pipelining for Oracle AI Database 26ai and later. +On older databases it executes operations individually, without reducing +round trips. Pipeline operations execute sequentially, even when true +pipelining is available. Select AI returns CLOBs, and reading these adds +database calls; do not assume the complete SDK operation uses one round trip. +See `python-oracledb pipelining +`__. + +Results preserve input order. Successful results are strings read from CLOBs. +For ``Action.RUNSQL``, this is the database's JSON response, rather than the +``pandas.DataFrame`` returned by ``AsyncProfile.run_sql()``. With +``continue_on_error=False`` (the default), an operation error raises an +exception. With ``continue_on_error=True``, failed entries contain driver +error objects; check their type before using them as text: + +.. code-block:: python + + results = await async_profile.run_pipeline( + [("How many promotions?", select_ai.Action.SHOWSQL)], + continue_on_error=True, + ) + for result in results: + if isinstance(result, str): + print(result) + else: + print(f"Pipeline operation failed: {result}") + ``AsyncProfile.run_pipeline()`` also accepts ``attributes=...``. The mapping is applied to every prompt/action request in the pipeline, while the saved profile remains unchanged. @@ -594,32 +647,131 @@ output:: .. latex:clearpage:: +****************************** +Share a profile asynchronously +****************************** + +While connected as the profile owner, grant access to an existing profile: + +.. code-block:: python + + profile = await select_ai.AsyncProfile.fetch("OCI_AI_PROFILE") + await profile.grant_access("APP_USER") + +The recipient can fetch the profile with ``owner="APP_OWNER"`` in a separate +connection. When access should end, run +``await profile.revoke_access("APP_USER")`` as the owner. See +:ref:`Sharing and ownership ` for the complete owner and recipient +sequence. + +.. raw:: latex + + \Needspace{25\baselineskip} + +************************************ +Give async feedback on generated SQL +************************************ + +Use feedback after reviewing the SQL produced for a prompt. Identify the +request with a ``(prompt, Action)`` tuple or a ``sql_id`` from +``V$MAPPED_SQL``. The example below assumes an open async connection and an +existing profile over the ``SH`` schema: + +.. code-block:: python + + profile = await select_ai.AsyncProfile.fetch("OCI_AI_PROFILE") + prompt = "How many promotions?" + sql = await profile.show_sql(prompt=prompt) + print(sql) + +After inspecting the SQL, record positive feedback if it answers the question: + +.. code-block:: python + + await profile.add_positive_feedback( + prompt_spec=(prompt, select_ai.Action.SHOWSQL) + ) + +For an incorrect answer, use ``add_negative_feedback()`` with the same request +identifier, ``response`` containing the expected SQL, and ``feedback_content`` +explaining the correction. Remove obsolete feedback with +``delete_feedback()`` and the same request identifier. +Await these methods when using ``AsyncProfile``. + +.. latex:clearpage:: + **************************** List profiles asynchronously **************************** -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. +Use ``AsyncProfile.list()`` to discover saved profiles in the current schema. +Call it on the class; you do not need to create an empty profile first. +Each returned object includes ``profile_name``, ``owner``, and the +owner-qualified ``qualified_name``. + +Connect and list your profiles +++++++++++++++++++++++++++++++ + +Set ``SELECT_AI_USER``, ``SELECT_AI_PASSWORD``, and +``SELECT_AI_DB_CONNECT_STRING`` before running this complete script. It prints +one qualified name per profile and closes the connection afterward: + +.. literalinclude:: ../../../samples/async/profiles_list.py + :language: python + :start-at: import + +For a schema named ``APP_OWNER`` containing two profiles, the output could be: + +.. code-block:: text + + APP_OWNER.OCI_AI_PROFILE + APP_OWNER.OCI_VECTOR_AI_PROFILE + +Your results depend on the profiles visible in your schema; result order is +not guaranteed. An empty result means no profiles match the requested scope. + +Filter by name +++++++++++++++ + +With a connection open, use ``profile_name_pattern`` to match names with a +regular expression. This example lists names starting with ``OCI_``: .. 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(profile_name_pattern="^OCI_"): + print(profile.qualified_name) + +``AsyncProfile.list()`` returns an async iterator. Use ``async for`` +directly; do not await the iterator. Put these snippets inside your async +function before disconnecting. + +.. raw:: latex + + \Needspace{27\baselineskip} + +Find and reuse a shared profile ++++++++++++++++++++++++++++++++ + +Pass ``owner`` to list profiles in another schema. The connected user must +have access to those profiles; specifying an owner does not grant access. +While connected as the recipient, list the profiles shared by ``APP_OWNER``: + +.. code-block:: python async for profile in select_ai.AsyncProfile.list(owner="APP_OWNER"): print(profile.qualified_name) -.. literalinclude:: ../../../samples/async/profiles_list.py - :language: python - :lines: 14- +Once you know the profile name, fetch that profile with the same owner and +use the returned object directly: -output:: +.. code-block:: python + + profile = await select_ai.AsyncProfile.fetch( + "OCI_AI_PROFILE", owner="APP_OWNER" + ) + print(profile.qualified_name) # APP_OWNER.OCI_AI_PROFILE + response = await profile.chat(prompt="What is retrieval augmented generation?") + print(response) - OCI_VECTOR_AI_PROFILE - OCI_AI_PROFILE +See :ref:`Sharing and ownership ` for the owner's grant and revoke +steps. diff --git a/doc/source/user_guide/cli.rst b/doc/source/user_guide/cli.rst index 84abdb1..115de9b 100644 --- a/doc/source/user_guide/cli.rst +++ b/doc/source/user_guide/cli.rst @@ -16,9 +16,11 @@ 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 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. +server, clustered gateway and worker, and Agent Card generator. RAG is +supported when the selected profile is already configured with a vector +index. The :ref:`A2A guide ` explains the difference between fixed and +dynamic connections and standalone and clustered topologies; this page gives +the corresponding CLI commands. .. only:: html @@ -33,39 +35,35 @@ index. :width: 100% The package provides an optional ``select-ai`` command line tool. Install the -CLI extra to use profile, SQL, chat, and A2A commands. The ``a2a`` extra is an -alias that makes the A2A dependency set explicit: +CLI extra to use profile, SQL, and chat commands. Install the separate ``a2a`` +extra for the A2A server and worker commands. If A2A dependencies are missing, +invoking the A2A command group or a server or worker command reports how to +install them; the other CLI commands remain available: .. code-block:: bash pip install 'select_ai[cli]' - # Equivalent installation for an A2A deployment: + # Install A2A support: 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: - -.. code-block:: bash - - export SELECT_AI_USER= - export SELECT_AI_PASSWORD= - export SELECT_AI_DB_CONNECT_STRING= +The A2A examples below pass database credentials with CLI options so the +commands are easy to copy and adapt. Environment-variable equivalents are +listed in the connection-options table. 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. +``select-ai a2a serve`` command accepts these options directly. In standalone +mode, providing all four connection values at startup selects the fixed +connection path; omitting values opens a dynamic A2UI form for them. In +clustered mode, the gateway can preconfigure any values and the form collects +the rest. The worker receives the resolved values when it opens a session. +For a private database CA, use ``--database-tls-ca-file``; the CLI reads PEM +from the specified file. .. list-table:: Connection options :header-rows: 1 @@ -84,12 +82,17 @@ and ``a2a worker`` receives those values internally when it opens a session. - ``SELECT_AI_WALLET_LOCATION`` * - ``--wallet-password`` - ``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 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. + * - ``--database-tls-ca-file`` + - ``SELECT_AI_DATABASE_TLS_CA_FILE`` + +Profile, SQL, and chat commands prompt for the database password when neither +``--password`` nor ``SELECT_AI_PASSWORD`` is set. For ``a2a serve``, omitting +any connection value selects dynamic mode, and the A2A client supplies the +missing values through an A2UI form. Supply ``--dsn``, ``--user``, +``--password``, and ``--team`` for fixed standalone mode. Wallet options apply +to fixed standalone A2A connections; dynamic sessions do not currently +support Oracle wallet authentication. The A2A CLI examples below include a +host, port, service name, credentials, and optional database CA file. Interactive chat ================ @@ -217,98 +220,245 @@ current defaults: * - Command - Purpose * - ``select-ai a2a serve`` - - Start the public A2A HTTP server in standalone or clustered deployment - mode. Standalone accepts database connection and wallet options; - clustered uses Consul to discover workers. + - Start the public A2A HTTP server. The default standalone topology needs + no worker or Consul. Add ``--deployment clustered`` to use a gateway, + workers, and Consul. * - ``select-ai a2a worker`` - - Start the internal worker that registers with Consul and creates an - isolated database-bearing child process for each submitted connection. + - Start an internal database worker for clustered topology. Standalone + topology does not use this command. * - ``select-ai a2a agent-card`` - Print a Gemini Enterprise-compatible A2A v0.3 Agent Card without starting a server. -Standalone server ------------------ +.. _a2a-cli-examples: -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: +A2A deployment examples +----------------------- -.. code-block:: bash +The :ref:`A2A guide ` explains how fixed and dynamic connections differ +from standalone and clustered topologies. These examples use CLI options for +the database settings. Replace the placeholder credentials and connection +string with your values. - export SELECT_AI_USER=select_ai_user - export SELECT_AI_PASSWORD='database-password' - export SELECT_AI_DB_CONNECT_STRING=db2025adb_medium +Fixed standalone connection +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Pass all four connection values at startup to use the standalone server's +shared connection pool. This needs no Consul or worker and supports streaming: + +.. code-block:: bash select-ai a2a serve \ + --deployment standalone \ --team ORACLE_AI_DATABASE_AGENT \ + --user DB_USERNAME \ + --password 'DB_PASSWORD' \ + --dsn 'dbhost.example.com:1521/db_service' \ + --database-tls-ca-file /run/secrets/database-ca.pem \ + --database-call-timeout-seconds 120 \ --host 0.0.0.0 \ --port 8000 \ --public-url https://a2a.example.com +The database CA file is optional when the server certificate is already +trusted by the operating system. ``--wallet-location`` and +``--wallet-password`` are also available for fixed standalone connections +that use an Oracle wallet. + .. 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 standalone connection +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Omit one or more of ``--dsn``, ``--user``, ``--password``, and ``--team`` to +have the A2A client supply those values through an A2UI form. The server runs +the session child processes itself, so this mode needs no Consul or separate +worker. Run a single Uvicorn process. Dynamic standalone supports task polling, +not streaming: + +.. code-block:: bash + + select-ai a2a serve \ + --deployment standalone \ + --database-tls-ca-file /run/secrets/database-ca.pem \ + --host 0.0.0.0 \ + --port 8000 \ + --public-url https://a2a.example.com -By default, ``a2a serve`` does not require an application OAuth token and -separates database sessions by A2A conversation. Add ``--require-oauth`` to -require ``Authorization: Bearer ...`` and scope sessions by authenticated -owner as well as conversation. The Agent Card advertises bearer security only -in that mode. +Remove ``--database-tls-ca-file`` when the database server certificate is +already trusted by the operating system. Any connection values provided on +the command line are fixed; the form asks only for the values that are +missing. -Dynamic gateway and worker --------------------------- +Clustered 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. +Clustered topology requires Consul and at least one worker. Consul is the +worker directory and session-routing service: it helps the gateway find a +healthy worker for a new session and send later requests back to that worker. +See :ref:`Why clustered deployments use Consul ` for a worked +example. Point the gateway and all workers at the same Consul service with +``--consul-url``. + +The gateway can run without database credentials; by default, the A2A client supplies them +through the connection form. For local development, start Consul and then run +the worker and gateway in separate terminals: .. code-block:: bash + # Terminal 1: local development Consul agent + consul agent -dev -bind=127.0.0.1 -client=127.0.0.1 + + # Terminal 2: database worker select-ai a2a worker \ --host 127.0.0.1 \ --port 8081 \ --consul-url http://127.0.0.1:8500 \ --worker-id local-worker \ - --worker-endpoint http://127.0.0.1:8081 + --worker-endpoint http://127.0.0.1:8081 \ + --database-call-timeout-seconds 120 + # Terminal 3: public gateway select-ai a2a serve \ --deployment clustered \ --host 127.0.0.1 \ --port 8000 \ --public-url http://127.0.0.1:8000 \ - --consul-url http://127.0.0.1:8500 - -The worker options are ``--host``, ``--port``, ``--worker-id``, -``--consul-url``, ``--worker-endpoint``, ``--session-ttl-seconds``, and -``--session-start-timeout-seconds``. ``WORKER_ID``, ``CONSUL_HTTP_URL``, and -``WORKER_ENDPOINT`` are environment fallbacks; explicit command-line values -take precedence. ``--port`` is also the port registered with Consul. Cluster -manifests can set ``WORKER_ADDRESS`` to the pod IP when no endpoint is -supplied. The worker's ``--tls-cert-file``, ``--tls-key-file``, and -``--tls-ca-file`` options enable gateway-to-worker mTLS; provide all three -together. - -The clustered serve options are ``--public-url`` (or ``PUBLIC_URL``), -``--consul-url`` (or ``CONSUL_HTTP_URL``), ``--worker-service`` (or -``WORKER_SERVICE``), and ``--session-ttl-seconds`` (or -``SESSION_TTL_SECONDS``). ``--dsn``, ``--user``, ``--password``, and ``--team`` -fix any supplied connection values; the generated A2UI form contains only the -missing values. ``--a2ui-form`` loads a custom A2UI JSON form that must submit -exactly those missing values. ``--require-oauth`` has the same ownership -behavior in standalone and clustered deployments. The optional -``--worker-tls-ca-file``, ``--worker-tls-cert-file``, and -``--worker-tls-key-file`` options configure the gateway's client side of -gateway-to-worker mTLS. These TLS settings protect the internal HTTP hop and -are separate from Oracle wallet authentication. + --consul-url http://127.0.0.1:8500 \ + --database-tls-ca-file /run/secrets/database-ca.pem \ + --database-call-timeout-seconds 120 + +The local Consul command is for development only. In a deployment, point the +gateway and workers at the existing Consul service and use network-reachable +worker addresses. The database CA option is needed only for a private database +CA; the gateway passes its PEM contents to the worker when opening a session. +Set the same ``--database-call-timeout-seconds`` value on the gateway and +worker. See the A2A guide for Consul setup and clustered deployment details. + +Important ``serve`` options include ``--team``, ``--host``, ``--port``, +``--public-url``, ``--description``, ``--pool-max-size``, and +``--database-call-timeout-seconds``. The default database call timeout is 120 +seconds. On the clustered gateway, this value sets the request deadline basis; +the worker applies it to Oracle calls and derives its child-process deadline. + +By default, ``a2a serve`` does not require an application OAuth token. A2A +task and context records use a shared owner scope, and each A2A context maps +to its own Select AI conversation. Add ``--require-oauth`` to require +``Authorization: Bearer ...`` and derive an owner scope as well as context. +The SDK does not validate token signatures or claims. A trusted upstream +service must validate the end-user token and prevent direct access that +bypasses validation. See :ref:`the A2A authentication contract +`. +The Agent Card advertises bearer security only in that mode. + +Clustered option reference +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Configure discovery first, then connection values and session limits. Explicit +CLI values take precedence over environment fallbacks. For a fixed clustered +connection, see the :ref:`complete gateway command `. + +Gateway discovery ++++++++++++++++++ + +``--public-url`` (``PUBLIC_URL``) + Client-visible base URL advertised in the Agent Card. + +``--consul-url`` (``CONSUL_HTTP_URL``) + Consul HTTP API URL. Use the same Consul service as the workers. + +``--worker-service`` (``WORKER_SERVICE``) + Consul service to discover. Default: ``select-ai-a2a-worker``. + +Worker registration ++++++++++++++++++++ + +``--host`` and ``--port`` + Worker listen address and port. The port is also registered with Consul. + +``--worker-id`` (``WORKER_ID``) + Unique worker identifier. Defaults to the host name. + +``--consul-url`` (``CONSUL_HTTP_URL``) + Consul HTTP API URL used to register the worker and send heartbeats. + +``--worker-endpoint`` (``WORKER_ENDPOINT``) + Worker URL reachable by the gateway. In cluster manifests, + ``WORKER_ADDRESS`` can supply the pod IP when no endpoint is given. + +Connection values and forms ++++++++++++++++++++++++++++ + +``--dsn``, ``--user``, ``--password``, and ``--team`` (gateway) + Fix the supplied values at startup. Set all four for a fixed connection; + otherwise the A2UI form asks only for the missing values. The environment + fallbacks are ``SELECT_AI_DB_CONNECT_STRING``, ``SELECT_AI_USER``, + ``SELECT_AI_PASSWORD``, and ``SELECT_AI_A2A_TEAM``. + +``--a2ui-form`` (gateway) + Path to a custom A2UI JSON form. It must submit exactly the missing + connection values. See :ref:`A2A Integration ` for the form contract. + +Session lifetime and timeouts ++++++++++++++++++++++++++++++ + +``--session-ttl-seconds`` (gateway and worker) + Lifetime of a temporary database session; default 900 seconds. + The gateway also reads ``SESSION_TTL_SECONDS``. Configure the same lifetime + on both processes. + +``--conversation-retention-days`` (worker) + Days to retain Oracle conversations; default 7. Environment fallback: + ``CONVERSATION_RETENTION_DAYS``. Use 0 to retain until manually deleted. + +``--session-start-timeout-seconds`` (worker) + Maximum wait for a child process to initialize its database session. + +``--database-call-timeout-seconds`` (gateway and worker) + Database call timeout; default 120 seconds. Set the same value on both + processes. The worker applies it to Oracle calls; both processes derive + their request deadlines from it. + +Authentication and TLS +++++++++++++++++++++++ + +``--require-oauth`` (gateway) + Require a bearer token and derive an owner scope for task and context + records. Token validation must occur upstream. Uses the same ownership + behavior as standalone mode. + +``--tls-cert-file`` and ``--tls-key-file`` (gateway) + Enable HTTPS on the public A2A listener. Supply both together; without + them, the listener uses HTTP. + +``--tls-ca-file`` (gateway) + Optional CA that makes the public listener require and verify client + certificates (mTLS). Requires ``--tls-cert-file`` and ``--tls-key-file``. + +``--worker-tls-ca-file``, ``--worker-tls-cert-file``, ``--worker-tls-key-file`` + Gateway TLS options for worker connections. The CA verifies the worker's + server certificate; it can be supplied alone for HTTPS without mTLS. If + omitted, Requests uses its default CA bundle. The client certificate and + key are optional, but must be supplied together; the worker must require + and trust them for mTLS. When any ``--worker-tls-*`` option is used, set + ``--worker-endpoint`` to an HTTPS URL. + Environment fallbacks are ``WORKER_TLS_CA_FILE``, + ``WORKER_TLS_CERT_FILE``, and ``WORKER_TLS_KEY_FILE``. + +``--tls-ca-file``, ``--tls-cert-file``, ``--tls-key-file`` (worker) + The certificate and key enable HTTPS on the worker and must be supplied + together. ``--tls-ca-file`` is optional; when supplied, the worker + requires and verifies client certificates for mTLS. Set + ``--worker-endpoint`` to an HTTPS URL so the gateway connects over TLS. + +``--database-tls-ca-file`` (gateway) + CA for verifying the Oracle database certificate. The gateway passes the + PEM contents to the worker session. This is separate from the certificates + for the gateway-to-worker HTTP connection and from Oracle wallet mTLS. Agent Card generation --------------------- @@ -353,8 +503,11 @@ Command summary * - ``select-ai profile translate`` - Translate text with a saved profile. * - ``select-ai a2a serve`` - - Start the public A2A server in standalone or clustered mode. + - Start the public A2A server. The default is standalone; use + ``--deployment clustered`` for a gateway and workers. Omitted + connection values are collected through A2UI. * - ``select-ai a2a worker`` - - Start the internal dynamic-session worker. + - Start a database worker for clustered topology. Standalone deployments + do not use a worker. * - ``select-ai a2a agent-card`` - Print an A2A v0.3 Agent Card. diff --git a/doc/source/user_guide/compatibility.rst b/doc/source/user_guide/compatibility.rst new file mode 100644 index 0000000..0560f0b --- /dev/null +++ b/doc/source/user_guide/compatibility.rst @@ -0,0 +1,71 @@ +.. _compatibility: + +****************************** +Compatibility and requirements +****************************** + +Select AI for Python 1.5.0 supports Python 3.11, 3.12, 3.13, and 3.14. +Installation requires ``oracledb>=2.0.0`` and ``pandas==3.0.3``. Some optional +features require driver APIs introduced after the minimum installable driver +version; upgrade ``oracledb`` if those APIs are unavailable. + +Installing the SDK does not install database PL/SQL packages or enable their +features. Availability depends on the Oracle Database service, version, and +package updates. Consult Oracle's `Select AI and Select AI Agent capability +matrix `__ +for your environment before choosing a workflow. + +.. list-table:: Feature requirements + :header-rows: 1 + :widths: 25 35 40 + + * - Feature + - Python installation + - Database and deployment requirements + * - Profiles, SQL generation, and chat + - ``select_ai`` + - ``DBMS_CLOUD_AI`` and ``DBMS_CLOUD``; provider credential and outbound + database network access. SQL execution also requires object privileges. + * - Async APIs + - ``select_ai`` with an async-capable ``oracledb`` driver + - Thin mode; use an async connection or async pool. Use a pool for + independent concurrent database operations. + * - Pipelines + - ``oracledb>=2.4.0`` with ``create_pipeline()`` and + ``AsyncConnection.run_pipeline()`` + - True pipelining requires Oracle AI Database 26ai or later. Older + databases execute individually. See :ref:`pipeline-compatibility`. + * - RAG vector indexes + - ``select_ai`` + - Vector-index support in ``DBMS_CLOUD_AI``, ``DBMS_CLOUD_PIPELINE``, an + embedding model, source access, and database storage capacity. + * - Agent teams and history + - ``select_ai`` + - ``DBMS_CLOUD_AI_AGENT`` and the corresponding agent history views. + * - Profile, index, team, and credential sharing + - ``select_ai`` + - Database packages providing the relevant grant/revoke APIs. The + recipient still needs privileges on workflow dependencies. + * - CLI profile, SQL, and chat commands + - ``select_ai[cli]`` + - Same requirements as the corresponding SDK operation. + * - A2A fixed standalone + - ``select_ai[a2a]`` + - Existing agent team; A2A storage privileges and quota. Supports + streaming and wallet authentication. + * - A2A dynamic standalone + - ``select_ai[a2a]`` + - One Uvicorn process; A2UI-capable client. Polling supported; streaming + and Oracle wallet authentication are unavailable. + * - A2A clustered + - ``select_ai[a2a]`` on gateway and workers + - Consul and at least one worker. Polling supported; streaming and + Oracle wallet authentication are unavailable. + +The OCI credential-loading examples also require ``oci`` and a configured +OCI API-signing key. This is a sample dependency, rather than an SDK runtime +requirement. See :ref:`Credential `. + +Provider models, regions, and account permissions are independent of Python +package compatibility. Replace example model identifiers and storage URIs +with values supported by your provider account and database integration. diff --git a/doc/source/user_guide/concurrent_prompt_processing.rst b/doc/source/user_guide/concurrent_prompt_processing.rst index 60b8aac..3a5b04d 100644 --- a/doc/source/user_guide/concurrent_prompt_processing.rst +++ b/doc/source/user_guide/concurrent_prompt_processing.rst @@ -46,7 +46,7 @@ Recipe summary * - Async pipeline - :ref:`async_pipeline_recipe` - Use ``run_pipeline()`` when all prompt/action pairs are known up front - and should be sent in a single database round trip. + and should be submitted as a database pipeline. * - Async queue - :ref:`async_queue_workers_recipe` - Use async queue workers for long-running async services or background @@ -63,6 +63,10 @@ The recipes use the same connection environment variables as the other samples: export SELECT_AI_PASSWORD= export SELECT_AI_DB_CONNECT_STRING= +.. raw:: latex + + \Needspace{10\baselineskip} + Optional environment variables control pool sizing and profile names: .. code-block:: sh @@ -75,6 +79,8 @@ Optional environment variables control pool sizing and profile names: Use ``SELECT_AI_PROFILE_NAME=async_oci_ai_profile`` for the async recipes if that is the async profile name in your environment. +.. latex:clearpage:: + .. _sync_thread_pool_recipe: ``sync_thread_pool.py`` @@ -83,9 +89,23 @@ that is the async profile name in your environment. This recipe uses ``ThreadPoolExecutor`` and ``as_completed()``. Results are printed in the order they finish. +Set up the imports, connection values, prompts, and any helper functions: + .. literalinclude:: ../../../recipes/concurrent_prompt_processing/sync_thread_pool.py :language: python - :lines: 14- + :start-at: import + :end-before: select_ai.create_pool( + +.. latex:clearpage:: + +The following code creates the connection pool, processes the prompts, and +closes the pool in a ``finally`` block: + +.. literalinclude:: ../../../recipes/concurrent_prompt_processing/sync_thread_pool.py + :language: python + :start-at: select_ai.create_pool( + +.. latex:clearpage:: .. _sync_ordered_results_recipe: @@ -95,9 +115,23 @@ printed in the order they finish. This recipe uses ``ThreadPoolExecutor.map()``. Prompts run concurrently, but results are printed in the same order as the input list. +Set up the imports, connection values, prompts, and any helper functions: + +.. literalinclude:: ../../../recipes/concurrent_prompt_processing/sync_ordered_results.py + :language: python + :start-at: import + :end-before: select_ai.create_pool( + +.. latex:clearpage:: + +The following code creates the connection pool, processes the prompts, and +closes the pool in a ``finally`` block: + .. literalinclude:: ../../../recipes/concurrent_prompt_processing/sync_ordered_results.py :language: python - :lines: 14- + :start-at: select_ai.create_pool( + +.. latex:clearpage:: .. _sync_queue_workers_recipe: @@ -107,9 +141,23 @@ results are printed in the same order as the input list. This recipe uses worker threads and ``queue.Queue``. It is useful when prompt producers and prompt processors are separate parts of an application. +Set up the imports, connection values, prompts, and any helper functions: + .. literalinclude:: ../../../recipes/concurrent_prompt_processing/sync_queue_workers.py :language: python - :lines: 14- + :start-at: import + :end-before: select_ai.create_pool( + +.. latex:clearpage:: + +The following code creates the connection pool, processes the prompts, and +closes the pool in a ``finally`` block: + +.. literalinclude:: ../../../recipes/concurrent_prompt_processing/sync_queue_workers.py + :language: python + :start-at: select_ai.create_pool( + +.. latex:clearpage:: .. _async_gather_recipe: @@ -119,9 +167,23 @@ producers and prompt processors are separate parts of an application. This recipe uses ``asyncio.gather()``. Prompts run concurrently, and results are returned in the same order as the input task list. +Set up the imports, connection values, prompts, and any helper functions: + +.. literalinclude:: ../../../recipes/concurrent_prompt_processing/async_gather.py + :language: python + :start-at: import + :end-before: async def main(): + +.. latex:clearpage:: + +The following code creates the connection pool, processes the prompts, and +closes the pool in a ``finally`` block: + .. literalinclude:: ../../../recipes/concurrent_prompt_processing/async_gather.py :language: python - :lines: 14- + :start-at: async def main(): + +.. latex:clearpage:: .. _async_as_completed_recipe: @@ -131,24 +193,58 @@ are returned in the same order as the input task list. This recipe uses ``asyncio.as_completed()``. It is useful for command-line tools or services that can forward each answer as soon as it is ready. +Set up the imports, connection values, prompts, and any helper functions: + .. literalinclude:: ../../../recipes/concurrent_prompt_processing/async_as_completed.py :language: python - :lines: 14- + :start-at: import + :end-before: async def main(): + +.. latex:clearpage:: + +The following code creates the connection pool, processes the prompts, and +closes the pool in a ``finally`` block: + +.. literalinclude:: ../../../recipes/concurrent_prompt_processing/async_as_completed.py + :language: python + :start-at: async def main(): + +.. latex:clearpage:: .. _async_pipeline_recipe: ``async_pipeline.py`` ===================== -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 +This recipe uses ``AsyncProfile.run_pipeline()`` to submit multiple +prompt/action pairs as a batch. Pipeline operations execute sequentially in +the database; they do not run in parallel. The application receives the batch results when the pipeline completes. Pass ``attributes=...`` to apply the same request-level profile overrides to every item in the pipeline. +True pipelining requires a supporting database version. On older databases, +the driver executes operations individually, without reducing round trips. +Reading returned CLOBs also requires additional database calls. See +:ref:`Pipeline compatibility ` for requirements and +result handling. + +Set up the imports, connection values, prompts, and any helper functions: + .. literalinclude:: ../../../recipes/concurrent_prompt_processing/async_pipeline.py :language: python - :lines: 14- + :start-at: import + :end-before: async def main(): + +.. latex:clearpage:: + +The following code creates the connection pool, processes the prompts, and +closes the pool in a ``finally`` block: + +.. literalinclude:: ../../../recipes/concurrent_prompt_processing/async_pipeline.py + :language: python + :start-at: async def main(): + +.. latex:clearpage:: .. _async_queue_workers_recipe: @@ -158,9 +254,21 @@ same request-level profile overrides to every item in the pipeline. This recipe uses ``asyncio.Queue`` and async worker tasks. It is useful for long-running async applications that receive prompts over time. +Set up the imports, connection values, prompts, and any helper functions: + +.. literalinclude:: ../../../recipes/concurrent_prompt_processing/async_queue_workers.py + :language: python + :start-at: import + :end-before: async def main(): + +.. latex:clearpage:: + +The following code creates the connection pool, processes the prompts, and +closes the pool in a ``finally`` block: + .. literalinclude:: ../../../recipes/concurrent_prompt_processing/async_queue_workers.py :language: python - :lines: 14- + :start-at: async def main(): Pool sizing =========== diff --git a/doc/source/user_guide/connection.rst b/doc/source/user_guide/connection.rst index 66267dd..2f740d0 100644 --- a/doc/source/user_guide/connection.rst +++ b/doc/source/user_guide/connection.rst @@ -9,7 +9,10 @@ to connect to the database and execute PL/SQL subprograms. The library keeps the active connection or connection pool for the current process so profile, credential, provider, vector index, and agent APIs can use -it without passing a connection object to each call. Use a standalone +it without passing a connection object to each call. Standalone connections +are local to the creating thread; pools are shared within a process. Do not +configure both a standalone connection and a pool for the same API style in +one thread: disconnect before switching. Use a standalone connection for scripts and notebooks. Use a connection pool for applications that handle concurrent work, such as web services or worker processes. diff --git a/doc/source/user_guide/conversation.rst b/doc/source/user_guide/conversation.rst index dd427b0..1f7c2ef 100644 --- a/doc/source/user_guide/conversation.rst +++ b/doc/source/user_guide/conversation.rst @@ -148,7 +148,7 @@ output:: .. latex:clearpage:: Chat session -+++++++++++++ +++++++++++++ Use ``chat_session()`` to keep context across multiple chat prompts. The second prompt in this example can refer to the previous answer because both prompts use @@ -197,7 +197,8 @@ a tag; pass ``force=True`` when a missing tag should not raise an error. prompts = list(conversation.list_prompts()) for prompt in prompts: print(prompt.conversation_prompt_id, prompt.prompt) - conversation.delete_prompt(prompts[-1].conversation_prompt_id) + if prompts: + conversation.delete_prompt(prompts[-1].conversation_prompt_id) conversation.remove_tag("PROJECT") The complete sample creates a conversation, stores one prompt through a chat @@ -206,10 +207,10 @@ tag creation, update, and removal: .. literalinclude:: ../../../samples/conversation_prompts_tags.py :language: python - :lines: 14- + :start-at: import The conversation ID, prompt ID, and model response vary between runs. -Representative output is: +Example output (generated IDs and model responses vary): output:: @@ -328,36 +329,77 @@ output:: 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. +Use prompt history to inspect or remove stored prompts, and tags to attach +application metadata to a conversation. This example first creates a +conversation and records a chat prompt so there is history to inspect. + +Create a conversation and save a prompt +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +With an async connection open, run the following inside an async function. +The saved profile ``oci_ai_profile`` must already exist: + +.. code-block:: python + + conversation = select_ai.AsyncConversation( + attributes=select_ai.ConversationAttributes(title="Prompt history demo") + ) + await conversation.create() + profile = await select_ai.AsyncProfile(profile_name="oci_ai_profile") + async with profile.chat_session(conversation=conversation) as session: + response = await session.chat("Explain retrieval augmented generation.") + print(response) + +Tag and inspect the conversation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A second ``add_tag()`` call with the same key updates its value. +``list_prompts()`` returns an async iterator of ``ConversationPrompt`` objects: .. code-block:: python await conversation.add_tag("PROJECT", "SELECT_AI") + await conversation.add_tag("PROJECT", "SELECT_AI_SAMPLES") 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) + +Remove a stored prompt and its tag +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Delete by prompt ID, then remove the tag by key. Check for an empty history +before choosing a prompt to delete: + +.. code-block:: python + + if prompts: + 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``: +Use ``force=True`` on ``delete_prompt()`` or ``remove_tag()`` when a missing +prompt or tag should be ignored. When finished with this temporary conversation, +call ``await conversation.delete(force=True)``. -.. literalinclude:: ../../../samples/async/conversation_prompts_tags.py - :language: python - :lines: 13- +Run the complete example +~~~~~~~~~~~~~~~~~~~~~~~~ -The conversation ID, prompt ID, and model response vary between runs. -Representative output is: +The `complete async prompt-history script `__ +adds connection setup and a ``finally`` block that deletes the temporary +conversation. Set the common database environment variables and optionally +set ``SELECT_AI_PROFILE_NAME`` (default ``oci_ai_profile``), then run from the +repository root: -output:: +.. code-block:: bash - conversation history works. - Prompt : Reply with exactly: conversation history works. - Prompts after deletion: [] + python samples/async/conversation_prompts_tags.py + +Example output (prompt IDs and model responses vary): + +.. code-block:: text + + conversation history works. + Prompt : Reply with exactly: conversation history works. + Prompts after deletion: [] .. latex:clearpage:: diff --git a/doc/source/user_guide/credential.rst b/doc/source/user_guide/credential.rst index f8c97cf..6836e7a 100644 --- a/doc/source/user_guide/credential.rst +++ b/doc/source/user_guide/credential.rst @@ -105,6 +105,26 @@ Create credential In this example, we create a credential object to authenticate to OCI Gen AI service provider: +The synchronous and asynchronous samples below use the OCI Python SDK to read +an existing API-signing configuration. Install this sample-specific dependency: + +.. code-block:: shell + + python -m pip install oci + +Configure the ``DEFAULT`` profile in ``~/.oci/config`` with ``user``, +``tenancy``, ``fingerprint``, ``region``, and ``key_file``. The ``key_file`` +must point to a readable private key whose public key is registered with the +OCI user. See `OCI SDK configuration +`__. +The samples call ``oci.config.from_file()`` with its default path and profile; +edit that call if you use another configuration. Keep the configuration and +private key outside your source repository. + +The ``oci`` package is needed only to load this configuration in the samples. +The Select AI credential API itself accepts the credential dictionary shown +above without requiring the OCI SDK. + 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. diff --git a/doc/source/user_guide/installation.rst b/doc/source/user_guide/installation.rst index f338f5b..d83f2cb 100644 --- a/doc/source/user_guide/installation.rst +++ b/doc/source/user_guide/installation.rst @@ -72,8 +72,15 @@ are isolated from your system Python installation. python -m pip install --upgrade "select_ai[cli]" - This installs the ``select-ai`` command and its A2A server support. See - :ref:`Command Line Interface `. + This installs the ``select-ai`` command with profile, SQL, and chat + commands. To use the A2A server and worker commands, install the separate + ``a2a`` extra: + + .. code-block:: shell + + python -m pip install --upgrade "select_ai[a2a]" + + See :ref:`Command Line Interface ` and :ref:`A2A Integration `. 6. If you are behind a proxy, use the ``--proxy`` option. For example: @@ -123,6 +130,9 @@ Oracle Database. Connected to the Database +Once the connection smoke test succeeds, follow :ref:`Run your first SQL +query ` to create a credential and profile and query sample data. + Install documentation dependencies ================================== diff --git a/doc/source/user_guide/introduction.rst b/doc/source/user_guide/introduction.rst index b6ce86f..2e182cc 100644 --- a/doc/source/user_guide/introduction.rst +++ b/doc/source/user_guide/introduction.rst @@ -14,26 +14,42 @@ Python ecosystem. It gives Python applications a higher-level API for working with AI providers, credentials, profiles, natural language prompts, vector indexes, conversations, summarization, synthetic data, and AI agent workflows. +.. only:: html + + .. figure:: /image/select_ai_a2a_server_demo.gif + :alt: Select AI A2A server demo from the project README + :width: 100% + + Connect an A2A client to a Select AI agent team. Follow the + :ref:`A2A guide ` to run the server. + +For a complete setup-to-query example, start with :ref:`Run your first SQL +query `. Existing users can review :ref:`What's new in 1.5.0 +` and :ref:`Compatibility `. + What you can build ================== -Use ``select_ai`` to: - -* Ask questions about database objects in natural language and generate SQL. -* Run generated SQL and return results as Python objects such as pandas data - frames. -* Generate narrative answers, explanations, prompt previews, translations, and - summaries. -* Use Retrieval Augmented Generation (RAG) with vector indexes over documents - and object storage content. -* Create synthetic data for database tables. -* Build context-aware chat sessions with database-backed conversations. -* Register tools, tasks, agents, and teams for database-backed AI agent - workflows. -* Use the optional ``select-ai`` command line interface for interactive chat - and SQL workflows. -* Use synchronous APIs, asynchronous APIs, and connection pools in scripts, - services, and web applications. +Start with the workflow you want to build: + +* **Natural language data apps:** generate SQL, return query results as pandas + DataFrames, or explain results in plain language with :ref:`Profiles ` + and :ref:`Async Profiles `. +* **RAG and chat assistants:** ground answers in your documents with + :ref:`Vector Indexes ` and keep chat context with + :ref:`Conversations `. +* **AI agent workflows:** compose tools, tasks, and agents into + :ref:`Teams `, then expose a team to other AI applications through + an :ref:`Agent2Agent (A2A) server `. +* **Python web services:** add Select AI endpoints to FastAPI, Flask, or Django + using the pool lifecycle patterns in :ref:`Web Frameworks `. +* **Concurrent prompt processing:** use threads, async tasks, queues, or a + database pipeline with the :ref:`Concurrency Guides `. +* **Content and test-data tools:** :ref:`summarize content `, + :ref:`translate text `, or generate + :ref:`Synthetic Data ` for database tables. +* **Terminal workflows:** chat, generate SQL, and run an A2A server with the + optional :ref:`Command Line Interface `. Core concepts ============= @@ -81,6 +97,11 @@ Most workflows use the same building blocks: - Define tools, tasks, agents, and teams for multi-step AI workflows. - :ref:`Agent ` + * - Agent2Agent (A2A) + - Expose an agent team as a discoverable service for A2A clients. Choose + standalone or clustered deployment and fixed or dynamic connections. + - :ref:`A2A server ` + Synchronous and asynchronous APIs ================================= diff --git a/doc/source/user_guide/privileges.rst b/doc/source/user_guide/privileges.rst index 920ddbe..f6cba55 100644 --- a/doc/source/user_guide/privileges.rst +++ b/doc/source/user_guide/privileges.rst @@ -103,6 +103,8 @@ output:: The async API is ``select_ai.async_grant_network_access(...)``. +.. latex:clearpage:: + .. literalinclude:: ../../../samples/async/grant_network_access.py :language: python :lines: 14- @@ -128,6 +130,8 @@ output:: The async API is ``select_ai.async_revoke_network_access(...)``. +.. latex:clearpage:: + .. literalinclude:: ../../../samples/async/revoke_network_access.py :language: python :lines: 14- diff --git a/doc/source/user_guide/profile.rst b/doc/source/user_guide/profile.rst index 8521790..6c9c565 100644 --- a/doc/source/user_guide/profile.rst +++ b/doc/source/user_guide/profile.rst @@ -338,6 +338,22 @@ 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``. +The SDK calls the database function before reading the returned CLOB in +chunks. This is CLOB consumption, rather than a guarantee that model tokens +arrive while generation is in progress. The first chunk is available only +after the database function returns. A partially consumed iterator keeps its +cursor and connection open. Close it if you stop early, and consume session +streams before leaving ``chat_session()``: + +.. code-block:: python + + from contextlib import closing + + with closing(profile.chat("Explain connection pooling.", stream=True)) as chunks: + for chunk in chunks: + print(chunk, end="") + # Breaking here also closes the iterator on context exit. + .. _request-profile-attributes: ***************************************** @@ -501,37 +517,129 @@ output:: Danke +.. latex:clearpage:: + +*************** +Share a profile +*************** + +While connected as the profile owner, grant access to an existing profile: + +.. code-block:: python + + profile = select_ai.Profile.fetch("OCI_AI_PROFILE") + profile.grant_access("APP_USER") + +The recipient can fetch the profile with ``owner="APP_OWNER"`` in a separate +connection. When access should end, run +``profile.revoke_access("APP_USER")`` as the owner. See +:ref:`Sharing and ownership ` for the complete owner and recipient +sequence. + +.. raw:: latex + + \Needspace{25\baselineskip} + +****************************** +Give feedback on generated SQL +****************************** + +Use feedback after reviewing the SQL produced for a prompt. Identify the +request with a ``(prompt, Action)`` tuple or a ``sql_id`` from +``V$MAPPED_SQL``. The example below assumes an open connection and an +existing profile over the ``SH`` schema: + +.. code-block:: python + + profile = select_ai.Profile.fetch("OCI_AI_PROFILE") + prompt = "How many promotions?" + sql = profile.show_sql(prompt=prompt) + print(sql) + +After inspecting the SQL, record positive feedback if it answers the question: + +.. code-block:: python + + profile.add_positive_feedback( + prompt_spec=(prompt, select_ai.Action.SHOWSQL) + ) + +For an incorrect answer, use ``add_negative_feedback()`` with the same request +identifier, ``response`` containing the expected SQL, and ``feedback_content`` +explaining the correction. Remove obsolete feedback with +``delete_feedback()`` and the same request identifier. + + .. latex:clearpage:: ************************** List profiles ************************** -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. 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. +Use ``Profile.list()`` to discover saved profiles in the current schema. +Call it on the class; you do not need to create an empty profile first. +Each returned object includes ``profile_name``, ``owner``, and the +owner-qualified ``qualified_name``. + +Connect and list your profiles +++++++++++++++++++++++++++++++ + +Set ``SELECT_AI_USER``, ``SELECT_AI_PASSWORD``, and +``SELECT_AI_DB_CONNECT_STRING`` before running this complete script. It prints +one qualified name per profile and closes the connection afterward: + +.. literalinclude:: ../../../samples/profiles_list.py + :language: python + :start-at: import + +For a schema named ``APP_OWNER`` containing two profiles, the output could be: + +.. code-block:: text + + APP_OWNER.OCI_AI_PROFILE + APP_OWNER.OCI_VECTOR_AI_PROFILE + +Your results depend on the profiles visible in your schema; result order is +not guaranteed. An empty result means no profiles match the requested scope. + +Filter by name +++++++++++++++ + +With a connection open, use ``profile_name_pattern`` to match names with a +regular expression. This example lists names starting with ``OCI_``: .. 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(profile_name_pattern="^OCI_"): + print(profile.qualified_name) + +.. raw:: latex + + \Needspace{27\baselineskip} + +Find and reuse a shared profile ++++++++++++++++++++++++++++++++ + +Pass ``owner`` to list profiles in another schema. The connected user must +have access to those profiles; specifying an owner does not grant access. +While connected as the recipient, list the profiles shared by ``APP_OWNER``: + +.. code-block:: python for profile in select_ai.Profile.list(owner="APP_OWNER"): print(profile.qualified_name) -.. literalinclude:: ../../../samples/profiles_list.py - :language: python - :lines: 14- +Once you know the profile name, fetch that profile with the same owner and +use the returned object directly: +.. code-block:: python -output:: + profile = select_ai.Profile.fetch( + "OCI_AI_PROFILE", owner="APP_OWNER" + ) + print(profile.qualified_name) # APP_OWNER.OCI_AI_PROFILE + response = profile.chat(prompt="What is retrieval augmented generation?") + print(response) - ASYNC_OCI_AI_PROFILE - OCI_VECTOR_AI_PROFILE - ASYNC_OCI_VECTOR_AI_PROFILE - OCI_AI_PROFILE +See :ref:`Sharing and ownership ` for the owner's grant and revoke +steps. diff --git a/doc/source/user_guide/profile_attributes.rst b/doc/source/user_guide/profile_attributes.rst index 047132a..03c95f4 100644 --- a/doc/source/user_guide/profile_attributes.rst +++ b/doc/source/user_guide/profile_attributes.rst @@ -91,13 +91,13 @@ Attribute groups Object list examples ==================== -Grant access to every supported object in a schema: +Include supported objects from a schema in the profile's metadata scope: .. code-block:: python object_list = [{"owner": "SH"}] -Grant access to selected tables: +Include selected tables: .. code-block:: python @@ -107,6 +107,9 @@ Grant access to selected tables: {"owner": "SH", "name": "PRODUCTS"}, ] +The object list does not grant database privileges. The connected user must +already have the privileges needed to access the underlying objects. + Restrict generated SQL to the selected objects: .. code-block:: python diff --git a/doc/source/user_guide/provider.rst b/doc/source/user_guide/provider.rst index 200b71c..e0ee69a 100644 --- a/doc/source/user_guide/provider.rst +++ b/doc/source/user_guide/provider.rst @@ -53,11 +53,16 @@ class in this library. - Uses OCI region and OCI Gen AI attributes. * - ``OpenAIProvider`` - ``openai`` - - Uses ``api.openai.com``. + - Keeps ``api.openai.com`` for network ACL setup; omits the default + endpoint from database profiles so Select AI selects its native URL. Examples -------- +.. raw:: latex + + \Needspace{14\baselineskip} + OCI Gen AI provider: .. code-block:: python @@ -68,6 +73,10 @@ OCI Gen AI provider: model="cohere.command-r-plus", ) +.. raw:: latex + + \Needspace{14\baselineskip} + OpenAI provider: .. code-block:: python @@ -76,6 +85,17 @@ OpenAI provider: model="gpt-4.1", ) +For the public OpenAI service, leave ``provider_endpoint`` at its default. +The SDK sends ``provider="openai"`` and the model name, without an endpoint +override. ``OpenAIProvider`` omits ``provider_endpoint`` from database profile +payloads. Use the base ``Provider`` class for a custom OpenAI-compatible +endpoint, as shown below. Do not append ``/v1/chat/completions``; the database +constructs the request path. + +.. raw:: latex + + \Needspace{14\baselineskip} + Azure OpenAI provider: .. code-block:: python @@ -86,6 +106,10 @@ Azure OpenAI provider: azure_embedding_deployment_name="text-embedding-deployment", ) +.. raw:: latex + + \Needspace{14\baselineskip} + AWS Bedrock provider: .. code-block:: python @@ -96,6 +120,8 @@ AWS Bedrock provider: model="anthropic.claude-3-5-sonnet-20240620-v1:0", ) +.. latex:clearpage:: + Custom provider endpoint: .. code-block:: python @@ -131,6 +157,104 @@ Custom provider endpoint: prompt="How many customers do I have?", ) +.. latex:clearpage:: + +``Provider`` +------------ + +.. autoclass:: select_ai.Provider + :members: + +.. latex:clearpage:: + +``AnthropicProvider`` +--------------------- +.. autoclass:: select_ai.AnthropicProvider + :members: + +.. latex:clearpage:: + +``AzureProvider`` +----------------- +.. autoclass:: select_ai.AzureProvider + :members: + +.. latex:clearpage:: + +``AWSProvider`` +--------------- +.. autoclass:: select_ai.AWSProvider + :members: + +.. latex:clearpage:: + +``CohereProvider`` +------------------ +.. autoclass:: select_ai.CohereProvider + :members: + +.. latex:clearpage:: + +``OpenAIProvider`` +------------------ +.. autoclass:: select_ai.OpenAIProvider + :members: + +.. latex:clearpage:: + +``OCIGenAIProvider`` +-------------------- +.. autoclass:: select_ai.OCIGenAIProvider + :members: + +.. latex:clearpage:: + +``GoogleProvider`` +------------------ +.. autoclass:: select_ai.GoogleProvider + :members: + +.. latex:clearpage:: + +``HuggingFaceProvider`` +----------------------- +.. autoclass:: select_ai.HuggingFaceProvider + :members: + +.. latex:clearpage:: + +Enable AI service provider +-------------------------- + +Enable using Sync API ++++++++++++++++++++++ + +This method adds ACL allowing database users to invoke AI provider's +HTTP endpoint. For non-HTTP or port-specific network access, use the network +ACL helpers described in :ref:`Privileges `. + +.. literalinclude:: ../../../samples/enable_ai_provider.py + :language: python + :lines: 14- + +output:: + + Enabled AI provider for user: + +.. latex:clearpage:: + +Enable using Async API +++++++++++++++++++++++ +.. literalinclude:: ../../../samples/async/enable_ai_provider.py + :language: python + :lines: 14- + +output:: + + Enabled AI provider for user: + +.. latex:clearpage:: + Cloud provider profile samples ------------------------------ @@ -153,6 +277,8 @@ 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. +.. latex:clearpage:: + AWS Bedrock +++++++++++ @@ -169,9 +295,21 @@ Set ``AWS_ACCESS_KEY_ID`` and ``AWS_SECRET_ACCESS_KEY`` before running it: python samples/profile_create_aws.py +First, load the configuration and grant network access as the administrator: + +.. literalinclude:: ../../../samples/profile_create_aws.py + :language: python + :start-at: import os + :end-before: select_ai.connect(user=app_user, password=app_password, dsn=dsn) + +.. latex:clearpage:: + +Then connect as the application user to create the credential and profile. +Append this code to the setup above: + .. literalinclude:: ../../../samples/profile_create_aws.py :language: python - :lines: 8- + :start-at: select_ai.connect(user=app_user, password=app_password, dsn=dsn) The sample creates ``aws_bedrock_meta_prf`` and prints the saved attributes and a test response: @@ -182,6 +320,8 @@ output:: {'credential_name': 'AWS_CRED', 'provider': AWSProvider(...), ...} Chat response: AWS chat succeeded. +.. latex:clearpage:: + Azure OpenAI ++++++++++++ @@ -198,9 +338,21 @@ Set ``AZURE_API_KEY`` before running it: python samples/profile_create_azure.py +First, load the configuration and grant network access as the administrator: + .. literalinclude:: ../../../samples/profile_create_azure.py :language: python - :lines: 8- + :start-at: import os + :end-before: select_ai.connect(user=app_user, password=app_password, dsn=dsn) + +.. latex:clearpage:: + +Then connect as the application user to create the credential and profile. +Append this code to the setup above: + +.. literalinclude:: ../../../samples/profile_create_azure.py + :language: python + :start-at: select_ai.connect(user=app_user, password=app_password, dsn=dsn) The sample creates ``azureai_prf``, fetches it to verify the saved profile, and prints a test response: @@ -212,6 +364,8 @@ output:: {'credential_name': 'AZUREAI_CRED', 'provider': AzureProvider(...), ...} Chat response: Azure chat succeeded. +.. latex:clearpage:: + Google Gemini (GCP) +++++++++++++++++++ @@ -227,114 +381,30 @@ Set ``GOOGLE_API_KEY`` before running it: python samples/profile_create_gcp.py +First, load the configuration and grant network access as the administrator: + .. 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`` ------------- - -.. autoclass:: select_ai.Provider - :members: - -.. latex:clearpage:: - -``AnthropicProvider`` ---------------------- -.. autoclass:: select_ai.AnthropicProvider - :members: - -.. latex:clearpage:: - -``AzureProvider`` ------------------ -.. autoclass:: select_ai.AzureProvider - :members: - -.. latex:clearpage:: - -``AWSProvider`` ---------------- -.. autoclass:: select_ai.AWSProvider - :members: - -.. latex:clearpage:: - -``CohereProvider`` ------------------- -.. autoclass:: select_ai.CohereProvider - :members: - -.. latex:clearpage:: - -``OpenAIProvider`` ------------------- -.. autoclass:: select_ai.OpenAIProvider - :members: - -.. latex:clearpage:: - -``OCIGenAIProvider`` --------------------- -.. autoclass:: select_ai.OCIGenAIProvider - :members: - -.. latex:clearpage:: - -``GoogleProvider`` ------------------- -.. autoclass:: select_ai.GoogleProvider - :members: - -.. latex:clearpage:: - -``HuggingFaceProvider`` ------------------------ -.. autoclass:: select_ai.HuggingFaceProvider - :members: + :start-at: import os + :end-before: select_ai.connect(user=app_user, password=app_password, dsn=dsn) .. latex:clearpage:: -Enable AI service provider --------------------------- - -Enable using Sync API -+++++++++++++++++++++ +Then connect as the application user to create the credential and profile. +Append this code to the setup above: -This method adds ACL allowing database users to invoke AI provider's -HTTP endpoint. For non-HTTP or port-specific network access, use the network -ACL helpers described in :ref:`Privileges `. - -.. literalinclude:: ../../../samples/enable_ai_provider.py +.. literalinclude:: ../../../samples/profile_create_gcp.py :language: python - :lines: 14- + :start-at: select_ai.connect(user=app_user, password=app_password, dsn=dsn) -output:: - - Enabled AI provider for user: - -.. latex:clearpage:: - -Enable using Async API -++++++++++++++++++++++ -.. literalinclude:: ../../../samples/async/enable_ai_provider.py - :language: python - :lines: 14- +The sample creates ``google_gemini_3_6_flash`` and prints the saved attributes +and a test response: output:: - Enabled AI provider for user: + Created profile: google_gemini_3_6_flash + {'credential_name': 'GOOGLE_CRED', 'provider': GoogleProvider(...), ...} + Chat response: GCP chat succeeded. .. latex:clearpage:: diff --git a/doc/source/user_guide/quickstart.rst b/doc/source/user_guide/quickstart.rst new file mode 100644 index 0000000..7c51895 --- /dev/null +++ b/doc/source/user_guide/quickstart.rst @@ -0,0 +1,144 @@ +.. _first-query: + +************************ +Run your first SQL query +************************ + +This walkthrough creates a temporary table with three rows, stores an OpenAI +API key in a temporary database credential, creates a Select AI profile, and +queries the table using natural language. It removes its table, credential, +and profile when finished. Use a development database account. + +Requests use the API key in ``OPENAI_API_KEY`` on each run. Existing database +credentials are not used or replaced. OpenAI API usage is billed separately +from ChatGPT and Codex subscriptions. + +Install and configure Python +============================ + +Use Python 3.11 through 3.14 and an Oracle Database environment with +``DBMS_CLOUD_AI`` available. See :ref:`Compatibility ` for +feature-specific requirements. + +.. code-block:: bash + + python3 -m venv .venv + source .venv/bin/activate + python -m pip install --upgrade pip + python -m pip install 'select_ai==1.5.0' + +On Windows, use the virtual-environment activation command in +:ref:`Installation `. + +Set the following values in your shell. Choose an OpenAI model supported by +your database integration and available to your API project: + +.. code-block:: bash + + export SELECT_AI_USER='your_database_user' + export SELECT_AI_PASSWORD='your_database_password' + export SELECT_AI_DB_CONNECT_STRING='your_database_connect_string' + export OPENAI_API_KEY='your_api_key' + export SELECT_AI_MODEL='gpt-4.1' + +To try a different key, replace ``OPENAI_API_KEY`` and rerun the script. Keep +keys out of source files and version control. OCI-specific environment +variables are not read by this sample. + +For wallet connections, add the parameters described in +:ref:`Wallet connections ` to the +sample's ``connect()`` call. Do not commit secrets to the repository. + +Prepare the database account +============================ + +A database administrator must grant the application user Select AI package +privileges and outbound HTTP access to ``api.openai.com``. Pass ``--setup`` to +perform both steps before running the example. Set the administrator variables +in addition to the application and OpenAI variables above: + +.. code-block:: bash + + export SELECT_AI_ADMIN_USER='your_database_admin_user' + export SELECT_AI_ADMIN_PASSWORD='your_database_admin_password' + python samples/quickstart.py --setup + +The script connects as the administrator, calls ``grant_privileges()`` and +``grant_network_access(users=..., host="api.openai.com", privileges="http")``, +commits the setup, and disconnects. It then connects as the application user +and runs the example. The administrator must be allowed to grant package +privileges and manage network ACLs. Administrator and application connections +use the same DSN; add required wallet arguments to both connection calls. + +Run setup once for each application user/database. For later runs, omit the +flag; administrator credentials are then unnecessary: + +.. code-block:: bash + + python samples/quickstart.py + +The grants remain after the temporary sample objects are removed. See +:ref:`Privileges ` to revoke package or network privileges later. + +The application account must also have ``CREATE SESSION``, ``CREATE TABLE``, +and sufficient tablespace quota to create the temporary table. Have the +administrator assign an appropriate quota in the account's default tablespace. +The helper does not grant these privileges or quota. The sample does not +create a public credential synonym and does not require ``CREATE PUBLIC +SYNONYM``. + +Run the example +=============== + +Save the following as ``quickstart.py``, or run +``python samples/quickstart.py`` from a repository checkout: + +.. literalinclude:: ../../../samples/quickstart.py + :language: python + :start-at: import argparse + +The script prints generated SQL and a DataFrame containing a count of three. +Table names include a generated suffix. SQL formatting and the model's chosen +column label may vary; representative result output is: + +.. code-block:: text + + ITEM_COUNT + 0 3 + +The profile's ``object_list`` includes the sample table using the schema name +resolved from the database connection. Other profile attributes use their +defaults, including object-list enforcement. The object list supplies metadata +for SQL generation; database privileges determine which objects the user can +access. See :ref:`Profile attributes ` for explicit +enforcement settings. + +``show_sql()`` generates SQL without executing it. ``run_sql()`` makes a +separate generation request and executes that response; it does not execute +the SQL string returned by the earlier call. In applications that require +approval of an exact statement, review the generated statement and execute +that approved statement through your database API. + +If connection fails, check the DSN, password, and wallet settings. For package +or network authorization failures, return to the administrator setup. For +model or provider errors, check the API key, model access, API billing, and +project limits. See :ref:`Error handling ` for Python exception +handling. The sample's cleanup removes only objects it successfully created; +administrator grants remain in place for subsequent workflows. + +Waiting and timeouts +==================== + +The script prints progress before connecting, creating objects, and requesting +SQL from OpenAI. If it waits at ``Generating SQL with OpenAI...``, the +database is processing the model request. Missing network ACL privileges usually raise +an authorization error; waiting alone does not prove that the ACL is missing. +Check database egress, provider availability, API billing, and project limits. +HTTP 429 can indicate quota or rate limits; retries may extend the wait. + +The sample sets ``tcp_connect_timeout=15`` for connection establishment and +``connection.call_timeout=120_000`` for database calls. The latter is in +milliseconds and limits each database round trip, rather than total script +runtime. Increase it deliberately for slower requests. See the +`driver timeout documentation +`__. diff --git a/doc/source/user_guide/sharing.rst b/doc/source/user_guide/sharing.rst index e05af91..a041802 100644 --- a/doc/source/user_guide/sharing.rst +++ b/doc/source/user_guide/sharing.rst @@ -4,135 +4,145 @@ 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. +Share a saved profile, vector index, agent team, or credential with a database +user or role. Run grants and revokes while connected as the object owner. The +recipient also needs the database and package :ref:`Privileges ` +required to use that object. -The sharing APIs are available in both synchronous and asynchronous forms: +The examples below use owner ``APP_OWNER`` and recipient ``APP_USER``. Replace +these names with your schemas. Each example assumes that its named object +already exists. Sharing an object does not grant access to other database +objects or services used by the workflow. -.. 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. +Connect before running an example +================================= + +For synchronous examples, connect as the object owner: + +.. code-block:: python + + import os + import select_ai + + select_ai.connect( + user=os.environ["SELECT_AI_USER"], # APP_OWNER + password=os.environ["SELECT_AI_PASSWORD"], + dsn=os.environ["SELECT_AI_DB_CONNECT_STRING"], + ) + +Call ``select_ai.disconnect()`` when finished. For asynchronous examples, use +``await select_ai.async_connect(...)`` with the same arguments inside an async +function, and call ``await select_ai.async_disconnect()`` when finished. +The async snippets below belong inside that function. + +.. latex:clearpage:: 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``. +Grant access to an existing profile while connected as ``APP_OWNER``: .. code-block:: python - profile = select_ai.Profile.fetch( - "OCI_AI_PROFILE", - owner="APP_OWNER", - ) - print(profile.owner) - print(profile.qualified_name) + profile = select_ai.Profile.fetch("OCI_AI_PROFILE") + profile.grant_access("APP_USER") - profiles = select_ai.Profile.list( - profile_name_pattern="^OCI_AI_PROFILE$", - owner="APP_OWNER", - ) - for profile in profiles: - print(profile.qualified_name) +In a separate connection as ``APP_USER``, fetch the shared profile by its name +and owner. The ``qualified_name`` property identifies both: + +.. code-block:: python -Grant and revoke access on the owner-side object: + profile = select_ai.Profile.fetch("OCI_AI_PROFILE", owner="APP_OWNER") + print(profile.qualified_name) + print(profile.chat(prompt="What is retrieval augmented generation?")) + +The qualified name printed is ``APP_OWNER.OCI_AI_PROFILE``. The chat answer +depends on the configured provider. To withdraw access, reconnect as +``APP_OWNER`` and run: .. code-block:: python - profile.grant_access("APP_USER") + profile = select_ai.Profile.fetch("OCI_AI_PROFILE") profile.revoke_access("APP_USER") -The asynchronous APIs use the same ``owner`` argument and return an async -iterator from ``AsyncProfile.list()``: +The async sequence uses ``AsyncProfile``. Run each step in the indicated +user's connection: .. code-block:: python - profile = await select_ai.AsyncProfile.fetch( - "OCI_AI_PROFILE", - owner="APP_OWNER", + # As APP_OWNER: share the profile. + profile = await select_ai.AsyncProfile.fetch("OCI_AI_PROFILE") + await profile.grant_access("APP_USER") + + # In a separate connection as APP_USER: use the shared profile. + shared = 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]) + print(shared.qualified_name) + print(await shared.chat(prompt="What is retrieval augmented generation?")) - await profile.grant_access("APP_USER") + # Reconnect as APP_OWNER when access should end. await profile.revoke_access("APP_USER") +``Profile.list()`` and ``AsyncProfile.list()`` also accept ``owner``. If it is +omitted, they list the current schema. See +:ref:`List profiles ` and +:ref:`List profiles asynchronously ` +for connection setup, filtering, and reuse. + +.. latex:clearpage:: + 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. +Share an existing vector index while connected as ``APP_OWNER``: .. 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 = select_ai.VectorIndex.fetch("PRODUCT_DOCS") index.grant_access("APP_USER") + +In a separate connection as ``APP_USER``, address the index by its owner: + +.. code-block:: python + + index = select_ai.VectorIndex.fetch("PRODUCT_DOCS", owner="APP_OWNER") + print(index.qualified_name) # APP_OWNER.PRODUCT_DOCS + +Reconnect as ``APP_OWNER`` to withdraw access: + +.. code-block:: python + + index = select_ai.VectorIndex.fetch("PRODUCT_DOCS") index.revoke_access("APP_USER") -The async form mirrors the synchronous API: +Use ``AsyncVectorIndex`` for the same sequence in an async application: .. 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", - ) - ] + # As APP_OWNER. + index = await select_ai.AsyncVectorIndex.fetch("PRODUCT_DOCS") await index.grant_access("APP_USER") + + # In a separate connection as APP_USER. + shared = await select_ai.AsyncVectorIndex.fetch( + "PRODUCT_DOCS", owner="APP_OWNER" + ) + print(shared.qualified_name) + + # Reconnect as APP_OWNER when access should end. await index.revoke_access("APP_USER") +``VectorIndex.list(owner="APP_OWNER")`` lists visible indexes in that schema; +``AsyncVectorIndex.list(owner="APP_OWNER")`` returns an async iterator. +See :ref:`Vector Index ` for creating and using an index for RAG. + +.. latex:clearpage:: + Agent teams =========== -Agent teams support synchronous and asynchronous access grants and revokes: +While connected as the team owner, grant access to an existing team: .. code-block:: python @@ -140,119 +150,109 @@ Agent teams support synchronous and asynchronous access grants and revokes: team = Team.fetch("MOVIE_AGENT_TEAM") team.grant_access("APP_USER") + +When the recipient no longer needs access, run this as the team owner: + +.. code-block:: python + team.revoke_access("APP_USER") +For async applications, use ``AsyncTeam`` and await each operation: + .. code-block:: python from select_ai.agent import AsyncTeam team = await AsyncTeam.fetch("MOVIE_AGENT_TEAM") await team.grant_access("APP_USER") + + # Revoke later, while connected as the team owner. 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. +Team fetch and list operations use the connected user's agent-team views and +do not accept an ``owner`` argument. See :ref:`AI Agent ` for team +creation and execution. + +.. latex:clearpage:: Credentials =========== -Credential access is exposed as module-level functions because credentials are -owned in the current schema: +Credentials use module-level access functions. While connected as the +credential owner, share an existing provider credential: .. code-block:: python - select_ai.grant_credential_access( - "MY_PROVIDER_CREDENTIAL", - "APP_USER", - ) - select_ai.revoke_credential_access( - "MY_PROVIDER_CREDENTIAL", - "APP_USER", - ) + select_ai.grant_credential_access("MY_PROVIDER_CREDENTIAL", "APP_USER") + +Revoke access when the recipient no longer needs it: + +.. code-block:: python + + select_ai.revoke_credential_access("MY_PROVIDER_CREDENTIAL", "APP_USER") -Use the asynchronous equivalents with ``await``: +The async functions take the same arguments: .. code-block:: python await select_ai.async_grant_credential_access( - "MY_PROVIDER_CREDENTIAL", - "APP_USER", + "MY_PROVIDER_CREDENTIAL", "APP_USER" ) + + # Revoke later, while connected as the credential owner. await select_ai.async_revoke_credential_access( - "MY_PROVIDER_CREDENTIAL", - "APP_USER", + "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: +Access functions return ``None`` after the grant or revoke completes. They do +not print a confirmation message. -.. code-block:: python +Credential creation and deletion can also manage a public synonym with the +same name. Pass ``public_synonym=True`` to ``create_credential()`` or +``delete_credential()`` (or their async equivalents). Creating or dropping the +synonym requires ``CREATE PUBLIC SYNONYM`` or ``DROP PUBLIC SYNONYM`` +respectively. See :ref:`Credential ` for credential setup. - select_ai.create_credential( - credential=credential, - replace=True, - public_synonym=True, - ) - select_ai.delete_credential( - "MY_PROVIDER_CREDENTIAL", - force=True, - public_synonym=True, - ) +Runnable sharing scripts +========================= -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 +The repository also provides a +`synchronous sharing script `__ +and an +`asynchronous sharing script `__ +that exercise all four object types. Use these when you want to check the APIs +together after working through the individual examples above. + +Set the common database connection variables and ``SELECT_AI_SHARE_GRANTEE``. +The connected user must own all four objects. The scripts use these defaults; +override them if your objects have other names: + +.. list-table:: Sharing script configuration + :header-rows: 1 + :widths: 48 52 + + * - Environment variable + - Default + * - ``SELECT_AI_PROFILE_NAME`` + - ``oci_ai_profile`` (sync); ``async_oci_ai_profile`` (async) + * - ``SELECT_AI_VECTOR_INDEX_NAME`` + - ``test_vector_index`` + * - ``SELECT_AI_TEAM_NAME`` + - ``MOVIE_AGENT_TEAM`` + * - ``SELECT_AI_CREDENTIAL_NAME`` + - ``my_oci_ai_profile_key`` + * - ``SELECT_AI_OWNER`` + - Current schema; if set, use the connected owner's schema. + +Run either script from the repository root: + +.. code-block:: bash + + export SELECT_AI_SHARE_GRANTEE=APP_USER + python samples/sharing.py + # Or run the async version: + python samples/async/sharing.py + +Each script prints the owner-qualified profile and vector-index names, then +confirms each grant and revoke. The scripts revoke every grant they make; +use the individual examples above to leave an object shared for application use. diff --git a/doc/source/user_guide/vector_index.rst b/doc/source/user_guide/vector_index.rst index 689fd60..9e689ec 100644 --- a/doc/source/user_guide/vector_index.rst +++ b/doc/source/user_guide/vector_index.rst @@ -196,6 +196,35 @@ objects used to create embeddings reside in OCI Object Storage. The profile uses an OCI Generative AI provider with an embedding model, and the vector index is linked to that profile during ``create()``. +Upload ``samples/data/refund-policy.txt`` from the repository to a bucket you +can read. The document says customers have 30 calendar days to request a +refund. Configure the sample with the URI of that uploaded file and the name +of an existing database credential authorized to read it: + +.. code-block:: bash + + export SELECT_AI_DOCUMENT_URI='https://objectstorage.REGION.oraclecloud.com/n/NAMESPACE/b/BUCKET/o/refund-policy.txt' + export SELECT_AI_OBJECT_STORAGE_CREDENTIAL='OBJECT_STORE_CRED' + export SELECT_AI_PROVIDER_CREDENTIAL='my_oci_ai_profile_key' + +Replace ``REGION``, ``NAMESPACE``, and ``BUCKET`` with your storage values. +Use the common database connection variables from :ref:`Connection `. +The provider credential and storage credential can be the same credential +when it has both permissions. Optionally set ``SELECT_AI_OCI_REGION`` and +``SELECT_AI_EMBEDDING_MODEL`` for your OCI configuration. The default region +is ``us-chicago-1`` and embedding model is ``cohere.embed-v4.0``; choose a model +available in your account and region. + +The creation samples replace ``test_vector_index`` and their named RAG +profile. Use a development schema, or edit these names before running if you +need to preserve existing definitions. The samples close their database +connections but leave the profile and index available for the next step. + +From the repository root, run ``python samples/vector_index_create.py``. +For async, use ``python samples/async/vector_index_create.py``. Both examples +wait for the initial load to complete. Run either workflow in its own schema, +or delete the existing sample index before switching between them. + .. literalinclude:: ../../../samples/vector_index_create.py :language: python :lines: 14- @@ -213,10 +242,8 @@ List vector index :language: python :lines: 15- -output:: - - Vector index TEST_VECTOR_INDEX - Vector index profile Profile(profile_name=oci_vector_ai_profile, attributes=ProfileAttributes(annotations=None, case_sensitive_values=None, comments=None, constraints=None, conversation=None, credential_name='my_oci_ai_profile_key', enable_sources=None, enable_source_offsets=None, enforce_object_list=None, max_tokens=1024, object_list=None, object_list_mode=None, provider=OCIGenAIProvider(embedding_model=None, model=None, provider_name='oci', provider_endpoint=None, region='us-chicago-1', oci_apiformat='GENERIC', oci_compartment_id=None, oci_endpoint_id=None, oci_runtimetype=None), seed=None, stop_tokens=None, streaming=None, temperature=None, vector_index_name='test_vector_index'), description=None) +The script prints each matching index name and its linked profile. Results +depend on the indexes visible to the connected user. .. latex:clearpage:: @@ -243,14 +270,13 @@ or update it without recreating the original Python object. :language: python :lines: 14- -output:: - - OracleVectorIndexAttributes(chunk_size=1024, chunk_overlap=128, location='https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph', match_limit=5, object_storage_credential_name='my_oci_ai_profile_key', profile_name='oci_vector_ai_profile', refresh_rate=1450, similarity_threshold=0.5, vector_distance_metric='COSINE', vector_db_endpoint=None, vector_db_credential_name=None, vector_db_provider=, vector_dimension=None, vector_table_name=None, pipeline_name='TEST_VECTOR_INDEX$VECPIPELINE') - - Profile(profile_name=oci_vector_ai_profile, attributes=ProfileAttributes(annotations=None, case_sensitive_values=None, comments=None, constraints=None, conversation=None, credential_name='my_oci_ai_profile_key', enable_custom_source_uri=None, enable_sources=None, enable_source_offsets=None, enforce_object_list=None, max_tokens=1024, object_list=None, object_list_mode=None, provider=OCIGenAIProvider(embedding_model='cohere.embed-english-v3.0', model=None, provider_name='oci', provider_endpoint=None, region='us-chicago-1', oci_apiformat='GENERIC', oci_compartment_id=None, oci_endpoint_id=None, oci_runtimetype=None), seed=None, stop_tokens=None, streaming=None, temperature=None, vector_index_name='test_vector_index'), description=MY OCI AI Profile) +The script prints the saved index attributes, its linked profile, and the next +refresh timestamp. The URI and credentials reflect your configuration; the +next refresh can be ``None`` when no refresh execution has been recorded. .. latex:clearpage:: + Update vector index attributes ++++++++++++++++++++++++++++++ @@ -262,12 +288,12 @@ and ``set_attributes()`` when updating several values together. :language: python :lines: 14- -output:: - - OracleVectorIndexAttributes(chunk_size=1024, chunk_overlap=128, location='https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph', match_limit=5, object_storage_credential_name='my_oci_ai_profile_key', profile_name='oci_vector_ai_profile', refresh_rate=1450, similarity_threshold=0.5, vector_distance_metric='COSINE', vector_db_endpoint=None, vector_db_credential_name=None, vector_db_provider=, vector_dimension=None, vector_table_name=None, pipeline_name='TEST_VECTOR_INDEX$VECPIPELINE') +The script sets ``refresh_rate`` to 1450 minutes and ``similarity_threshold`` +to 0.5, then prints the attributes held by the Python object. .. latex:clearpage:: + RAG using vector index ++++++++++++++++++++++ @@ -280,24 +306,23 @@ answer in the indexed content. :language: python :lines: 14- -output:: +The answer should identify the **30 calendar day** refund window. Exact +wording and source formatting depend on the provider and profile settings. +For example: - The conda environments in your object store are: - 1. fccenv - 2. myrenv - 3. fully-loaded-mlenv - 4. graphenv +output:: - These environments are listed in the provided data as separate JSON documents, each containing information about a specific conda environment. + Customers can request a refund within 30 calendar days of purchase. - Sources: - - fccenv-manifest.json (https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph/fccenv-manifest.json) - - myrenv-manifest.json (https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph/myrenv-manifest.json) - - fully-loaded-mlenv-manifest.json (https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph/fully-loaded-mlenv-manifest.json) - - graphenv-manifest.json (https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph/graphenv-manifest.json) +If no answer is grounded in the policy, verify that the uploaded URI and +storage credential are correct, that the initial load completed, and that +this profile is linked to ``test_vector_index``. Run the corresponding RAG +script after creating the index. See `Delete vector index`_ to remove the +sample index and its data; delete the sample profile separately when finished. .. latex:clearpage:: + Delete vector index +++++++++++++++++++ @@ -359,7 +384,7 @@ Async create vector index output:: - created vector index: test_vector_index + Created vector index: test_vector_index .. latex:clearpage:: @@ -371,11 +396,8 @@ Async list vector index :language: python :lines: 15- -output:: - - Vector index TEST_VECTOR_INDEX - Vector index profile AsyncProfile(profile_name=oci_vector_ai_profile, attributes=ProfileAttributes(annotations=None, case_sensitive_values=None, comments=None, constraints=None, conversation=None, credential_name='my_oci_ai_profile_key', enable_sources=None, enable_source_offsets=None, enforce_object_list=None, max_tokens=1024, object_list=None, object_list_mode=None, provider=OCIGenAIProvider(embedding_model=None, model=None, provider_name='oci', provider_endpoint=None, region='us-chicago-1', oci_apiformat='GENERIC', oci_compartment_id=None, oci_endpoint_id=None, oci_runtimetype=None), seed=None, stop_tokens=None, streaming=None, temperature=None, vector_index_name='test_vector_index'), description=None) - +The script prints matching index names and their linked profiles. Results +depend on your schema and configuration. .. latex:clearpage:: @@ -390,14 +412,12 @@ the class method ``AsyncVectorIndex.fetch(index_name)`` :language: python :lines: 14- -output:: - - OracleVectorIndexAttributes(chunk_size=1024, chunk_overlap=128, location='https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph', match_limit=5, object_storage_credential_name='my_oci_ai_profile_key', profile_name='oci_vector_ai_profile', refresh_rate=1450, similarity_threshold=0.5, vector_distance_metric='COSINE', vector_db_endpoint=None, vector_db_credential_name=None, vector_db_provider=, vector_dimension=None, vector_table_name=None, pipeline_name='TEST_VECTOR_INDEX$VECPIPELINE') - - AsyncProfile(profile_name=oci_vector_ai_profile, attributes=ProfileAttributes(annotations=None, case_sensitive_values=None, comments=None, constraints=None, conversation=None, credential_name='my_oci_ai_profile_key', enable_custom_source_uri=None, enable_sources=None, enable_source_offsets=None, enforce_object_list=None, max_tokens=1024, object_list=None, object_list_mode=None, provider=OCIGenAIProvider(embedding_model='cohere.embed-english-v3.0', model=None, provider_name='oci', provider_endpoint=None, region='us-chicago-1', oci_apiformat='GENERIC', oci_compartment_id=None, oci_endpoint_id=None, oci_runtimetype=None), seed=None, stop_tokens=None, streaming=None, temperature=None, vector_index_name='test_vector_index'), description=MY OCI AI Profile) +The script prints the saved attributes and linked profile. Source URIs, +credentials, and other values depend on the index you created. .. latex:clearpage:: + Async update vector index attributes ++++++++++++++++++++++++++++++++++++ @@ -408,12 +428,12 @@ To update attributes, use either ``async_vector_index.set_attribute()`` or :language: python :lines: 14- -output:: - - OracleVectorIndexAttributes(chunk_size=1024, chunk_overlap=128, location='https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph', match_limit=5, object_storage_credential_name='my_oci_ai_profile_key', profile_name='oci_vector_ai_profile', refresh_rate=1450, similarity_threshold=0.5, vector_distance_metric='COSINE', vector_db_endpoint=None, vector_db_credential_name=None, vector_db_provider=, vector_dimension=None, vector_table_name=None, pipeline_name='TEST_VECTOR_INDEX$VECPIPELINE') +The script updates the refresh rate and similarity threshold, then prints +the attributes held by the Python object. .. latex:clearpage:: + Async RAG using vector index ++++++++++++++++++++++++++++ @@ -421,18 +441,48 @@ Async RAG using vector index :language: python :lines: 15- +The answer should identify the **30 calendar day** refund window. Exact +wording and source formatting depend on the provider and profile settings. +For example: + output:: - The conda environments in your object store are: - 1. fccenv - 2. myrenv - 3. fully-loaded-mlenv - 4. graphenv + Customers can request a refund within 30 calendar days of purchase. + +If no answer is grounded in the policy, verify that the uploaded URI and +storage credential are correct, that the initial load completed, and that +this profile is linked to ``test_vector_index``. Run the corresponding RAG +script after creating the index. See `Delete vector index`_ to remove the +sample index and its data; delete the sample profile separately when finished. + +.. latex:clearpage:: + + +Share a vector index +==================== + +While connected as the index owner, grant access to an existing index: + +.. code-block:: python + + index = select_ai.VectorIndex.fetch("PRODUCT_DOCS") + index.grant_access("APP_USER") + +In a separate connection as the recipient, use +``VectorIndex.fetch("PRODUCT_DOCS", owner="APP_OWNER")``. It returns an object +whose ``qualified_name`` is ``APP_OWNER.PRODUCT_DOCS``. When access should end, +run ``index.revoke_access("APP_USER")`` as the owner. + +For an async application, use ``AsyncVectorIndex`` and await each operation: + +.. code-block:: python + + # Run as the index owner. + index = await select_ai.AsyncVectorIndex.fetch("PRODUCT_DOCS") + await index.grant_access("APP_USER") - These environments are listed in the provided data as separate JSON documents, each containing information about a specific conda environment. + # Run later as the index owner, when access should end. + await index.revoke_access("APP_USER") - Sources: - - fccenv-manifest.json (https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph/fccenv-manifest.json) - - myrenv-manifest.json (https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph/myrenv-manifest.json) - - fully-loaded-mlenv-manifest.json (https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph/fully-loaded-mlenv-manifest.json) - - graphenv-manifest.json (https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph/graphenv-manifest.json) +See :ref:`Sharing and ownership ` for the recipient's async fetch +example and connection setup. diff --git a/doc/source/user_guide/web_frameworks.rst b/doc/source/user_guide/web_frameworks.rst index 42baefb..73a0051 100644 --- a/doc/source/user_guide/web_frameworks.rst +++ b/doc/source/user_guide/web_frameworks.rst @@ -95,6 +95,8 @@ For production deployments, store these values in your deployment platform's secret manager or environment configuration. Do not hard-code database passwords, wallet passwords, or provider credentials in application source. +.. latex:clearpage:: + FastAPI synchronous endpoints ============================= @@ -162,9 +164,14 @@ Stop the server by pressing ``Ctrl+C`` in the terminal where ``uvicorn`` is running. FastAPI runs the lifespan shutdown hook and ``select_ai.disconnect()`` closes the pool. -This example creates the ``Profile`` proxy inside each handler. The proxy is -lightweight; the database connection is acquired from the pool only when the -profile method calls the database. +This example constructs ``Profile`` inside each handler. Construction reads +the saved profile description and attributes from the database, acquiring +connections from the pool before the action itself runs. This keeps metadata +current but adds database calls to each request. Reusing an initialized profile +avoids those reads; refresh it when saved profile settings change, and avoid +mutating a shared profile object while requests are using it. + +.. latex:clearpage:: FastAPI asynchronous endpoints ============================== @@ -219,6 +226,8 @@ Start and stop the server the same way: Press ``Ctrl+C`` to stop it. +.. latex:clearpage:: + Flask example ============= @@ -260,6 +269,8 @@ synchronous Select AI pool when the application is created. return app +.. latex:clearpage:: + Django example ============== @@ -291,6 +302,10 @@ pool once per process, then call ``Profile`` APIs inside views. increment=int(os.getenv("SELECT_AI_POOL_INCREMENT", "1")), ) +.. latex:clearpage:: + +Add the view in ``myapp/views.py``: + .. code-block:: python # myapp/views.py @@ -341,6 +356,8 @@ control what happens when all pooled connections are busy. .. code-block:: python + import oracledb + select_ai.create_pool( user=user, password=password, @@ -348,9 +365,17 @@ control what happens when all pooled connections are busy. min_size=2, max_size=8, increment=2, - wait_timeout=10, + getmode=oracledb.POOL_GETMODE_TIMEDWAIT, + wait_timeout=10_000, ) +``wait_timeout`` is in milliseconds and takes effect only with +``getmode=oracledb.POOL_GETMODE_TIMEDWAIT``. The example waits up to ten seconds +for a connection. The default ``POOL_GETMODE_WAIT`` waits without this limit. +Pool acquisition timeouts do not limit SQL execution or model generation time. +See the `python-oracledb pool parameters +`__. + Choose a timeout that matches your API latency budget. For public HTTP APIs, it is usually better to fail fast and return an application error than to let requests pile up until every worker is blocked. diff --git a/docs/_images/a2a_architecture.svg b/docs/_images/a2a_architecture.svg new file mode 100644 index 0000000..a9b9dfc --- /dev/null +++ b/docs/_images/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 + DBMS_AI_A2A_TASKS[$] + DBMS_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 + + + a2a serve --deployment clustered + 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/docs/_images/conversation_object_model.svg b/docs/_images/conversation_object_model.svg new file mode 100644 index 0000000..fb77f07 --- /dev/null +++ b/docs/_images/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/docs/_images/profile_object_model.svg b/docs/_images/profile_object_model.svg new file mode 100644 index 0000000..345a552 --- /dev/null +++ b/docs/_images/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/docs/_images/select_ai_a2a_server_demo.gif b/docs/_images/select_ai_a2a_server_demo.gif new file mode 100644 index 0000000..9b0ab9f Binary files /dev/null and b/docs/_images/select_ai_a2a_server_demo.gif differ diff --git a/docs/_images/vector_index_object_model.svg b/docs/_images/vector_index_object_model.svg new file mode 100644 index 0000000..a9e47eb --- /dev/null +++ b/docs/_images/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/docs/_sources/index.rst.txt b/docs/_sources/index.rst.txt index 0a2dc6a..8241b5d 100644 --- a/docs/_sources/index.rst.txt +++ b/docs/_sources/index.rst.txt @@ -21,6 +21,9 @@ Getting Started user_guide/introduction.rst user_guide/installation.rst user_guide/connection.rst + user_guide/quickstart.rst + user_guide/compatibility.rst + release_notes.rst Actions @@ -59,6 +62,15 @@ Credential user_guide/credential.rst +Sharing and ownership +===================== + +.. toctree:: + :numbered: + :maxdepth: 3 + + user_guide/sharing.rst + Profile Attributes ================== @@ -143,6 +155,15 @@ Async AI Agent user_guide/async_agent.rst +A2A Integration +=============== + +.. toctree:: + :numbered: + :maxdepth: 3 + + user_guide/a2a.rst + Command Line Interface ====================== @@ -169,3 +190,11 @@ Concurrent Prompt Processing :maxdepth: 3 user_guide/concurrent_prompt_processing.rst + +API Reference +============= + +.. toctree:: + :maxdepth: 3 + + user_guide/api_reference.rst diff --git a/docs/_sources/release_notes.rst.txt b/docs/_sources/release_notes.rst.txt new file mode 100644 index 0000000..105e805 --- /dev/null +++ b/docs/_sources/release_notes.rst.txt @@ -0,0 +1,63 @@ +.. _release-1-5-0: + +******************* +What's new in 1.5.0 +******************* + +Version 1.5.0 adds A2A integration, object sharing, request-level profile +overrides, and typed agent history APIs. The changes below are relative to +1.4.1. + +New and expanded APIs +===================== + +* Expose an agent team with the optional :ref:`A2A server `. Choose fixed + or dynamic connections and standalone or clustered deployment. A2A includes + Oracle-backed task/context storage, task polling, A2UI connection forms, and + A2A 1.0/v0.3 JSON-RPC compatibility. +* :ref:`Share profiles, vector indexes, teams, and credentials ` + with database users or roles. Profile and vector-index fetch/list APIs + accept ``owner`` and expose ``qualified_name``. +* Pass an ``attributes`` mapping to profile and session actions for + :ref:`request-only overrides `. Async pipelines + can apply the same override mapping to every operation. +* Inspect agent execution with :ref:`TeamHistory, TaskHistory, and ToolHistory + ` and their async equivalents. +* Enable and disable profiles without deleting them. Configure + ``additional_instructions``, integer ``seed``, and translation language + defaults with ``ProfileAttributes``. Per-call translation languages are + optional and can use the saved defaults. +* Configure supervised teams with ``AgentAttributes.supervisor`` and + ``TeamAttributes.supervisor_agent``; inspect the database-generated + ``supervisor_task`` after creation. + +Upgrade considerations +====================== + +* **Python 3.11 is now the minimum.** Python 3.9 and 3.10 are no longer + supported. Supported versions are 3.11 through 3.14. +* **pandas is now pinned to 3.0.3**, replacing 2.2.3. Check applications that + consume DataFrames for pandas compatibility when upgrading. +* ``ProfileAttributes.seed`` is now typed as an integer. Supply an integer + rather than a numeric string. +* Unspecified ``SyntheticDataParams`` values are omitted so the database can + apply its defaults. Set values explicitly if your application relies on + particular generation settings. +* Install ``select_ai[a2a]`` to use A2A commands; ``select_ai[cli]`` covers + the other CLI commands. Prepare A2A database storage privileges as described + in :ref:`A2A prerequisites `. +* ``--require-oauth`` requires a bearer token and derives an owner scope. + Token signatures and claims must be validated by a trusted upstream service; + the SDK flag alone does not verify identity. See :ref:`A2A authentication + `. + +Upgrade in your application's virtual environment: + +.. code-block:: bash + + python -m pip install --upgrade 'select_ai==1.5.0' + # If you use A2A: + python -m pip install --upgrade 'select_ai[a2a]==1.5.0' + +See :ref:`Compatibility ` for the feature requirements and +:ref:`Run your first SQL query ` for a complete example. diff --git a/docs/_sources/user_guide/a2a.rst.txt b/docs/_sources/user_guide/a2a.rst.txt new file mode 100644 index 0000000..d984f03 --- /dev/null +++ b/docs/_sources/user_guide/a2a.rst.txt @@ -0,0 +1,1241 @@ +.. _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. + +Make two independent choices: **where the server runs** (topology) and +**when database connection values are supplied** (fixed or dynamic). +For example, a clustered deployment can use fixed credentials, and a +standalone deployment can ask each client for credentials. + +.. only:: html + + .. figure:: /image/select_ai_a2a_server_demo.gif + :alt: Select AI A2A server demo from the project README + :width: 100% + + A Select AI agent team accessed through an A2A client. + +Choose a topology +================= + +* **Standalone:** run one public ``select-ai a2a serve`` process. It handles + A2A requests and database work locally. Start here for a single server; + Consul and a separate worker are not required. +* **Clustered:** run a public gateway with + ``select-ai a2a serve --deployment clustered`` and one or more + ``select-ai a2a worker`` processes. Consul lets the gateway discover healthy + workers and route each session back to its worker. Use this when database + sessions need to run across multiple workers. See + :ref:`Why clustered deployments use Consul ` for the role of + this additional service. + +.. _a2a-why-consul: + +Why clustered deployments use Consul +==================================== + +Consul is a separate service that keeps a directory of running workers and +records which worker owns each A2A session. The gateway consults this directory +to answer two questions: "Where can I start a new session?" and "Where should I +send the next request for an existing session?" + +This matters because each database session runs in a child process on one +worker. That process holds the session's database connection and credentials. +Later prompts, task-status requests, and cancellation requests must reach that +same worker. A different worker cannot continue using its live connection. + +Consul provides three things for the gateway: + +* **Worker discovery:** each worker registers its address when it starts. The + gateway can find workers without a manually maintained list of addresses. +* **Worker health:** workers send regular heartbeats to Consul to report that + they are running. The gateway selects a worker whose health check is passing + when opening a new session. +* **Session and task routing:** the gateway records the worker assigned to a + session and the session associated with each task. Subsequent requests can + reach the worker that owns them, even when other workers are available. + +For example, suppose two workers, A and B, are running. The gateway opens your +session on worker A and records that assignment in Consul. Your next prompt +and a request to check your task's status both go to A. Another client's new +session can run on B. If A stops, the gateway cannot move its live database +connection to B; the client must reconnect to open a new session. See +:ref:`Session isolation, routing, and cleanup ` +for expiry and reconnection behavior. + +Consul stores worker addresses and temporary routing records. Database +passwords are not stored in Consul; task results and conversation history are +stored in Oracle Database. See :ref:`Persistent Oracle-backed state ` +for the database storage details. + +**When do you need it?** The current clustered implementation requires Consul, +even with only one worker or fixed database credentials. Standalone deployment +handles sessions locally and does not require Consul. For a first deployment, +use standalone unless you need separate worker processes across machines. + +Choose how connections are configured +===================================== + +Each session needs four values: a database DSN, username, password, and an +existing AI Agent Team name. Configure these on ``a2a serve`` with ``--dsn``, +``--user``, ``--password``, and ``--team``, or their environment fallbacks. + +* **Fixed:** configure all four values before starting the server. Clients + send prompts without completing a connection form. In standalone topology, + the server uses a shared connection pool. In clustered topology, the gateway + automatically opens a session on a worker using the configured values. +* **Dynamic:** leave one or more values unset. The client's first message + receives an A2UI connection form asking for those missing values. After the + client submits the form, the server opens an isolated database session and + the client can send prompts using that A2A context ID. The client must + support the A2UI handshake; see the runnable samples below. + +For example, configure ``--dsn`` and ``--team`` for an application that always +uses one database and one team, but lets each user connect with their own +username and password. The form then asks only for username and password. +Leave all four values unset to let the client choose the database and team +as well. Values configured on the server cannot be changed by the client. + +"Dynamic" describes collecting connection values when a session starts. +Changing a database or user requires a new session. It does not switch the +connection underneath an existing session. + +.. list-table:: Supported combinations + :header-rows: 1 + :widths: 25 37 38 + + * - Topology + - Fixed connection + - Dynamic connection + * - Standalone + - Shared async pool; streaming supported. + - Local child process per session; task polling supported. + * - Clustered + - Worker session opened automatically; task polling supported. + - Worker session opened after form submission; task polling supported. + +Dynamic standalone and all clustered sessions do not support streaming. +Standalone dynamic mode requires one Uvicorn process. Clustered mode requires +Consul and at least one worker even when the connection is fixed. +See :ref:`A2A CLI examples ` for commands and +:ref:`Command Line Interface ` for options. + +.. only:: html + + .. figure:: /image/a2a_architecture.svg + :alt: Standalone A2A server and clustered gateway-worker architecture + :width: 100% + + Select AI A2A topologies. Dynamic sessions can run inside the standalone + server or through the clustered gateway and worker processes. + +.. only:: latex + + .. figure:: /image/a2a_architecture.png + :alt: Standalone A2A server and clustered gateway-worker architecture + :width: 100% + + Select AI A2A topologies. Dynamic sessions can run inside the standalone + server or through the clustered gateway and worker processes. + +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. Clustered deployments also need +Consul; standalone deployments do not. + +The ``--deployment`` option defaults to ``standalone``. Set it to +``clustered`` to use a gateway and workers. There is no separate connection-mode +CLI flag: the connection is fixed when all four connection values are supplied +and dynamic when any are missing. Environment values count as supplied values. +See :ref:`A2A CLI examples ` for complete commands. + +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 all supported combinations: + +``/.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. + +Installation and prerequisites +============================== + +Install the A2A dependencies before running the server, gateway, or worker: + +.. raw:: latex + + \Needspace{5\baselineskip} + +.. 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 database user also needs ``CREATE TABLE``, ``CREATE VIEW``, and sufficient +quota in its default tablespace for A2A task and context storage. Ask a database +administrator to grant these privileges to each database user that will open +an A2A session. The SDK's ``grant_privileges()`` helper grants package execute +privileges; it does not grant these storage privileges or tablespace quota. + +At initialization, the runtime creates missing internal tables and executes +``CREATE OR REPLACE VIEW`` for its read-only views. ``CREATE VIEW`` is therefore +required even when the tables already exist. See +:ref:`Persistent Oracle-backed state `. + +The fixed standalone server needs all database values at startup. Dynamic +standalone and clustered sessions can collect missing values through A2UI. +Clustered deployment additionally requires Consul and at least one worker; +workers must be able to reach both Consul and Oracle Database. + +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 +===================== + +Fixed database connection +~~~~~~~~~~~~~~~~~~~~~~~~~ + +When all four connection values are configured at startup, the standalone +server uses one shared asynchronous connection pool for the selected team. +This mode supports streaming and does not need Consul or a separate worker. +Replace the example values with your database username, password, host, port, +service name, team, and CA-file path. Supply all four connection values to +use fixed standalone mode; omitting any value selects dynamic standalone mode. + +.. raw:: latex + + \Needspace{14\baselineskip} + +.. code-block:: bash + + select-ai a2a serve \ + --deployment standalone \ + --team ORACLE_AI_DATABASE_AGENT \ + --user DB_USERNAME \ + --password 'DB_PASSWORD' \ + --dsn 'dbhost.example.com:1521/db_service' \ + --database-tls-ca-file /run/secrets/database-ca.pem \ + --host 0.0.0.0 \ + --port 8000 \ + --public-url https://a2a.example.com + +The ``--database-tls-ca-file`` option is only needed when the database +certificate is signed by a CA not already trusted by the system. The +``--public-url`` value 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: + +.. raw:: latex + + \Needspace{6\baselineskip} + +.. 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. + +Dynamic standalone sessions +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If one or more of ``--dsn``, ``--user``, ``--password``, and ``--team`` are +omitted, the standalone server asks the A2A client for those values through an +A2UI form. The server creates an isolated child process for each session. This +is still standalone: it needs no Consul service and no ``a2a worker`` process. +Run one Uvicorn process for this mode. Dynamic standalone sessions support +task polling but do not support streaming. + +.. raw:: latex + + \Needspace{9\baselineskip} + +.. code-block:: bash + + select-ai a2a serve \ + --deployment standalone \ + --host 0.0.0.0 \ + --port 8000 \ + --public-url https://a2a.example.com + +If you set some connection values at startup, the form asks only for the +remaining values. See :ref:`A2A CLI examples ` for the +clustered gateway and worker commands. + +End-user OAuth and session ownership +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, ``select-ai a2a serve`` does not require an application OAuth +token. Each A2A context maps to its own Select AI conversation, while records +use a shared owner scope (``anonymous`` in fixed standalone mode and +``a2a-conversation`` in session-based modes). A hosting platform such as +private Cloud Run can authenticate the calling service, but the A2A server +does not verify which human user owns the conversation. + +Add ``--require-oauth`` to require a bearer token and derive the owner scope +from it. Configure a trusted upstream service to validate the token first: + +.. raw:: latex + + \Needspace{15\baselineskip} + +.. code-block:: bash + + select-ai a2a serve \ + --deployment standalone \ + --require-oauth \ + --team ORACLE_AI_DATABASE_AGENT \ + --user DB_USERNAME \ + --password 'DB_PASSWORD' \ + --dsn 'dbhost.example.com:1521/db_service' \ + --database-tls-ca-file /run/secrets/database-ca.pem \ + --host 0.0.0.0 \ + --port 8000 \ + --public-url https://a2a.example.com + +Every request to the A2A JSON-RPC endpoint must then contain +``Authorization: Bearer ...``. Agent Card discovery remains public. The SDK +requires bearer syntax and derives an owner scope, but does not validate token +signatures, expiration, issuer, or audience. It accepts arbitrary nonempty +bearer values. This flag alone does not establish a verified end-user identity. + +The deployment must validate tokens in a trusted authenticating proxy or +service before forwarding them, and prevent callers from reaching the A2A +listener through a path that bypasses that validation. Cloud Run IAM validation +of a calling service is separate from validation of the end-user token. + +The server uses the derived owner and A2A context to scope task and context +records and the Select AI conversation. For an upstream-validated JWT with +``iss`` and ``sub`` claims, ownership remains stable across token refreshes. +For an opaque access token, ownership is derived from a hash of the token; +refreshing it starts a new owner scope. Tokens are not persisted by this +authentication backend. + +The Agent Card advertises bearer security only with ``--require-oauth``. + +Agent Card discovery +~~~~~~~~~~~~~~~~~~~~ + +Fetch the card after starting the server: + +.. raw:: latex + + \Needspace{5\baselineskip} + +.. 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: + +.. raw:: latex + + \Needspace{7\baselineskip} + +.. 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: + +.. raw:: latex + + \Needspace{12\baselineskip} + +.. 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 fixed 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 `__: + +.. latex:clearpage:: + +.. literalinclude:: ../../../samples/a2a/blocking_task.py + :language: python + :start-at: import json + +The blocking sample omits ``configuration``. Example output (task IDs and model responses vary): + +.. raw:: latex + + \Needspace{10\baselineskip} + +.. 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. + +.. latex:clearpage:: + +First, define the endpoint, prompt, terminal states, and JSON-RPC helper: + +.. literalinclude:: ../../../samples/a2a/task_poll.py + :language: python + :start-at: import json + :end-before: # blocking=False + +.. latex:clearpage:: + +Append the send-and-poll loop to that setup: + +.. literalinclude:: ../../../samples/a2a/task_poll.py + :language: python + :start-at: # blocking=False + +The polling sample sends a non-blocking request and then calls ``tasks/get``: + +.. raw:: latex + + \Needspace{12\baselineskip} + +.. 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 standalone and clustered deployments use Oracle implementations of the +A2A task and context stores. In fixed standalone mode, the server initializes +these stores when the application starts. In dynamic standalone mode, each +session child initializes the stores after connecting. In clustered mode, +``select-ai a2a serve --deployment clustered`` does not connect to Oracle +itself; the ``select-ai a2a worker`` command starts the internal worker, and +each connected worker session initializes the stores after it opens its +database connection. On first initialization, the stores create these tables +if they do not already exist: + +``DBMS_AI_A2A_TASKS$`` and ``DBMS_AI_A2A_TASKS`` + Stores the task ID, context ID, serialized task JSON, owner, and update + timestamp. It supports task retrieval, filtering, listing, pagination, and + deletion. The object with the ``$`` suffix is the writable internal table; + the object without it is a read-only view of the same columns. + +``DBMS_AI_A2A_CONTEXTS$`` and ``DBMS_AI_A2A_CONTEXTS`` + Maps an A2A context and owner to an Oracle conversation ID. The object with + the ``$`` suffix is the writable internal table; the object without it is a + read-only view of the same columns. + +When a request starts a new context, Select AI creates an +``AsyncConversation`` and passes its ID to ``AsyncTeam.run``. Later messages in +the same A2A context reuse that conversation, so the team can use the +conversation history. In standalone modes, task and context ownership is +resolved from the A2A request context in fixed standalone mode; an absent owner +is stored as ``anonymous``. Dynamic standalone and clustered runtimes pass the +gateway's derived owner scope to the child runtime, using ``a2a-conversation`` +when OAuth is not required. Routing binds the owner and A2A context ID to an +internal session ID. That internal ID identifies the child process; it is +separate from the client-visible A2A context ID. + +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 fixed standalone +``select-ai a2a serve`` path. Dynamic standalone sessions and clustered worker +sessions do not currently accept wallet settings. Pass the unzipped wallet +directory and password with CLI options: + +.. raw:: latex + + \Needspace{15\baselineskip} + +.. code-block:: bash + + select-ai a2a serve \ + --deployment standalone \ + --team ORACLE_AI_DATABASE_AGENT \ + --user DB_USERNAME \ + --password 'DB_PASSWORD' \ + --dsn 'dbhost.example.com:1521/db_service' \ + --wallet-location /secure/wallet/Wallet_database \ + --wallet-password 'WALLET_PASSWORD' \ + --host 0.0.0.0 \ + --port 8000 \ + --public-url https://a2a.example.com + +``--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. + +Private database CA certificates +================================= + +For a database whose server certificate is signed by a private CA, add +``--database-tls-ca-file /path/to/database-ca.pem`` to the relevant +``select-ai a2a serve`` command. The CLI reads PEM from that file and uses it +to validate Oracle Database TLS in fixed standalone, dynamic standalone, and +clustered sessions. In clustered mode, the gateway sends the certificate to +the worker when it opens a session. The separate ``--worker-tls-ca-file`` +option validates the worker certificate on the gateway-to-worker connection. + +Database call timeout +===================== + +Use ``--database-call-timeout-seconds`` to configure the maximum duration of +an Oracle Database call. It defaults to 120 seconds and sets the timeout for +fixed standalone connections and dynamic standalone or clustered worker +sessions. In clustered mode, the gateway uses this value to derive its request +deadline, so set the same value on the gateway and worker. + +Clustered A2A gateway and worker +================================ + +The clustered topology separates the public A2A protocol endpoint from the +database-bearing runtime. The gateway can start without database credentials; +an A2UI form collects them from the client. Values configured on the gateway +are prefilled and the form asks only for the missing values. A worker registers +with Consul and waits for the gateway to assign sessions. Each worker session +uses the persistent Oracle-backed task, context, and conversation state +described in :ref:`Persistent Oracle-backed state +`; Consul stores only 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``: + +.. raw:: latex + + \Needspace{5\baselineskip} + +.. 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: + +.. raw:: latex + + \Needspace{22\baselineskip} + +.. code-block:: bash + + # Terminal 1: Consul + consul agent -dev -bind=127.0.0.1 -client=127.0.0.1 + + # Terminal 2: worker + select-ai a2a worker \ + --host 127.0.0.1 \ + --port 8081 \ + --consul-url http://127.0.0.1:8500 \ + --worker-id local-worker \ + --worker-endpoint http://127.0.0.1:8081 + + # Terminal 3: public gateway + select-ai a2a serve \ + --deployment clustered \ + --host 127.0.0.1 \ + --port 8000 \ + --public-url http://127.0.0.1:8000 \ + --consul-url http://127.0.0.1:8500 + +The worker uses ``--consul-url``, ``--worker-id``, and the optional +``--worker-endpoint`` when registering with Consul. Their environment +fallbacks are ``CONSUL_HTTP_URL``, ``WORKER_ID``, and ``WORKER_ENDPOINT``; +explicit command-line values take precedence. ``--port`` is both the listening +port and the registered port. Cluster manifests can set ``WORKER_ADDRESS`` to +the pod IP when no advertised endpoint is supplied. Clustered serve uses +``PUBLIC_URL``, ``CONSUL_HTTP_URL``, ``WORKER_SERVICE`` (default +``select-ai-a2a-worker``), and ``SESSION_TTL_SECONDS``. + +Pass any fixed connection values with ``--dsn``, ``--user``, ``--password``, +and ``--team``. The generated form contains only the remaining values. Use +``--a2ui-form`` to supply a validated custom form for those missing values. + +The worker must be able to resolve the submitted DSN. For a TNS alias, set +``TNS_ADMIN`` in the worker terminal before starting it. The gateway session +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. + +.. _a2a-clustered-fixed: + +Clustered deployment with a fixed connection +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Yes, a clustered deployment can use fixed connection information. Start +Consul and the worker as shown above, then run this gateway command in place +of the dynamic gateway command. Supply the connection values on the gateway; +the gateway passes them to the selected worker when opening each session: + +.. raw:: latex + + \Needspace{14\baselineskip} + +.. code-block:: bash + + select-ai a2a serve \ + --deployment clustered \ + --host 127.0.0.1 \ + --port 8000 \ + --public-url http://127.0.0.1:8000 \ + --consul-url http://127.0.0.1:8500 \ + --dsn 'dbhost.example.com:1521/db_service' \ + --user APP_USER \ + --password "$SELECT_AI_PASSWORD" \ + --team ORACLE_AI_DATABASE_AGENT + +Set ``SELECT_AI_PASSWORD`` to the database password before running the +command. The client sends an ordinary A2A prompt; the gateway skips the form +and creates the worker session automatically. Workers still isolate sessions +in child processes. All clients use the configured database identity and +team. Fixed clustered connections retain the clustered restrictions: no +streaming and no wallet parameters for the database session. + +To collect each user's credentials instead, remove ``--user`` and +``--password`` and unset their environment fallbacks ``SELECT_AI_USER`` and +``SELECT_AI_PASSWORD`` in the gateway environment. Keep ``--dsn`` and +``--team``. The same gateway and workers now serve dynamic sessions whose +forms ask only for username and password. + +A2UI database-connection form +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When connection properties are missing, the first ``message/send`` for a +context returns a temporary task containing a database connection form. The +form is an A2UI v0.9 data artifact with the ``application/json+a2ui`` MIME +type. It contains only the values not configured on the server: + +.. list-table:: Dynamic connection form + :header-rows: 1 + :widths: 25 75 + :align: left + + * - Field + - Purpose + * - ``connection_url`` + - Oracle connect descriptor, simplified connect string, or TNS alias + available to the worker. + * - ``username`` + - Database user for this session. + * - ``password`` + - Database password. The form renders this field as obscured input. + * - ``ai_agent`` + - Select AI Agent Team to run in this session. + * - ``submit_database_connection`` + - A2UI action that submits exactly the displayed values. + +For example, a clustered server started with ``--dsn`` and ``--team`` renders +only username and password. Server-configured values are immutable and cannot +be overridden by a form submission. When all four values are configured, the +server skips the form and opens the worker session automatically. + +Use ``--a2ui-form PATH`` to replace the generated form. The file must be a +top-level JSON array of A2UI v0.9 operations using the advertised catalog and +one consistent template ``surfaceId``. It must contain exactly one +``submit_database_connection`` action whose context submits exactly the +missing canonical properties: ``connection_url``, ``username``, ``password``, +and/or ``ai_agent``. A collected password must use an obscured ``TextField`` +and the +template must not contain a password default. The server validates the file at +startup and replaces its template surface ID with a fresh ID every time the +form is displayed. + +After the action is submitted in clustered mode, the gateway selects a healthy +worker through Consul and opens a session. In dynamic standalone mode, the +server opens the session locally. 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 dynamic-session samples perform this handshake automatically. They use the +repository-local helper module +`samples/a2a/dynamic/_common.py `__; +``_common`` +is not a package that users install with ``pip``. Running the samples from the +repository root as shown below makes that helper available automatically. + +The helper functions are: + +``call(method, params)`` + Sends one A2A v0.3 JSON-RPC request to the configured server endpoint. + +``connect(prompt)`` + Sends the initial prompt, reads the A2UI connection form, submits the + ``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 dynamic session. Passing + ``blocking=False`` adds the non-blocking request option. + +``print_task_summary(task)`` + Prints the task state, result artifact name, and text parts without dumping + the connection-form details. + +If you copy a dynamic-session sample into another directory, copy the +`_common.py helper `__ +with it, or replace these helpers with an A2A client implementation of your +own. + +The complete dynamic-session setup is also documented in the +`dynamic sample README `__. +The examples below are the +`blocking_task.py sample `__ +and the +`task_poll.py sample `__. + +.. raw:: latex + + \Needspace{18\baselineskip} + +.. literalinclude:: ../../../samples/a2a/dynamic/blocking_task.py + :language: python + :start-at: from _common + +Set the database values before running the sample: + +.. raw:: latex + + \Needspace{8\baselineskip} + +.. 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: + +.. raw:: latex + + \Needspace{5\baselineskip} + +.. code-block:: bash + + python samples/a2a/dynamic/blocking_task.py + +Example output (task IDs and model responses vary): + +.. raw:: latex + + \Needspace{7\baselineskip} + +.. code-block:: text + + Task 15c...: completed + Artifact: database-agent-result + The database contains ... + +The dynamic polling sample uses the same connection-form handshake, then +passes ``configuration.blocking: false`` and polls ``tasks/get``: + +.. raw:: latex + + \Needspace{30\baselineskip} + +.. literalinclude:: ../../../samples/a2a/dynamic/task_poll.py + :language: python + :start-at: import time + +.. raw:: latex + + \Needspace{9\baselineskip} + +.. 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 binds the owner and A2A context ID to one +worker session with its own internal session 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-a2a/sessions/`` and ``select-ai-a2a/tasks/`` key-value records, + which + route a context or task back to the worker that owns it. + +The task and conversation contents are not stored in Consul. They are stored +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. + +Public gateway HTTPS +~~~~~~~~~~~~~~~~~~~~ + +The ``--tls-*`` options on ``select-ai a2a serve`` configure the public A2A +listener. They do not configure gateway-to-worker traffic. + +* With no TLS options, Uvicorn listens over HTTP. +* Supply ``--tls-cert-file`` and ``--tls-key-file`` together to serve HTTPS. + Clients verify the gateway certificate using their configured trust store. +* Add ``--tls-ca-file`` to require clients to present a certificate signed by + that CA. This enables mTLS on the public connection. The CA file requires + the certificate and key. + +For example, this command serves HTTPS without requiring client certificates: + +.. code-block:: bash + + select-ai a2a serve \ + --team ORACLE_AI_DATABASE_AGENT \ + --user DB_USERNAME \ + --password DB_PASSWORD \ + --dsn 'dbhost.example.com:1521/db_service' \ + --host 0.0.0.0 \ + --port 8000 \ + --public-url https://a2a.example.com \ + --tls-cert-file /run/secrets/gateway.crt \ + --tls-key-file /run/secrets/gateway.key + +Add ``--tls-ca-file /run/secrets/client-ca.pem`` when public clients must +authenticate with client certificates. ``--public-url`` controls the URL +advertised in the Agent Card; the TLS options control Uvicorn's listener. + +Gateway-to-worker HTTP and TLS +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The gateway-to-worker connection is independent of the public A2A listener. +The public listener can use HTTPS while workers use HTTP, or the gateway can +use HTTPS to workers while its public listener uses HTTP. + +.. list-table:: Gateway-to-worker TLS options + :header-rows: 1 + :widths: 28 36 36 + + * - Option + - Process + - Purpose + * - ``--tls-cert-file`` and ``--tls-key-file`` + - Worker + - Enable HTTPS on the worker. Supply both together. + * - ``--tls-ca-file`` + - Worker + - Optional CA for requiring and verifying gateway client certificates. + Supplying it enables the worker side of mTLS. + * - ``--worker-tls-ca-file`` + - Gateway + - Optional CA bundle for verifying the worker's server certificate. + Supply it alone for HTTPS with a private CA and no mTLS. If omitted, + Requests uses its default CA bundle (which can be overridden with + ``REQUESTS_CA_BUNDLE``). + * - ``--worker-tls-cert-file`` and ``--worker-tls-key-file`` + - Gateway + - Optional gateway client certificate and key. Supply both together; + the worker must require and trust this client certificate for mTLS. + +Whenever a gateway ``--worker-tls-*`` option is supplied, register the worker +with an HTTPS URL using ``--worker-endpoint``. If no gateway worker TLS option +is supplied, Requests still verifies an HTTPS worker certificate against its +default CA bundle. When the worker has no advertised endpoint, the gateway +builds an HTTP URL from the worker address and port in the Consul record. + +For HTTPS without mTLS, configure the worker's certificate and key, then +advertise its HTTPS URL. If the certificate uses a private CA, give that CA +to the gateway with ``--worker-tls-ca-file``. Do not set the gateway client +certificate or the worker's ``--tls-ca-file`` in this mode: + +.. code-block:: bash + + select-ai a2a worker \ + --worker-endpoint https://worker.example.com:8443 \ + --tls-cert-file /run/secrets/worker.crt \ + --tls-key-file /run/secrets/worker.key + + select-ai a2a serve \ + --deployment clustered \ + --worker-tls-ca-file /run/secrets/private-worker-ca.pem + +For mTLS, the worker requires a client certificate with ``--tls-ca-file``. +The gateway presents its certificate and key with ``--worker-tls-cert-file`` +and ``--worker-tls-key-file``. The gateway also needs +``--worker-tls-ca-file`` when the worker certificate is signed by a private +CA; omit it when the Requests default CA bundle trusts the worker certificate. +The two directions can use the same CA or different CAs: + +.. code-block:: bash + + select-ai a2a worker \ + --worker-endpoint https://worker.example.com:8443 \ + --tls-cert-file /run/secrets/worker.crt \ + --tls-key-file /run/secrets/worker.key \ + --tls-ca-file /run/secrets/gateway-client-ca.pem + + select-ai a2a serve \ + --deployment clustered \ + --worker-tls-ca-file /run/secrets/worker-server-ca.pem \ + --worker-tls-cert-file /run/secrets/gateway-client.crt \ + --worker-tls-key-file /run/secrets/gateway-client.key + +The worker CA validates the gateway's client certificate. The gateway CA +validates the worker's server certificate. This protects only the +gateway-to-worker connection; it does not configure TLS for the public A2A +listener or Oracle Database connections. + +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. Select its connection provisioning +explicitly: + +.. raw:: latex + + \Needspace{8\baselineskip} + +.. code-block:: bash + + gcloud/standalone/deploy.sh \ + --project PROJECT_ID \ + --connection-mode dynamic \ + --build + +The script creates or reuses an Artifact Registry repository, runtime service +account, any requested Secret Manager secrets, and Cloud Run service. With no +database options, dynamic standalone mode fixes neither the Connection URL nor +AI Agent; its A2UI form requests Connection URL, username, password, and AI +Agent. Pass +``--db-dsn-secret`` or ``--a2a-team`` to fix either value independently, or +``--rotate-db-config`` to configure the deployment's Connection URL. Fixed +mode prompts for Connection URL, username, and password, requires +``--a2a-team``, and creates the shared startup pool. The service is private; the +script grants +``run.routes.invoke`` to the active deployment identity and the Gemini +Enterprise Discovery Engine service agent. + +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: + +* ``--connection-mode``: required ``fixed`` or ``dynamic`` provisioning. +* ``--service``: Cloud Run service name; use a different service for each + deployment profile. +* ``--a2a-team``: fix the AI Agent in the deployment; required in fixed mode. +* ``--require-oauth``: require an upstream-validated end-user bearer token + and derive an owner scope as well as the A2A conversation scope. The SDK + does not validate the token itself; see `End-user OAuth and session ownership`_. +* ``--pool-max-size``: maximum Oracle connections per Cloud Run instance. +* ``--max-instances``: Cloud Run instance limit; dynamic standalone mode + requires one. +* ``--wallet-archive``: upload or replace an Autonomous Database wallet ZIP + in fixed mode. +* ``--db-dsn-secret``: fix the Connection URL using an existing Secret Manager + secret. +* ``--rotate-db-config``: prompt for and create or rotate the deployment's + database values. + +For a wallet deployment, ``--wallet-archive`` stores the ZIP and wallet +password in service-specific Secret Manager secrets. Cloud Run mounts the ZIP +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. + +The Google Cloud scripts do not require end-user OAuth by default. Cloud Run +IAM authenticates Gemini Enterprise, and ``a2a serve`` separates database +sessions by A2A conversation without asserting a verified human owner. Add +``--require-oauth`` to the deployment command when user ownership is required, +and ensure a trusted upstream service validates the end-user token. The flag +alone does not verify identity. Gemini Enterprise must then be configured with end-user OAuth rather +than **Skip & Finish**. It uses ``X-Serverless-Authorization`` for Cloud Run +IAM and sends the user token separately in ``Authorization``. + +Clustered Google Cloud deployment +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`gcloud/cluster/deploy.sh `__ +deploys the clustered topology: Cloud Run for the +public server, GKE Autopilot for worker replicas, and Consul for discovery and +routing. + +.. raw:: latex + + \Needspace{8\baselineskip} + +.. code-block:: bash + + gcloud/cluster/deploy.sh \ + --project PROJECT_ID \ + --region us-central1 \ + --worker-replicas 3 + +The script and +`gcloud/cluster/cloudbuild.yaml `__ +perform these steps: + +1. Enable the required Google Cloud APIs and create or reuse Artifact Registry + and a GKE Autopilot cluster. +2. Configure additive VPC DNS so StatefulSet worker names resolve to current + Pod IPs after a worker is recreated. +3. Deploy the A2A namespace, internal Consul service, and worker service. +4. Build and publish the image with Cloud Build. +5. Deploy the requested worker replicas with ``select-ai a2a worker`` and the + selected session TTL. +6. Deploy Cloud Run with ``select-ai a2a serve --deployment clustered`` and + set ``PUBLIC_URL`` to the final Cloud Run URL. + +The Cloud Run server uses direct VPC egress to reach the internal Consul +load-balancer address and worker endpoints. The GKE worker service is headless +service discovery, not a public load balancer. Consul selects a healthy worker +when a session opens; the route is then pinned to that worker. + +Important clustered 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. +* ``--conversation-retention-days``: number of days to retain Oracle + conversations (default 7; use 0 to disable automatic removal). +* ``--enable-worker-mtls``: create and use server-to-worker certificates. +* ``--rotate-worker-mtls``: replace the existing test certificates and restart + the worker workload. + +The default clustered deployment uses private-VPC HTTP between Cloud Run and +GKE. With ``--enable-worker-mtls``, the script creates short-lived test PKI +material in Secret Manager, creates the Kubernetes TLS secrets, deploys the +worker StatefulSet variant, and mounts the server client certificate into +Cloud Run. The worker certificate uses the GKE StatefulSet DNS name, so the +``--gke-dns-domain`` value must remain consistent with the cluster. + +The clustered deployment does not upload an Oracle wallet because the dynamic +worker session path currently accepts only DSN/user/password. If database mTLS +is required, use the standalone deployment or provide a separate database +connection mechanism to the worker implementation. + +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/cluster/deploy.sh `__ + - Creates or reuses the clustered GKE/Cloud Run topology and supplies build + substitutions. + * - `gcloud/cluster/cloudbuild.yaml `__ + - Builds the image, deploys Consul and workers, and deploys Cloud Run. + * - `gcloud/cluster/gke manifests `__ + - Namespace, Consul, headless worker service, and HTTP or mTLS worker + workloads. + * - `gcloud/cluster/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`` or + ``PUBLIC_URL``. The URL must be the client-visible base URL, not an internal + container address. +* If the gateway returns the connection form repeatedly, check that the worker + is passing in Consul, that the worker can resolve and connect to Oracle, and + that the session TTL has not expired. +* 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/docs/_sources/user_guide/actions.rst.txt b/docs/_sources/user_guide/actions.rst.txt index 82fafec..481505d 100644 --- a/docs/_sources/user_guide/actions.rst.txt +++ b/docs/_sources/user_guide/actions.rst.txt @@ -57,6 +57,21 @@ The following actions can be performed using ``select_ai``: Action methods ============== +Request-specific profile attributes can be supplied with the optional +``attributes`` mapping. For example, override ``additional_instructions`` for +a single chat request: + +.. code-block:: python + + response = profile.chat( + prompt="Write sample Python code to build and test a regression model", + attributes={ + "additional_instructions": ( + "Return executable code as plain text without Markdown fences." + ) + }, + ) + .. list-table:: Action to method mapping :header-rows: 1 :widths: 30 35 35 diff --git a/docs/_sources/user_guide/agent.rst.txt b/docs/_sources/user_guide/agent.rst.txt index aad3c00..98b57e2 100644 --- a/docs/_sources/user_guide/agent.rst.txt +++ b/docs/_sources/user_guide/agent.rst.txt @@ -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,117 @@ 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 + + from select_ai.agent import Agent, AgentAttributes, Team, TeamAttributes + + # MOVIE_ANALYST and ANALYZE_MOVIE_TASK must already exist. + supervisor = Agent( + agent_name="MOVIE_SUPERVISOR", + attributes=AgentAttributes( + profile_name="oci_ai_profile", + role="You supervise and coordinate the team.", + supervisor=True, + ), + ) + + supervisor.create() + + team = Team( + team_name="MOVIE_AGENT_TEAM", + attributes=TeamAttributes( + agents=[ + {"name": "MOVIE_ANALYST", "task": "ANALYZE_MOVIE_TASK"} + ], + process="sequential", + supervisor_agent=supervisor.agent_name, + ), + ) + team.create() + +Agent and team constructors build Python objects; ``create()`` persists them +in Oracle Database. The example uses new names and raises an error if those +objects already exist. Use ``replace=True`` on ``create()`` only when you +intend to replace existing definitions. + +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. + +Set the common database connection variables and ``SELECT_AI_PROFILE_NAME`` +(default ``oci_ai_profile``) to an existing profile. Run +``python samples/agent/team_supervisor_inspect.py`` from the repository root. +The script removes the objects and conversation it creates during cleanup. + +.. latex:clearpage:: + +First, configure the connection and define the task and worker agent: + +.. literalinclude:: ../../../samples/agent/team_supervisor_inspect.py + :language: python + :start-at: import os + :end-before: supervisor = Agent( + +.. latex:clearpage:: + +Append the supervisor, team, and conversation definitions: + +.. literalinclude:: ../../../samples/agent/team_supervisor_inspect.py + :language: python + :start-at: supervisor = Agent( + :end-before: created_objects = [] + +.. latex:clearpage:: + +Finally, create the objects, run the team, inspect its metadata, and clean up: + +.. literalinclude:: ../../../samples/agent/team_supervisor_inspect.py + :language: python + :start-at: created_objects = [] + +Example output (generated names, model responses, and JSON metadata vary): + +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 +564,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 +600,93 @@ 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. Example output (generated IDs and model responses vary): + +output:: + + BEGIN + DBMS_CLOUD_AI_AGENT.CREATE_TASK(...); + END; + / + +.. latex:clearpage:: + +************* +Agent history +************* + +``TeamHistory``, ``TaskHistory``, and ``ToolHistory`` provide typed, +read-only access to the current user's Select AI Agent history views. They +query only ``USER_AI_AGENT_TEAM_HISTORY``, ``USER_AI_AGENT_TASK_HISTORY``, +and ``USER_AI_AGENT_TOOL_HISTORY`` respectively. Results are yielded newest +first. Tool ``input`` and ``output`` values are decoded to Python objects when +they contain valid JSON; other CLOB payloads are returned as strings. + +.. code-block:: python + + from select_ai.agent import TaskHistory, TeamHistory, ToolHistory + + for run in TeamHistory.list(team_name="MOVIE_AGENT_TEAM", limit=10): + print(run.team_exec_id, run.state) + + for run in TaskHistory.list(team_exec_id=""): + print(run.task_name, run.result) + + for call in ToolHistory.list(tool_name="MOVIE_SQL_TOOL", limit=20): + print(call.input, call.output) + +The sample retrieves a team's latest execution and uses its ``team_exec_id`` +to retrieve the associated task and tool history. + +.. autoclass:: select_ai.agent.TeamHistory + :members: + +.. autoclass:: select_ai.agent.TaskHistory + :members: + +.. autoclass:: select_ai.agent.ToolHistory + :members: + +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:: + ***************** AI agent examples ***************** diff --git a/docs/_sources/user_guide/api_reference.rst.txt b/docs/_sources/user_guide/api_reference.rst.txt new file mode 100644 index 0000000..efea259 --- /dev/null +++ b/docs/_sources/user_guide/api_reference.rst.txt @@ -0,0 +1,86 @@ +.. _error-handling: + +************************************ +Errors, sessions, and result objects +************************************ + +Error handling +============== + +SDK validation and lookup errors are separate from Oracle driver errors. +Catch a specific SDK exception when the application can recover from it. +Database and provider failures surfaced by PL/SQL can raise +``oracledb.DatabaseError``; inspect the driver error code when deciding what +the application should do. Avoid retrying authorization or configuration +failures without correcting their cause. + +.. code-block:: python + + import oracledb + import select_ai + + # Assumes an open connection. Missing profiles are not created here. + try: + profile = select_ai.Profile.fetch("APP_PROFILE") + answer = profile.chat("Explain retrieval augmented generation.") + except select_ai.ProfileNotFoundError: + print("Create APP_PROFILE or choose an existing profile.") + except select_ai.DatabaseNotConnectedError: + print("Establish a connection before using the profile.") + except oracledb.DatabaseError as exc: + error, = exc.args + print(f"Database operation failed (code {error.code}).") + raise + +Do not expose raw database errors or connection credentials in public HTTP +responses. For web applications, translate expected failures into your +application's error response and close pools in the application shutdown hook. + +.. automodule:: select_ai.errors + :members: + +Session objects +=============== + +Obtain sessions through ``Profile.chat_session()`` or +``AsyncProfile.chat_session()``. A session holds a connection for the duration +of its context. Consume and close streaming iterators inside that context. + +.. autoclass:: select_ai.profile.Session + :members: + +.. autoclass:: select_ai.async_profile.AsyncSession + :members: + +Agent history results +===================== + +History APIs yield these typed event objects. Availability and values depend +on the current user's database history views. + +.. autoclass:: select_ai.agent.history.TeamHistoryEvent + :members: + +.. autoclass:: select_ai.agent.history.TaskHistoryEvent + :members: + +.. autoclass:: select_ai.agent.history.ToolHistoryEvent + :members: + +Enumerations +============ + +.. autoclass:: select_ai.action.Action + :members: + +.. autoclass:: select_ai.agent.tool.ToolType + :members: + +.. autoclass:: select_ai.agent.tool.NotificationType + :members: + +.. autoclass:: select_ai.vector_index.VectorDistanceMetric + :members: + +.. autoclass:: select_ai.vector_index.VectorDBProvider + :members: diff --git a/docs/_sources/user_guide/async_agent.rst.txt b/docs/_sources/user_guide/async_agent.rst.txt index df17e2d..2e2edc1 100644 --- a/docs/_sources/user_guide/async_agent.rst.txt +++ b/docs/_sources/user_guide/async_agent.rst.txt @@ -5,6 +5,21 @@ use ``asyncio`` and ``select_ai.async_connect()`` or ``select_ai.create_pool_async()``. +The history API follows the same pattern. ``AsyncTeamHistory``, +``AsyncTaskHistory``, and ``AsyncToolHistory`` query only the current user's +history views and yield typed events newest first. +See :ref:`async-agent-history` for the complete API reference and sample. + +.. code-block:: python + + from select_ai.agent import AsyncToolHistory + + async for call in AsyncToolHistory.list(limit=10): + print(call.tool_name, call.output) + +The async sample retrieves a team's latest execution and uses its +``team_exec_id`` to retrieve the associated task and tool history. + The async agent object model mirrors the synchronous agent object model: .. list-table:: Sync and async agent APIs @@ -136,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`` @@ -234,6 +281,116 @@ 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 + + from select_ai.agent import ( + AgentAttributes, AsyncAgent, AsyncTeam, TeamAttributes, + ) + + # Inside an async function, with an open async connection. + # MOVIE_ANALYST and ANALYZE_MOVIE_TASK must already exist. + supervisor = AsyncAgent( + agent_name="MOVIE_SUPERVISOR", + attributes=AgentAttributes( + profile_name="oci_ai_profile", + role="You supervise and coordinate the team.", + supervisor=True, + ), + ) + + await supervisor.create() + + team = AsyncTeam( + team_name="MOVIE_AGENT_TEAM", + attributes=TeamAttributes( + agents=[ + {"name": "MOVIE_ANALYST", "task": "ANALYZE_MOVIE_TASK"} + ], + process="sequential", + supervisor_agent=supervisor.agent_name, + ), + ) + await team.create() + +Constructors build Python objects. Await ``create()`` to persist each object +in Oracle Database. The example expects new supervisor and team names; pass +``replace=True`` to ``create()`` only when replacement is intended. + +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. + +Set the common database connection variables and ``SELECT_AI_PROFILE_NAME`` +(default ``oci_ai_profile``) to an existing profile. Run +``python samples/agent/async/team_supervisor_inspect.py`` from the repository +root. The script removes its generated objects and conversation during cleanup. + +.. latex:clearpage:: + +Configure the imports and connection values: + +.. literalinclude:: ../../../samples/agent/async/team_supervisor_inspect.py + :language: python + :start-at: import asyncio + :end-before: async def main(): + +.. latex:clearpage:: + +Append the async function and its task, agent, and team definitions: + +.. literalinclude:: ../../../samples/agent/async/team_supervisor_inspect.py + :language: python + :start-at: async def main(): + :end-before: created_objects = [] + +.. latex:clearpage:: + +Continue inside ``main()`` with creation, execution, inspection, and cleanup. +Keep this block indented as part of the function, then add the entry point: + +.. literalinclude:: ../../../samples/agent/async/team_supervisor_inspect.py + :language: python + :start-at: created_objects = [] + +Example output (generated names, model responses, and JSON metadata vary): + +output:: + + Team response: + Supervisor agent: SAMPLE_SUPERVISOR_ + Supervisor task: + Team description: + Team tools: + .. latex:clearpage:: Run Team @@ -337,6 +494,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 @@ -360,6 +533,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/docs/_sources/user_guide/async_profile.rst.txt b/docs/_sources/user_guide/async_profile.rst.txt index de1c9f8..9d737a1 100644 --- a/docs/_sources/user_guide/async_profile.rst.txt +++ b/docs/_sources/user_guide/async_profile.rst.txt @@ -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,99 @@ 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``. +The SDK awaits the database function before reading its returned CLOB in +chunks. The first chunk is available after that function returns; this does +not guarantee token delivery while the model is generating. A partially +consumed iterator retains its cursor and connection. Use ``aclosing()`` when +iteration can stop early, and consume session streams before leaving the +``chat_session()`` context: + +.. code-block:: python + + from contextlib import aclosing + + chunks = await async_profile.chat("Explain connection pooling.", stream=True) + async with aclosing(chunks): + async for chunk in chunks: + print(chunk, end="") + +.. _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 +523,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 +567,57 @@ output:: Async pipeline ********************* +.. _pipeline-compatibility: + +Pipeline compatibility and results +++++++++++++++++++++++++++++++++++ + +``run_pipeline()`` requires ``python-oracledb>=2.4.0``, which provides +``create_pipeline()`` and ``AsyncConnection.run_pipeline()``. If an older +driver lacks these APIs, upgrade it with ``python -m pip install --upgrade +oracledb``. Async operations use Thin mode. + +The driver documents true pipelining for Oracle AI Database 26ai and later. +On older databases it executes operations individually, without reducing +round trips. Pipeline operations execute sequentially, even when true +pipelining is available. Select AI returns CLOBs, and reading these adds +database calls; do not assume the complete SDK operation uses one round trip. +See `python-oracledb pipelining +`__. + +Results preserve input order. Successful results are strings read from CLOBs. +For ``Action.RUNSQL``, this is the database's JSON response, rather than the +``pandas.DataFrame`` returned by ``AsyncProfile.run_sql()``. With +``continue_on_error=False`` (the default), an operation error raises an +exception. With ``continue_on_error=True``, failed entries contain driver +error objects; check their type before using them as text: + +.. code-block:: python + + results = await async_profile.run_pipeline( + [("How many promotions?", select_ai.Action.SHOWSQL)], + continue_on_error=True, + ) + for result in results: + if isinstance(result, str): + print(result) + else: + print(f"Pipeline operation failed: {result}") + +``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- @@ -451,18 +647,131 @@ output:: .. latex:clearpage:: +****************************** +Share a profile asynchronously +****************************** + +While connected as the profile owner, grant access to an existing profile: + +.. code-block:: python + + profile = await select_ai.AsyncProfile.fetch("OCI_AI_PROFILE") + await profile.grant_access("APP_USER") + +The recipient can fetch the profile with ``owner="APP_OWNER"`` in a separate +connection. When access should end, run +``await profile.revoke_access("APP_USER")`` as the owner. See +:ref:`Sharing and ownership ` for the complete owner and recipient +sequence. + +.. raw:: latex + + \Needspace{25\baselineskip} + +************************************ +Give async feedback on generated SQL +************************************ + +Use feedback after reviewing the SQL produced for a prompt. Identify the +request with a ``(prompt, Action)`` tuple or a ``sql_id`` from +``V$MAPPED_SQL``. The example below assumes an open async connection and an +existing profile over the ``SH`` schema: + +.. code-block:: python + + profile = await select_ai.AsyncProfile.fetch("OCI_AI_PROFILE") + prompt = "How many promotions?" + sql = await profile.show_sql(prompt=prompt) + print(sql) + +After inspecting the SQL, record positive feedback if it answers the question: + +.. code-block:: python + + await profile.add_positive_feedback( + prompt_spec=(prompt, select_ai.Action.SHOWSQL) + ) + +For an incorrect answer, use ``add_negative_feedback()`` with the same request +identifier, ``response`` containing the expected SQL, and ``feedback_content`` +explaining the correction. Remove obsolete feedback with +``delete_feedback()`` and the same request identifier. +Await these methods when using ``AsyncProfile``. + +.. latex:clearpage:: + **************************** List profiles asynchronously **************************** -Profile listing returns profiles visible to the connected database user. The -async list API returns an async iterator. +Use ``AsyncProfile.list()`` to discover saved profiles in the current schema. +Call it on the class; you do not need to create an empty profile first. +Each returned object includes ``profile_name``, ``owner``, and the +owner-qualified ``qualified_name``. + +Connect and list your profiles +++++++++++++++++++++++++++++++ + +Set ``SELECT_AI_USER``, ``SELECT_AI_PASSWORD``, and +``SELECT_AI_DB_CONNECT_STRING`` before running this complete script. It prints +one qualified name per profile and closes the connection afterward: .. literalinclude:: ../../../samples/async/profiles_list.py :language: python - :lines: 14- + :start-at: import -output:: +For a schema named ``APP_OWNER`` containing two profiles, the output could be: + +.. code-block:: text + + APP_OWNER.OCI_AI_PROFILE + APP_OWNER.OCI_VECTOR_AI_PROFILE + +Your results depend on the profiles visible in your schema; result order is +not guaranteed. An empty result means no profiles match the requested scope. + +Filter by name +++++++++++++++ + +With a connection open, use ``profile_name_pattern`` to match names with a +regular expression. This example lists names starting with ``OCI_``: + +.. code-block:: python + + async for profile in select_ai.AsyncProfile.list(profile_name_pattern="^OCI_"): + print(profile.qualified_name) + +``AsyncProfile.list()`` returns an async iterator. Use ``async for`` +directly; do not await the iterator. Put these snippets inside your async +function before disconnecting. + +.. raw:: latex + + \Needspace{27\baselineskip} + +Find and reuse a shared profile ++++++++++++++++++++++++++++++++ + +Pass ``owner`` to list profiles in another schema. The connected user must +have access to those profiles; specifying an owner does not grant access. +While connected as the recipient, list the profiles shared by ``APP_OWNER``: + +.. code-block:: python + + async for profile in select_ai.AsyncProfile.list(owner="APP_OWNER"): + print(profile.qualified_name) + +Once you know the profile name, fetch that profile with the same owner and +use the returned object directly: + +.. code-block:: python + + profile = await select_ai.AsyncProfile.fetch( + "OCI_AI_PROFILE", owner="APP_OWNER" + ) + print(profile.qualified_name) # APP_OWNER.OCI_AI_PROFILE + response = await profile.chat(prompt="What is retrieval augmented generation?") + print(response) - OCI_VECTOR_AI_PROFILE - OCI_AI_PROFILE +See :ref:`Sharing and ownership ` for the owner's grant and revoke +steps. diff --git a/docs/_sources/user_guide/cli.rst.txt b/docs/_sources/user_guide/cli.rst.txt index d8d084e..115de9b 100644 --- a/docs/_sources/user_guide/cli.rst.txt +++ b/docs/_sources/user_guide/cli.rst.txt @@ -15,9 +15,12 @@ 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 +server, clustered gateway and worker, and Agent Card generator. RAG is +supported when the selected profile is already configured with a vector +index. The :ref:`A2A guide ` explains the difference between fixed and +dynamic connections and standalone and clustered topologies; this page gives +the corresponding CLI commands. .. only:: html @@ -32,28 +35,35 @@ workflows will be added in upcoming releases as the CLI evolves. :width: 100% The package provides an optional ``select-ai`` command line tool. Install the -CLI extra to use it: +CLI extra to use profile, SQL, and chat commands. Install the separate ``a2a`` +extra for the A2A server and worker commands. If A2A dependencies are missing, +invoking the A2A command group or a server or worker command reports how to +install them; the other CLI commands remain available: .. code-block:: bash pip install 'select_ai[cli]' + # Install A2A support: + 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. -Set the database connection details as environment variables, or pass them as -command line options: - -.. code-block:: bash - - export SELECT_AI_USER= - export SELECT_AI_PASSWORD= - export SELECT_AI_DB_CONNECT_STRING= +The A2A examples below pass database credentials with CLI options so the +commands are easy to copy and adapt. Environment-variable equivalents are +listed in the connection-options table. Connection options ================== -All CLI commands accept the same database connection options: +Database-backed commands accept the following connection options. The +``select-ai a2a serve`` command accepts these options directly. In standalone +mode, providing all four connection values at startup selects the fixed +connection path; omitting values opens a dynamic A2UI form for them. In +clustered mode, the gateway can preconfigure any values and the form collects +the rest. The worker receives the resolved values when it opens a session. +For a private database CA, use ``--database-tls-ca-file``; the CLI reads PEM +from the specified file. .. list-table:: Connection options :header-rows: 1 @@ -72,10 +82,17 @@ All CLI commands accept the same database connection options: - ``SELECT_AI_WALLET_LOCATION`` * - ``--wallet-password`` - ``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. + * - ``--database-tls-ca-file`` + - ``SELECT_AI_DATABASE_TLS_CA_FILE`` + +Profile, SQL, and chat commands prompt for the database password when neither +``--password`` nor ``SELECT_AI_PASSWORD`` is set. For ``a2a serve``, omitting +any connection value selects dynamic mode, and the A2A client supplies the +missing values through an A2UI form. Supply ``--dsn``, ``--user``, +``--password``, and ``--team`` for fixed standalone mode. Wallet options apply +to fixed standalone A2A connections; dynamic sessions do not currently +support Oracle wallet authentication. The A2A CLI examples below include a +host, port, service name, credentials, and optional database CA file. Interactive chat ================ @@ -181,6 +198,284 @@ 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 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 the public A2A HTTP server. The default standalone topology needs + no worker or Consul. Add ``--deployment clustered`` to use a gateway, + workers, and Consul. + * - ``select-ai a2a worker`` + - Start an internal database worker for clustered topology. Standalone + topology does not use this command. + * - ``select-ai a2a agent-card`` + - Print a Gemini Enterprise-compatible A2A v0.3 Agent Card without + starting a server. + +.. _a2a-cli-examples: + +A2A deployment examples +----------------------- + +The :ref:`A2A guide ` explains how fixed and dynamic connections differ +from standalone and clustered topologies. These examples use CLI options for +the database settings. Replace the placeholder credentials and connection +string with your values. + +Fixed standalone connection +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Pass all four connection values at startup to use the standalone server's +shared connection pool. This needs no Consul or worker and supports streaming: + +.. code-block:: bash + + select-ai a2a serve \ + --deployment standalone \ + --team ORACLE_AI_DATABASE_AGENT \ + --user DB_USERNAME \ + --password 'DB_PASSWORD' \ + --dsn 'dbhost.example.com:1521/db_service' \ + --database-tls-ca-file /run/secrets/database-ca.pem \ + --database-call-timeout-seconds 120 \ + --host 0.0.0.0 \ + --port 8000 \ + --public-url https://a2a.example.com + +The database CA file is optional when the server certificate is already +trusted by the operating system. ``--wallet-location`` and +``--wallet-password`` are also available for fixed standalone connections +that use an Oracle wallet. + +.. only:: html + + .. image:: /image/select_ai_a2a_server_demo.gif + :alt: Select AI A2A server CLI demo + :width: 100% + +Dynamic standalone connection +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Omit one or more of ``--dsn``, ``--user``, ``--password``, and ``--team`` to +have the A2A client supply those values through an A2UI form. The server runs +the session child processes itself, so this mode needs no Consul or separate +worker. Run a single Uvicorn process. Dynamic standalone supports task polling, +not streaming: + +.. code-block:: bash + + select-ai a2a serve \ + --deployment standalone \ + --database-tls-ca-file /run/secrets/database-ca.pem \ + --host 0.0.0.0 \ + --port 8000 \ + --public-url https://a2a.example.com + +Remove ``--database-tls-ca-file`` when the database server certificate is +already trusted by the operating system. Any connection values provided on +the command line are fixed; the form asks only for the values that are +missing. + +Clustered gateway and worker +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Clustered topology requires Consul and at least one worker. Consul is the +worker directory and session-routing service: it helps the gateway find a +healthy worker for a new session and send later requests back to that worker. +See :ref:`Why clustered deployments use Consul ` for a worked +example. Point the gateway and all workers at the same Consul service with +``--consul-url``. + +The gateway can run without database credentials; by default, the A2A client supplies them +through the connection form. For local development, start Consul and then run +the worker and gateway in separate terminals: + +.. code-block:: bash + + # Terminal 1: local development Consul agent + consul agent -dev -bind=127.0.0.1 -client=127.0.0.1 + + # Terminal 2: database worker + select-ai a2a worker \ + --host 127.0.0.1 \ + --port 8081 \ + --consul-url http://127.0.0.1:8500 \ + --worker-id local-worker \ + --worker-endpoint http://127.0.0.1:8081 \ + --database-call-timeout-seconds 120 + + # Terminal 3: public gateway + select-ai a2a serve \ + --deployment clustered \ + --host 127.0.0.1 \ + --port 8000 \ + --public-url http://127.0.0.1:8000 \ + --consul-url http://127.0.0.1:8500 \ + --database-tls-ca-file /run/secrets/database-ca.pem \ + --database-call-timeout-seconds 120 + +The local Consul command is for development only. In a deployment, point the +gateway and workers at the existing Consul service and use network-reachable +worker addresses. The database CA option is needed only for a private database +CA; the gateway passes its PEM contents to the worker when opening a session. +Set the same ``--database-call-timeout-seconds`` value on the gateway and +worker. See the A2A guide for Consul setup and clustered deployment details. + +Important ``serve`` options include ``--team``, ``--host``, ``--port``, +``--public-url``, ``--description``, ``--pool-max-size``, and +``--database-call-timeout-seconds``. The default database call timeout is 120 +seconds. On the clustered gateway, this value sets the request deadline basis; +the worker applies it to Oracle calls and derives its child-process deadline. + +By default, ``a2a serve`` does not require an application OAuth token. A2A +task and context records use a shared owner scope, and each A2A context maps +to its own Select AI conversation. Add ``--require-oauth`` to require +``Authorization: Bearer ...`` and derive an owner scope as well as context. +The SDK does not validate token signatures or claims. A trusted upstream +service must validate the end-user token and prevent direct access that +bypasses validation. See :ref:`the A2A authentication contract +`. +The Agent Card advertises bearer security only in that mode. + +Clustered option reference +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Configure discovery first, then connection values and session limits. Explicit +CLI values take precedence over environment fallbacks. For a fixed clustered +connection, see the :ref:`complete gateway command `. + +Gateway discovery ++++++++++++++++++ + +``--public-url`` (``PUBLIC_URL``) + Client-visible base URL advertised in the Agent Card. + +``--consul-url`` (``CONSUL_HTTP_URL``) + Consul HTTP API URL. Use the same Consul service as the workers. + +``--worker-service`` (``WORKER_SERVICE``) + Consul service to discover. Default: ``select-ai-a2a-worker``. + +Worker registration ++++++++++++++++++++ + +``--host`` and ``--port`` + Worker listen address and port. The port is also registered with Consul. + +``--worker-id`` (``WORKER_ID``) + Unique worker identifier. Defaults to the host name. + +``--consul-url`` (``CONSUL_HTTP_URL``) + Consul HTTP API URL used to register the worker and send heartbeats. + +``--worker-endpoint`` (``WORKER_ENDPOINT``) + Worker URL reachable by the gateway. In cluster manifests, + ``WORKER_ADDRESS`` can supply the pod IP when no endpoint is given. + +Connection values and forms ++++++++++++++++++++++++++++ + +``--dsn``, ``--user``, ``--password``, and ``--team`` (gateway) + Fix the supplied values at startup. Set all four for a fixed connection; + otherwise the A2UI form asks only for the missing values. The environment + fallbacks are ``SELECT_AI_DB_CONNECT_STRING``, ``SELECT_AI_USER``, + ``SELECT_AI_PASSWORD``, and ``SELECT_AI_A2A_TEAM``. + +``--a2ui-form`` (gateway) + Path to a custom A2UI JSON form. It must submit exactly the missing + connection values. See :ref:`A2A Integration ` for the form contract. + +Session lifetime and timeouts ++++++++++++++++++++++++++++++ + +``--session-ttl-seconds`` (gateway and worker) + Lifetime of a temporary database session; default 900 seconds. + The gateway also reads ``SESSION_TTL_SECONDS``. Configure the same lifetime + on both processes. + +``--conversation-retention-days`` (worker) + Days to retain Oracle conversations; default 7. Environment fallback: + ``CONVERSATION_RETENTION_DAYS``. Use 0 to retain until manually deleted. + +``--session-start-timeout-seconds`` (worker) + Maximum wait for a child process to initialize its database session. + +``--database-call-timeout-seconds`` (gateway and worker) + Database call timeout; default 120 seconds. Set the same value on both + processes. The worker applies it to Oracle calls; both processes derive + their request deadlines from it. + +Authentication and TLS +++++++++++++++++++++++ + +``--require-oauth`` (gateway) + Require a bearer token and derive an owner scope for task and context + records. Token validation must occur upstream. Uses the same ownership + behavior as standalone mode. + +``--tls-cert-file`` and ``--tls-key-file`` (gateway) + Enable HTTPS on the public A2A listener. Supply both together; without + them, the listener uses HTTP. + +``--tls-ca-file`` (gateway) + Optional CA that makes the public listener require and verify client + certificates (mTLS). Requires ``--tls-cert-file`` and ``--tls-key-file``. + +``--worker-tls-ca-file``, ``--worker-tls-cert-file``, ``--worker-tls-key-file`` + Gateway TLS options for worker connections. The CA verifies the worker's + server certificate; it can be supplied alone for HTTPS without mTLS. If + omitted, Requests uses its default CA bundle. The client certificate and + key are optional, but must be supplied together; the worker must require + and trust them for mTLS. When any ``--worker-tls-*`` option is used, set + ``--worker-endpoint`` to an HTTPS URL. + Environment fallbacks are ``WORKER_TLS_CA_FILE``, + ``WORKER_TLS_CERT_FILE``, and ``WORKER_TLS_KEY_FILE``. + +``--tls-ca-file``, ``--tls-cert-file``, ``--tls-key-file`` (worker) + The certificate and key enable HTTPS on the worker and must be supplied + together. ``--tls-ca-file`` is optional; when supplied, the worker + requires and verifies client certificates for mTLS. Set + ``--worker-endpoint`` to an HTTPS URL so the gateway connects over TLS. + +``--database-tls-ca-file`` (gateway) + CA for verifying the Oracle database certificate. The gateway passes the + PEM contents to the worker session. This is separate from the certificates + for the gateway-to-worker HTTP connection and from Oracle wallet mTLS. + +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 +502,12 @@ 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 public A2A server. The default is standalone; use + ``--deployment clustered`` for a gateway and workers. Omitted + connection values are collected through A2UI. + * - ``select-ai a2a worker`` + - Start a database worker for clustered topology. Standalone deployments + do not use a worker. + * - ``select-ai a2a agent-card`` + - Print an A2A v0.3 Agent Card. diff --git a/docs/_sources/user_guide/compatibility.rst.txt b/docs/_sources/user_guide/compatibility.rst.txt new file mode 100644 index 0000000..0560f0b --- /dev/null +++ b/docs/_sources/user_guide/compatibility.rst.txt @@ -0,0 +1,71 @@ +.. _compatibility: + +****************************** +Compatibility and requirements +****************************** + +Select AI for Python 1.5.0 supports Python 3.11, 3.12, 3.13, and 3.14. +Installation requires ``oracledb>=2.0.0`` and ``pandas==3.0.3``. Some optional +features require driver APIs introduced after the minimum installable driver +version; upgrade ``oracledb`` if those APIs are unavailable. + +Installing the SDK does not install database PL/SQL packages or enable their +features. Availability depends on the Oracle Database service, version, and +package updates. Consult Oracle's `Select AI and Select AI Agent capability +matrix `__ +for your environment before choosing a workflow. + +.. list-table:: Feature requirements + :header-rows: 1 + :widths: 25 35 40 + + * - Feature + - Python installation + - Database and deployment requirements + * - Profiles, SQL generation, and chat + - ``select_ai`` + - ``DBMS_CLOUD_AI`` and ``DBMS_CLOUD``; provider credential and outbound + database network access. SQL execution also requires object privileges. + * - Async APIs + - ``select_ai`` with an async-capable ``oracledb`` driver + - Thin mode; use an async connection or async pool. Use a pool for + independent concurrent database operations. + * - Pipelines + - ``oracledb>=2.4.0`` with ``create_pipeline()`` and + ``AsyncConnection.run_pipeline()`` + - True pipelining requires Oracle AI Database 26ai or later. Older + databases execute individually. See :ref:`pipeline-compatibility`. + * - RAG vector indexes + - ``select_ai`` + - Vector-index support in ``DBMS_CLOUD_AI``, ``DBMS_CLOUD_PIPELINE``, an + embedding model, source access, and database storage capacity. + * - Agent teams and history + - ``select_ai`` + - ``DBMS_CLOUD_AI_AGENT`` and the corresponding agent history views. + * - Profile, index, team, and credential sharing + - ``select_ai`` + - Database packages providing the relevant grant/revoke APIs. The + recipient still needs privileges on workflow dependencies. + * - CLI profile, SQL, and chat commands + - ``select_ai[cli]`` + - Same requirements as the corresponding SDK operation. + * - A2A fixed standalone + - ``select_ai[a2a]`` + - Existing agent team; A2A storage privileges and quota. Supports + streaming and wallet authentication. + * - A2A dynamic standalone + - ``select_ai[a2a]`` + - One Uvicorn process; A2UI-capable client. Polling supported; streaming + and Oracle wallet authentication are unavailable. + * - A2A clustered + - ``select_ai[a2a]`` on gateway and workers + - Consul and at least one worker. Polling supported; streaming and + Oracle wallet authentication are unavailable. + +The OCI credential-loading examples also require ``oci`` and a configured +OCI API-signing key. This is a sample dependency, rather than an SDK runtime +requirement. See :ref:`Credential `. + +Provider models, regions, and account permissions are independent of Python +package compatibility. Replace example model identifiers and storage URIs +with values supported by your provider account and database integration. diff --git a/docs/_sources/user_guide/concurrent_prompt_processing.rst.txt b/docs/_sources/user_guide/concurrent_prompt_processing.rst.txt index 1f2e165..3a5b04d 100644 --- a/docs/_sources/user_guide/concurrent_prompt_processing.rst.txt +++ b/docs/_sources/user_guide/concurrent_prompt_processing.rst.txt @@ -46,7 +46,7 @@ Recipe summary * - Async pipeline - :ref:`async_pipeline_recipe` - Use ``run_pipeline()`` when all prompt/action pairs are known up front - and should be sent in a single database round trip. + and should be submitted as a database pipeline. * - Async queue - :ref:`async_queue_workers_recipe` - Use async queue workers for long-running async services or background @@ -63,6 +63,10 @@ The recipes use the same connection environment variables as the other samples: export SELECT_AI_PASSWORD= export SELECT_AI_DB_CONNECT_STRING= +.. raw:: latex + + \Needspace{10\baselineskip} + Optional environment variables control pool sizing and profile names: .. code-block:: sh @@ -75,6 +79,8 @@ Optional environment variables control pool sizing and profile names: Use ``SELECT_AI_PROFILE_NAME=async_oci_ai_profile`` for the async recipes if that is the async profile name in your environment. +.. latex:clearpage:: + .. _sync_thread_pool_recipe: ``sync_thread_pool.py`` @@ -83,9 +89,23 @@ that is the async profile name in your environment. This recipe uses ``ThreadPoolExecutor`` and ``as_completed()``. Results are printed in the order they finish. +Set up the imports, connection values, prompts, and any helper functions: + .. literalinclude:: ../../../recipes/concurrent_prompt_processing/sync_thread_pool.py :language: python - :lines: 14- + :start-at: import + :end-before: select_ai.create_pool( + +.. latex:clearpage:: + +The following code creates the connection pool, processes the prompts, and +closes the pool in a ``finally`` block: + +.. literalinclude:: ../../../recipes/concurrent_prompt_processing/sync_thread_pool.py + :language: python + :start-at: select_ai.create_pool( + +.. latex:clearpage:: .. _sync_ordered_results_recipe: @@ -95,9 +115,23 @@ printed in the order they finish. This recipe uses ``ThreadPoolExecutor.map()``. Prompts run concurrently, but results are printed in the same order as the input list. +Set up the imports, connection values, prompts, and any helper functions: + +.. literalinclude:: ../../../recipes/concurrent_prompt_processing/sync_ordered_results.py + :language: python + :start-at: import + :end-before: select_ai.create_pool( + +.. latex:clearpage:: + +The following code creates the connection pool, processes the prompts, and +closes the pool in a ``finally`` block: + .. literalinclude:: ../../../recipes/concurrent_prompt_processing/sync_ordered_results.py :language: python - :lines: 14- + :start-at: select_ai.create_pool( + +.. latex:clearpage:: .. _sync_queue_workers_recipe: @@ -107,9 +141,23 @@ results are printed in the same order as the input list. This recipe uses worker threads and ``queue.Queue``. It is useful when prompt producers and prompt processors are separate parts of an application. +Set up the imports, connection values, prompts, and any helper functions: + .. literalinclude:: ../../../recipes/concurrent_prompt_processing/sync_queue_workers.py :language: python - :lines: 14- + :start-at: import + :end-before: select_ai.create_pool( + +.. latex:clearpage:: + +The following code creates the connection pool, processes the prompts, and +closes the pool in a ``finally`` block: + +.. literalinclude:: ../../../recipes/concurrent_prompt_processing/sync_queue_workers.py + :language: python + :start-at: select_ai.create_pool( + +.. latex:clearpage:: .. _async_gather_recipe: @@ -119,9 +167,23 @@ producers and prompt processors are separate parts of an application. This recipe uses ``asyncio.gather()``. Prompts run concurrently, and results are returned in the same order as the input task list. +Set up the imports, connection values, prompts, and any helper functions: + +.. literalinclude:: ../../../recipes/concurrent_prompt_processing/async_gather.py + :language: python + :start-at: import + :end-before: async def main(): + +.. latex:clearpage:: + +The following code creates the connection pool, processes the prompts, and +closes the pool in a ``finally`` block: + .. literalinclude:: ../../../recipes/concurrent_prompt_processing/async_gather.py :language: python - :lines: 14- + :start-at: async def main(): + +.. latex:clearpage:: .. _async_as_completed_recipe: @@ -131,23 +193,58 @@ are returned in the same order as the input task list. This recipe uses ``asyncio.as_completed()``. It is useful for command-line tools or services that can forward each answer as soon as it is ready. +Set up the imports, connection values, prompts, and any helper functions: + .. literalinclude:: ../../../recipes/concurrent_prompt_processing/async_as_completed.py :language: python - :lines: 14- + :start-at: import + :end-before: async def main(): + +.. latex:clearpage:: + +The following code creates the connection pool, processes the prompts, and +closes the pool in a ``finally`` block: + +.. literalinclude:: ../../../recipes/concurrent_prompt_processing/async_as_completed.py + :language: python + :start-at: async def main(): + +.. latex:clearpage:: .. _async_pipeline_recipe: ``async_pipeline.py`` ===================== -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. +This recipe uses ``AsyncProfile.run_pipeline()`` to submit multiple +prompt/action pairs as a batch. Pipeline operations execute sequentially in +the database; they do not run in parallel. The application receives the batch +results when the pipeline completes. Pass ``attributes=...`` to apply the +same request-level profile overrides to every item in the pipeline. + +True pipelining requires a supporting database version. On older databases, +the driver executes operations individually, without reducing round trips. +Reading returned CLOBs also requires additional database calls. See +:ref:`Pipeline compatibility ` for requirements and +result handling. + +Set up the imports, connection values, prompts, and any helper functions: .. literalinclude:: ../../../recipes/concurrent_prompt_processing/async_pipeline.py :language: python - :lines: 14- + :start-at: import + :end-before: async def main(): + +.. latex:clearpage:: + +The following code creates the connection pool, processes the prompts, and +closes the pool in a ``finally`` block: + +.. literalinclude:: ../../../recipes/concurrent_prompt_processing/async_pipeline.py + :language: python + :start-at: async def main(): + +.. latex:clearpage:: .. _async_queue_workers_recipe: @@ -157,9 +254,21 @@ results when the pipeline completes. This recipe uses ``asyncio.Queue`` and async worker tasks. It is useful for long-running async applications that receive prompts over time. +Set up the imports, connection values, prompts, and any helper functions: + +.. literalinclude:: ../../../recipes/concurrent_prompt_processing/async_queue_workers.py + :language: python + :start-at: import + :end-before: async def main(): + +.. latex:clearpage:: + +The following code creates the connection pool, processes the prompts, and +closes the pool in a ``finally`` block: + .. literalinclude:: ../../../recipes/concurrent_prompt_processing/async_queue_workers.py :language: python - :lines: 14- + :start-at: async def main(): Pool sizing =========== diff --git a/docs/_sources/user_guide/connection.rst.txt b/docs/_sources/user_guide/connection.rst.txt index 66267dd..2f740d0 100644 --- a/docs/_sources/user_guide/connection.rst.txt +++ b/docs/_sources/user_guide/connection.rst.txt @@ -9,7 +9,10 @@ to connect to the database and execute PL/SQL subprograms. The library keeps the active connection or connection pool for the current process so profile, credential, provider, vector index, and agent APIs can use -it without passing a connection object to each call. Use a standalone +it without passing a connection object to each call. Standalone connections +are local to the creating thread; pools are shared within a process. Do not +configure both a standalone connection and a pool for the same API style in +one thread: disconnect before switching. Use a standalone connection for scripts and notebooks. Use a connection pool for applications that handle concurrent work, such as web services or worker processes. diff --git a/docs/_sources/user_guide/conversation.rst.txt b/docs/_sources/user_guide/conversation.rst.txt index a9db8f7..1f7c2ef 100644 --- a/docs/_sources/user_guide/conversation.rst.txt +++ b/docs/_sources/user_guide/conversation.rst.txt @@ -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. @@ -130,7 +148,7 @@ output:: .. latex:clearpage:: Chat session -+++++++++++++ +++++++++++++ Use ``chat_session()`` to keep context across multiple chat prompts. The second prompt in this example can refer to the previous answer because both prompts use @@ -157,6 +175,51 @@ 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) + if prompts: + 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 + :start-at: import + +The conversation ID, prompt ID, and model response vary between runs. +Example output (generated IDs and model responses vary): + +output:: + + conversation history works. + Prompt : Reply with exactly: conversation history works. + Prompts after deletion: [] + +.. latex:clearpage:: + List conversations ++++++++++++++++++ @@ -223,6 +286,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 +326,83 @@ output:: .. latex:clearpage:: +Async prompt history and tags ++++++++++++++++++++++++++++++ + +Use prompt history to inspect or remove stored prompts, and tags to attach +application metadata to a conversation. This example first creates a +conversation and records a chat prompt so there is history to inspect. + +Create a conversation and save a prompt +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +With an async connection open, run the following inside an async function. +The saved profile ``oci_ai_profile`` must already exist: + +.. code-block:: python + + conversation = select_ai.AsyncConversation( + attributes=select_ai.ConversationAttributes(title="Prompt history demo") + ) + await conversation.create() + profile = await select_ai.AsyncProfile(profile_name="oci_ai_profile") + async with profile.chat_session(conversation=conversation) as session: + response = await session.chat("Explain retrieval augmented generation.") + print(response) + +Tag and inspect the conversation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A second ``add_tag()`` call with the same key updates its value. +``list_prompts()`` returns an async iterator of ``ConversationPrompt`` objects: + +.. code-block:: python + + await conversation.add_tag("PROJECT", "SELECT_AI") + await conversation.add_tag("PROJECT", "SELECT_AI_SAMPLES") + prompts = [prompt async for prompt in conversation.list_prompts()] + for prompt in prompts: + print(prompt.conversation_prompt_id, prompt.prompt) + +Remove a stored prompt and its tag +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Delete by prompt ID, then remove the tag by key. Check for an empty history +before choosing a prompt to delete: + +.. code-block:: python + + if prompts: + await conversation.delete_prompt(prompts[-1].conversation_prompt_id) + await conversation.remove_tag("PROJECT") + +Use ``force=True`` on ``delete_prompt()`` or ``remove_tag()`` when a missing +prompt or tag should be ignored. When finished with this temporary conversation, +call ``await conversation.delete(force=True)``. + +Run the complete example +~~~~~~~~~~~~~~~~~~~~~~~~ + +The `complete async prompt-history script `__ +adds connection setup and a ``finally`` block that deletes the temporary +conversation. Set the common database environment variables and optionally +set ``SELECT_AI_PROFILE_NAME`` (default ``oci_ai_profile``), then run from the +repository root: + +.. code-block:: bash + + python samples/async/conversation_prompts_tags.py + +Example output (prompt IDs and model responses vary): + +.. code-block:: text + + conversation history works. + Prompt : Reply with exactly: conversation history works. + Prompts after deletion: [] + +.. latex:clearpage:: + Async list conversations ++++++++++++++++++++++++ diff --git a/docs/_sources/user_guide/credential.rst.txt b/docs/_sources/user_guide/credential.rst.txt index 19ad639..6836e7a 100644 --- a/docs/_sources/user_guide/credential.rst.txt +++ b/docs/_sources/user_guide/credential.rst.txt @@ -25,6 +25,7 @@ formats. :header-rows: 1 :widths: 30 70 :align: left + :class: longtable * - AI provider - Credential format @@ -104,12 +105,37 @@ Create credential In this example, we create a credential object to authenticate to OCI Gen AI service provider: +The synchronous and asynchronous samples below use the OCI Python SDK to read +an existing API-signing configuration. Install this sample-specific dependency: + +.. code-block:: shell + + python -m pip install oci + +Configure the ``DEFAULT`` profile in ``~/.oci/config`` with ``user``, +``tenancy``, ``fingerprint``, ``region``, and ``key_file``. The ``key_file`` +must point to a readable private key whose public key is registered with the +OCI user. See `OCI SDK configuration +`__. +The samples call ``oci.config.from_file()`` with its default path and profile; +edit that call if you use another configuration. Keep the configuration and +private key outside your source repository. + +The ``oci`` package is needed only to load this configuration in the samples. +The Select AI credential API itself accepts the credential dictionary shown +above without requiring the OCI SDK. + 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 +147,8 @@ output:: .. latex:clearpage:: -Async API -+++++++++ +Create Async API +++++++++++++++++ .. literalinclude:: ../../../samples/async/create_ai_credential.py :language: python @@ -140,10 +166,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 +180,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 +190,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/docs/_sources/user_guide/installation.rst.txt b/docs/_sources/user_guide/installation.rst.txt index 078f39d..d83f2cb 100644 --- a/docs/_sources/user_guide/installation.rst.txt +++ b/docs/_sources/user_guide/installation.rst.txt @@ -72,8 +72,15 @@ are isolated from your system Python installation. python -m pip install --upgrade "select_ai[cli]" - This installs the ``select-ai`` command. See :ref:`Command Line Interface - `. + This installs the ``select-ai`` command with profile, SQL, and chat + commands. To use the A2A server and worker commands, install the separate + ``a2a`` extra: + + .. code-block:: shell + + python -m pip install --upgrade "select_ai[a2a]" + + See :ref:`Command Line Interface ` and :ref:`A2A Integration `. 6. If you are behind a proxy, use the ``--proxy`` option. For example: @@ -123,6 +130,9 @@ Oracle Database. Connected to the Database +Once the connection smoke test succeeds, follow :ref:`Run your first SQL +query ` to create a credential and profile and query sample data. + Install documentation dependencies ================================== diff --git a/docs/_sources/user_guide/introduction.rst.txt b/docs/_sources/user_guide/introduction.rst.txt index ab0774d..2e182cc 100644 --- a/docs/_sources/user_guide/introduction.rst.txt +++ b/docs/_sources/user_guide/introduction.rst.txt @@ -14,26 +14,42 @@ Python ecosystem. It gives Python applications a higher-level API for working with AI providers, credentials, profiles, natural language prompts, vector indexes, conversations, summarization, synthetic data, and AI agent workflows. +.. only:: html + + .. figure:: /image/select_ai_a2a_server_demo.gif + :alt: Select AI A2A server demo from the project README + :width: 100% + + Connect an A2A client to a Select AI agent team. Follow the + :ref:`A2A guide ` to run the server. + +For a complete setup-to-query example, start with :ref:`Run your first SQL +query `. Existing users can review :ref:`What's new in 1.5.0 +` and :ref:`Compatibility `. + What you can build ================== -Use ``select_ai`` to: - -* Ask questions about database objects in natural language and generate SQL. -* Run generated SQL and return results as Python objects such as pandas data - frames. -* Generate narrative answers, explanations, prompt previews, translations, and - summaries. -* Use Retrieval Augmented Generation (RAG) with vector indexes over documents - and object storage content. -* Create synthetic data for database tables. -* Build context-aware chat sessions with database-backed conversations. -* Register tools, tasks, agents, and teams for database-backed AI agent - workflows. -* Use the optional ``select-ai`` command line interface for interactive chat - and SQL workflows. -* Use synchronous APIs, asynchronous APIs, and connection pools in scripts, - services, and web applications. +Start with the workflow you want to build: + +* **Natural language data apps:** generate SQL, return query results as pandas + DataFrames, or explain results in plain language with :ref:`Profiles ` + and :ref:`Async Profiles `. +* **RAG and chat assistants:** ground answers in your documents with + :ref:`Vector Indexes ` and keep chat context with + :ref:`Conversations `. +* **AI agent workflows:** compose tools, tasks, and agents into + :ref:`Teams `, then expose a team to other AI applications through + an :ref:`Agent2Agent (A2A) server `. +* **Python web services:** add Select AI endpoints to FastAPI, Flask, or Django + using the pool lifecycle patterns in :ref:`Web Frameworks `. +* **Concurrent prompt processing:** use threads, async tasks, queues, or a + database pipeline with the :ref:`Concurrency Guides `. +* **Content and test-data tools:** :ref:`summarize content `, + :ref:`translate text `, or generate + :ref:`Synthetic Data ` for database tables. +* **Terminal workflows:** chat, generate SQL, and run an A2A server with the + optional :ref:`Command Line Interface `. Core concepts ============= @@ -59,6 +75,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. @@ -77,6 +97,11 @@ Most workflows use the same building blocks: - Define tools, tasks, agents, and teams for multi-step AI workflows. - :ref:`Agent ` + * - Agent2Agent (A2A) + - Expose an agent team as a discoverable service for A2A clients. Choose + standalone or clustered deployment and fixed or dynamic connections. + - :ref:`A2A server ` + Synchronous and asynchronous APIs ================================= diff --git a/docs/_sources/user_guide/privileges.rst.txt b/docs/_sources/user_guide/privileges.rst.txt index 920ddbe..f6cba55 100644 --- a/docs/_sources/user_guide/privileges.rst.txt +++ b/docs/_sources/user_guide/privileges.rst.txt @@ -103,6 +103,8 @@ output:: The async API is ``select_ai.async_grant_network_access(...)``. +.. latex:clearpage:: + .. literalinclude:: ../../../samples/async/grant_network_access.py :language: python :lines: 14- @@ -128,6 +130,8 @@ output:: The async API is ``select_ai.async_revoke_network_access(...)``. +.. latex:clearpage:: + .. literalinclude:: ../../../samples/async/revoke_network_access.py :language: python :lines: 14- diff --git a/docs/_sources/user_guide/profile.rst.txt b/docs/_sources/user_guide/profile.rst.txt index 9f1749d..6c9c565 100644 --- a/docs/_sources/user_guide/profile.rst.txt +++ b/docs/_sources/user_guide/profile.rst.txt @@ -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,95 @@ 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``. +The SDK calls the database function before reading the returned CLOB in +chunks. This is CLOB consumption, rather than a guarantee that model tokens +arrive while generation is in progress. The first chunk is available only +after the database function returns. A partially consumed iterator keeps its +cursor and connection open. Close it if you stop early, and consume session +streams before leaving ``chat_session()``: + +.. code-block:: python + + from contextlib import closing + + with closing(profile.chat("Explain connection pooling.", stream=True)) as chunks: + for chunk in chunks: + print(chunk, end="") + # Breaking here also closes the iterator on context exit. + +.. _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 +480,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- @@ -366,24 +517,129 @@ output:: Danke +.. latex:clearpage:: + +*************** +Share a profile +*************** + +While connected as the profile owner, grant access to an existing profile: + +.. code-block:: python + + profile = select_ai.Profile.fetch("OCI_AI_PROFILE") + profile.grant_access("APP_USER") + +The recipient can fetch the profile with ``owner="APP_OWNER"`` in a separate +connection. When access should end, run +``profile.revoke_access("APP_USER")`` as the owner. See +:ref:`Sharing and ownership ` for the complete owner and recipient +sequence. + +.. raw:: latex + + \Needspace{25\baselineskip} + +****************************** +Give feedback on generated SQL +****************************** + +Use feedback after reviewing the SQL produced for a prompt. Identify the +request with a ``(prompt, Action)`` tuple or a ``sql_id`` from +``V$MAPPED_SQL``. The example below assumes an open connection and an +existing profile over the ``SH`` schema: + +.. code-block:: python + + profile = select_ai.Profile.fetch("OCI_AI_PROFILE") + prompt = "How many promotions?" + sql = profile.show_sql(prompt=prompt) + print(sql) + +After inspecting the SQL, record positive feedback if it answers the question: + +.. code-block:: python + + profile.add_positive_feedback( + prompt_spec=(prompt, select_ai.Action.SHOWSQL) + ) + +For an incorrect answer, use ``add_negative_feedback()`` with the same request +identifier, ``response`` containing the expected SQL, and ``feedback_content`` +explaining the correction. Remove obsolete feedback with +``delete_feedback()`` and the same request identifier. + + .. latex:clearpage:: ************************** 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. +Use ``Profile.list()`` to discover saved profiles in the current schema. +Call it on the class; you do not need to create an empty profile first. +Each returned object includes ``profile_name``, ``owner``, and the +owner-qualified ``qualified_name``. + +Connect and list your profiles +++++++++++++++++++++++++++++++ + +Set ``SELECT_AI_USER``, ``SELECT_AI_PASSWORD``, and +``SELECT_AI_DB_CONNECT_STRING`` before running this complete script. It prints +one qualified name per profile and closes the connection afterward: .. literalinclude:: ../../../samples/profiles_list.py :language: python - :lines: 14- + :start-at: import +For a schema named ``APP_OWNER`` containing two profiles, the output could be: -output:: +.. code-block:: text + + APP_OWNER.OCI_AI_PROFILE + APP_OWNER.OCI_VECTOR_AI_PROFILE + +Your results depend on the profiles visible in your schema; result order is +not guaranteed. An empty result means no profiles match the requested scope. + +Filter by name +++++++++++++++ + +With a connection open, use ``profile_name_pattern`` to match names with a +regular expression. This example lists names starting with ``OCI_``: + +.. code-block:: python + + for profile in select_ai.Profile.list(profile_name_pattern="^OCI_"): + print(profile.qualified_name) + +.. raw:: latex + + \Needspace{27\baselineskip} + +Find and reuse a shared profile ++++++++++++++++++++++++++++++++ + +Pass ``owner`` to list profiles in another schema. The connected user must +have access to those profiles; specifying an owner does not grant access. +While connected as the recipient, list the profiles shared by ``APP_OWNER``: + +.. code-block:: python + + for profile in select_ai.Profile.list(owner="APP_OWNER"): + print(profile.qualified_name) + +Once you know the profile name, fetch that profile with the same owner and +use the returned object directly: + +.. code-block:: python + + profile = select_ai.Profile.fetch( + "OCI_AI_PROFILE", owner="APP_OWNER" + ) + print(profile.qualified_name) # APP_OWNER.OCI_AI_PROFILE + response = profile.chat(prompt="What is retrieval augmented generation?") + print(response) - ASYNC_OCI_AI_PROFILE - OCI_VECTOR_AI_PROFILE - ASYNC_OCI_VECTOR_AI_PROFILE - OCI_AI_PROFILE +See :ref:`Sharing and ownership ` for the owner's grant and revoke +steps. diff --git a/docs/_sources/user_guide/profile_attributes.rst.txt b/docs/_sources/user_guide/profile_attributes.rst.txt index 4adf67a..03c95f4 100644 --- a/docs/_sources/user_guide/profile_attributes.rst.txt +++ b/docs/_sources/user_guide/profile_attributes.rst.txt @@ -74,6 +74,15 @@ Attribute groups - Tunes model generation behavior. * - ``conversation`` - Enables conversation history for context-aware chat workflows. + * - ``additional_instructions`` + - Provides persistent guidance, business rules, or response constraints + for requests that use the profile. + * - ``source_language``, ``target_language`` + - Set default languages for ``Profile.translate()`` and + ``AsyncProfile.translate()``. 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 @@ -82,13 +91,13 @@ Attribute groups Object list examples ==================== -Grant access to every supported object in a schema: +Include supported objects from a schema in the profile's metadata scope: .. code-block:: python object_list = [{"owner": "SH"}] -Grant access to selected tables: +Include selected tables: .. code-block:: python @@ -98,6 +107,9 @@ Grant access to selected tables: {"owner": "SH", "name": "PRODUCTS"}, ] +The object list does not grant database privileges. The connected user must +already have the privileges needed to access the underlying objects. + Restrict generated SQL to the selected objects: .. code-block:: python @@ -129,5 +141,22 @@ responses: stop_tokens='[";"]', ) +Additional instructions +======================= + +Use ``additional_instructions`` for guidance that should apply to every +request made with a profile: + +.. code-block:: python + + attributes = select_ai.ProfileAttributes( + provider=provider, + credential_name="my_oci_ai_profile_key", + additional_instructions="Return concise, executable Python code.", + ) + +For guidance that applies to only one request, pass an ``attributes`` mapping +to an action method instead of changing the saved profile. + .. autoclass:: select_ai.ProfileAttributes :members: diff --git a/docs/_sources/user_guide/provider.rst.txt b/docs/_sources/user_guide/provider.rst.txt index f75f007..e0ee69a 100644 --- a/docs/_sources/user_guide/provider.rst.txt +++ b/docs/_sources/user_guide/provider.rst.txt @@ -53,11 +53,16 @@ class in this library. - Uses OCI region and OCI Gen AI attributes. * - ``OpenAIProvider`` - ``openai`` - - Uses ``api.openai.com``. + - Keeps ``api.openai.com`` for network ACL setup; omits the default + endpoint from database profiles so Select AI selects its native URL. Examples -------- +.. raw:: latex + + \Needspace{14\baselineskip} + OCI Gen AI provider: .. code-block:: python @@ -68,6 +73,10 @@ OCI Gen AI provider: model="cohere.command-r-plus", ) +.. raw:: latex + + \Needspace{14\baselineskip} + OpenAI provider: .. code-block:: python @@ -76,6 +85,17 @@ OpenAI provider: model="gpt-4.1", ) +For the public OpenAI service, leave ``provider_endpoint`` at its default. +The SDK sends ``provider="openai"`` and the model name, without an endpoint +override. ``OpenAIProvider`` omits ``provider_endpoint`` from database profile +payloads. Use the base ``Provider`` class for a custom OpenAI-compatible +endpoint, as shown below. Do not append ``/v1/chat/completions``; the database +constructs the request path. + +.. raw:: latex + + \Needspace{14\baselineskip} + Azure OpenAI provider: .. code-block:: python @@ -86,6 +106,10 @@ Azure OpenAI provider: azure_embedding_deployment_name="text-embedding-deployment", ) +.. raw:: latex + + \Needspace{14\baselineskip} + AWS Bedrock provider: .. code-block:: python @@ -96,6 +120,8 @@ AWS Bedrock provider: model="anthropic.claude-3-5-sonnet-20240620-v1:0", ) +.. latex:clearpage:: + Custom provider endpoint: .. code-block:: python @@ -229,6 +255,159 @@ output:: .. latex:clearpage:: +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. + +.. latex:clearpage:: + +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 + +First, load the configuration and grant network access as the administrator: + +.. literalinclude:: ../../../samples/profile_create_aws.py + :language: python + :start-at: import os + :end-before: select_ai.connect(user=app_user, password=app_password, dsn=dsn) + +.. latex:clearpage:: + +Then connect as the application user to create the credential and profile. +Append this code to the setup above: + +.. literalinclude:: ../../../samples/profile_create_aws.py + :language: python + :start-at: select_ai.connect(user=app_user, password=app_password, dsn=dsn) + +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. + +.. latex:clearpage:: + +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 + +First, load the configuration and grant network access as the administrator: + +.. literalinclude:: ../../../samples/profile_create_azure.py + :language: python + :start-at: import os + :end-before: select_ai.connect(user=app_user, password=app_password, dsn=dsn) + +.. latex:clearpage:: + +Then connect as the application user to create the credential and profile. +Append this code to the setup above: + +.. literalinclude:: ../../../samples/profile_create_azure.py + :language: python + :start-at: select_ai.connect(user=app_user, password=app_password, dsn=dsn) + +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. + +.. latex:clearpage:: + +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 + +First, load the configuration and grant network access as the administrator: + +.. literalinclude:: ../../../samples/profile_create_gcp.py + :language: python + :start-at: import os + :end-before: select_ai.connect(user=app_user, password=app_password, dsn=dsn) + +.. latex:clearpage:: + +Then connect as the application user to create the credential and profile. +Append this code to the setup above: + +.. literalinclude:: ../../../samples/profile_create_gcp.py + :language: python + :start-at: select_ai.connect(user=app_user, password=app_password, dsn=dsn) + +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:: + Disable AI service provider --------------------------- diff --git a/docs/_sources/user_guide/quickstart.rst.txt b/docs/_sources/user_guide/quickstart.rst.txt new file mode 100644 index 0000000..7c51895 --- /dev/null +++ b/docs/_sources/user_guide/quickstart.rst.txt @@ -0,0 +1,144 @@ +.. _first-query: + +************************ +Run your first SQL query +************************ + +This walkthrough creates a temporary table with three rows, stores an OpenAI +API key in a temporary database credential, creates a Select AI profile, and +queries the table using natural language. It removes its table, credential, +and profile when finished. Use a development database account. + +Requests use the API key in ``OPENAI_API_KEY`` on each run. Existing database +credentials are not used or replaced. OpenAI API usage is billed separately +from ChatGPT and Codex subscriptions. + +Install and configure Python +============================ + +Use Python 3.11 through 3.14 and an Oracle Database environment with +``DBMS_CLOUD_AI`` available. See :ref:`Compatibility ` for +feature-specific requirements. + +.. code-block:: bash + + python3 -m venv .venv + source .venv/bin/activate + python -m pip install --upgrade pip + python -m pip install 'select_ai==1.5.0' + +On Windows, use the virtual-environment activation command in +:ref:`Installation `. + +Set the following values in your shell. Choose an OpenAI model supported by +your database integration and available to your API project: + +.. code-block:: bash + + export SELECT_AI_USER='your_database_user' + export SELECT_AI_PASSWORD='your_database_password' + export SELECT_AI_DB_CONNECT_STRING='your_database_connect_string' + export OPENAI_API_KEY='your_api_key' + export SELECT_AI_MODEL='gpt-4.1' + +To try a different key, replace ``OPENAI_API_KEY`` and rerun the script. Keep +keys out of source files and version control. OCI-specific environment +variables are not read by this sample. + +For wallet connections, add the parameters described in +:ref:`Wallet connections ` to the +sample's ``connect()`` call. Do not commit secrets to the repository. + +Prepare the database account +============================ + +A database administrator must grant the application user Select AI package +privileges and outbound HTTP access to ``api.openai.com``. Pass ``--setup`` to +perform both steps before running the example. Set the administrator variables +in addition to the application and OpenAI variables above: + +.. code-block:: bash + + export SELECT_AI_ADMIN_USER='your_database_admin_user' + export SELECT_AI_ADMIN_PASSWORD='your_database_admin_password' + python samples/quickstart.py --setup + +The script connects as the administrator, calls ``grant_privileges()`` and +``grant_network_access(users=..., host="api.openai.com", privileges="http")``, +commits the setup, and disconnects. It then connects as the application user +and runs the example. The administrator must be allowed to grant package +privileges and manage network ACLs. Administrator and application connections +use the same DSN; add required wallet arguments to both connection calls. + +Run setup once for each application user/database. For later runs, omit the +flag; administrator credentials are then unnecessary: + +.. code-block:: bash + + python samples/quickstart.py + +The grants remain after the temporary sample objects are removed. See +:ref:`Privileges ` to revoke package or network privileges later. + +The application account must also have ``CREATE SESSION``, ``CREATE TABLE``, +and sufficient tablespace quota to create the temporary table. Have the +administrator assign an appropriate quota in the account's default tablespace. +The helper does not grant these privileges or quota. The sample does not +create a public credential synonym and does not require ``CREATE PUBLIC +SYNONYM``. + +Run the example +=============== + +Save the following as ``quickstart.py``, or run +``python samples/quickstart.py`` from a repository checkout: + +.. literalinclude:: ../../../samples/quickstart.py + :language: python + :start-at: import argparse + +The script prints generated SQL and a DataFrame containing a count of three. +Table names include a generated suffix. SQL formatting and the model's chosen +column label may vary; representative result output is: + +.. code-block:: text + + ITEM_COUNT + 0 3 + +The profile's ``object_list`` includes the sample table using the schema name +resolved from the database connection. Other profile attributes use their +defaults, including object-list enforcement. The object list supplies metadata +for SQL generation; database privileges determine which objects the user can +access. See :ref:`Profile attributes ` for explicit +enforcement settings. + +``show_sql()`` generates SQL without executing it. ``run_sql()`` makes a +separate generation request and executes that response; it does not execute +the SQL string returned by the earlier call. In applications that require +approval of an exact statement, review the generated statement and execute +that approved statement through your database API. + +If connection fails, check the DSN, password, and wallet settings. For package +or network authorization failures, return to the administrator setup. For +model or provider errors, check the API key, model access, API billing, and +project limits. See :ref:`Error handling ` for Python exception +handling. The sample's cleanup removes only objects it successfully created; +administrator grants remain in place for subsequent workflows. + +Waiting and timeouts +==================== + +The script prints progress before connecting, creating objects, and requesting +SQL from OpenAI. If it waits at ``Generating SQL with OpenAI...``, the +database is processing the model request. Missing network ACL privileges usually raise +an authorization error; waiting alone does not prove that the ACL is missing. +Check database egress, provider availability, API billing, and project limits. +HTTP 429 can indicate quota or rate limits; retries may extend the wait. + +The sample sets ``tcp_connect_timeout=15`` for connection establishment and +``connection.call_timeout=120_000`` for database calls. The latter is in +milliseconds and limits each database round trip, rather than total script +runtime. Increase it deliberately for slower requests. See the +`driver timeout documentation +`__. diff --git a/docs/_sources/user_guide/sharing.rst.txt b/docs/_sources/user_guide/sharing.rst.txt new file mode 100644 index 0000000..a041802 --- /dev/null +++ b/docs/_sources/user_guide/sharing.rst.txt @@ -0,0 +1,258 @@ +.. _sharing: + +********************** +Sharing and ownership +********************** + +Share a saved profile, vector index, agent team, or credential with a database +user or role. Run grants and revokes while connected as the object owner. The +recipient also needs the database and package :ref:`Privileges ` +required to use that object. + +The examples below use owner ``APP_OWNER`` and recipient ``APP_USER``. Replace +these names with your schemas. Each example assumes that its named object +already exists. Sharing an object does not grant access to other database +objects or services used by the workflow. + +Connect before running an example +================================= + +For synchronous examples, connect as the object owner: + +.. code-block:: python + + import os + import select_ai + + select_ai.connect( + user=os.environ["SELECT_AI_USER"], # APP_OWNER + password=os.environ["SELECT_AI_PASSWORD"], + dsn=os.environ["SELECT_AI_DB_CONNECT_STRING"], + ) + +Call ``select_ai.disconnect()`` when finished. For asynchronous examples, use +``await select_ai.async_connect(...)`` with the same arguments inside an async +function, and call ``await select_ai.async_disconnect()`` when finished. +The async snippets below belong inside that function. + +.. latex:clearpage:: + +Profiles +======== + +Grant access to an existing profile while connected as ``APP_OWNER``: + +.. code-block:: python + + profile = select_ai.Profile.fetch("OCI_AI_PROFILE") + profile.grant_access("APP_USER") + +In a separate connection as ``APP_USER``, fetch the shared profile by its name +and owner. The ``qualified_name`` property identifies both: + +.. code-block:: python + + profile = select_ai.Profile.fetch("OCI_AI_PROFILE", owner="APP_OWNER") + print(profile.qualified_name) + print(profile.chat(prompt="What is retrieval augmented generation?")) + +The qualified name printed is ``APP_OWNER.OCI_AI_PROFILE``. The chat answer +depends on the configured provider. To withdraw access, reconnect as +``APP_OWNER`` and run: + +.. code-block:: python + + profile = select_ai.Profile.fetch("OCI_AI_PROFILE") + profile.revoke_access("APP_USER") + +The async sequence uses ``AsyncProfile``. Run each step in the indicated +user's connection: + +.. code-block:: python + + # As APP_OWNER: share the profile. + profile = await select_ai.AsyncProfile.fetch("OCI_AI_PROFILE") + await profile.grant_access("APP_USER") + + # In a separate connection as APP_USER: use the shared profile. + shared = await select_ai.AsyncProfile.fetch( + "OCI_AI_PROFILE", owner="APP_OWNER" + ) + print(shared.qualified_name) + print(await shared.chat(prompt="What is retrieval augmented generation?")) + + # Reconnect as APP_OWNER when access should end. + await profile.revoke_access("APP_USER") + +``Profile.list()`` and ``AsyncProfile.list()`` also accept ``owner``. If it is +omitted, they list the current schema. See +:ref:`List profiles ` and +:ref:`List profiles asynchronously ` +for connection setup, filtering, and reuse. + +.. latex:clearpage:: + +Vector indexes +============== + +Share an existing vector index while connected as ``APP_OWNER``: + +.. code-block:: python + + index = select_ai.VectorIndex.fetch("PRODUCT_DOCS") + index.grant_access("APP_USER") + +In a separate connection as ``APP_USER``, address the index by its owner: + +.. code-block:: python + + index = select_ai.VectorIndex.fetch("PRODUCT_DOCS", owner="APP_OWNER") + print(index.qualified_name) # APP_OWNER.PRODUCT_DOCS + +Reconnect as ``APP_OWNER`` to withdraw access: + +.. code-block:: python + + index = select_ai.VectorIndex.fetch("PRODUCT_DOCS") + index.revoke_access("APP_USER") + +Use ``AsyncVectorIndex`` for the same sequence in an async application: + +.. code-block:: python + + # As APP_OWNER. + index = await select_ai.AsyncVectorIndex.fetch("PRODUCT_DOCS") + await index.grant_access("APP_USER") + + # In a separate connection as APP_USER. + shared = await select_ai.AsyncVectorIndex.fetch( + "PRODUCT_DOCS", owner="APP_OWNER" + ) + print(shared.qualified_name) + + # Reconnect as APP_OWNER when access should end. + await index.revoke_access("APP_USER") + +``VectorIndex.list(owner="APP_OWNER")`` lists visible indexes in that schema; +``AsyncVectorIndex.list(owner="APP_OWNER")`` returns an async iterator. +See :ref:`Vector Index ` for creating and using an index for RAG. + +.. latex:clearpage:: + +Agent teams +=========== + +While connected as the team owner, grant access to an existing team: + +.. code-block:: python + + from select_ai.agent import Team + + team = Team.fetch("MOVIE_AGENT_TEAM") + team.grant_access("APP_USER") + +When the recipient no longer needs access, run this as the team owner: + +.. code-block:: python + + team.revoke_access("APP_USER") + +For async applications, use ``AsyncTeam`` and await each operation: + +.. code-block:: python + + from select_ai.agent import AsyncTeam + + team = await AsyncTeam.fetch("MOVIE_AGENT_TEAM") + await team.grant_access("APP_USER") + + # Revoke later, while connected as the team owner. + await team.revoke_access("APP_USER") + +Team fetch and list operations use the connected user's agent-team views and +do not accept an ``owner`` argument. See :ref:`AI Agent ` for team +creation and execution. + +.. latex:clearpage:: + +Credentials +=========== + +Credentials use module-level access functions. While connected as the +credential owner, share an existing provider credential: + +.. code-block:: python + + select_ai.grant_credential_access("MY_PROVIDER_CREDENTIAL", "APP_USER") + +Revoke access when the recipient no longer needs it: + +.. code-block:: python + + select_ai.revoke_credential_access("MY_PROVIDER_CREDENTIAL", "APP_USER") + +The async functions take the same arguments: + +.. code-block:: python + + await select_ai.async_grant_credential_access( + "MY_PROVIDER_CREDENTIAL", "APP_USER" + ) + + # Revoke later, while connected as the credential owner. + await select_ai.async_revoke_credential_access( + "MY_PROVIDER_CREDENTIAL", "APP_USER" + ) + +Access functions return ``None`` after the grant or revoke completes. They do +not print a confirmation message. + +Credential creation and deletion can also manage a public synonym with the +same name. Pass ``public_synonym=True`` to ``create_credential()`` or +``delete_credential()`` (or their async equivalents). Creating or dropping the +synonym requires ``CREATE PUBLIC SYNONYM`` or ``DROP PUBLIC SYNONYM`` +respectively. See :ref:`Credential ` for credential setup. + +Runnable sharing scripts +========================= + +The repository also provides a +`synchronous sharing script `__ +and an +`asynchronous sharing script `__ +that exercise all four object types. Use these when you want to check the APIs +together after working through the individual examples above. + +Set the common database connection variables and ``SELECT_AI_SHARE_GRANTEE``. +The connected user must own all four objects. The scripts use these defaults; +override them if your objects have other names: + +.. list-table:: Sharing script configuration + :header-rows: 1 + :widths: 48 52 + + * - Environment variable + - Default + * - ``SELECT_AI_PROFILE_NAME`` + - ``oci_ai_profile`` (sync); ``async_oci_ai_profile`` (async) + * - ``SELECT_AI_VECTOR_INDEX_NAME`` + - ``test_vector_index`` + * - ``SELECT_AI_TEAM_NAME`` + - ``MOVIE_AGENT_TEAM`` + * - ``SELECT_AI_CREDENTIAL_NAME`` + - ``my_oci_ai_profile_key`` + * - ``SELECT_AI_OWNER`` + - Current schema; if set, use the connected owner's schema. + +Run either script from the repository root: + +.. code-block:: bash + + export SELECT_AI_SHARE_GRANTEE=APP_USER + python samples/sharing.py + # Or run the async version: + python samples/async/sharing.py + +Each script prints the owner-qualified profile and vector-index names, then +confirms each grant and revoke. The scripts revoke every grant they make; +use the individual examples above to leave an object shared for application use. diff --git a/docs/_sources/user_guide/synthetic_data.rst.txt b/docs/_sources/user_guide/synthetic_data.rst.txt index c607360..d6b6caa 100644 --- a/docs/_sources/user_guide/synthetic_data.rst.txt +++ b/docs/_sources/user_guide/synthetic_data.rst.txt @@ -72,7 +72,9 @@ Use ``SyntheticDataParams`` to control how generation is performed: ``sample_rows`` controls how many existing rows are used as examples for the model. ``table_statistics`` and ``comments`` include additional table metadata. ``priority`` controls resource priority for generation work; supported values -are ``HIGH``, ``MEDIUM``, and ``LOW``. +are ``HIGH``, ``MEDIUM``, and ``LOW``. All parameters are optional. Parameters +that are not supplied are omitted from the request, allowing the database to +apply its defaults. Sync and async APIs =================== @@ -144,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/docs/_sources/user_guide/vector_index.rst.txt b/docs/_sources/user_guide/vector_index.rst.txt index f90e4d4..9e689ec 100644 --- a/docs/_sources/user_guide/vector_index.rst.txt +++ b/docs/_sources/user_guide/vector_index.rst.txt @@ -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()`` @@ -185,6 +196,35 @@ objects used to create embeddings reside in OCI Object Storage. The profile uses an OCI Generative AI provider with an embedding model, and the vector index is linked to that profile during ``create()``. +Upload ``samples/data/refund-policy.txt`` from the repository to a bucket you +can read. The document says customers have 30 calendar days to request a +refund. Configure the sample with the URI of that uploaded file and the name +of an existing database credential authorized to read it: + +.. code-block:: bash + + export SELECT_AI_DOCUMENT_URI='https://objectstorage.REGION.oraclecloud.com/n/NAMESPACE/b/BUCKET/o/refund-policy.txt' + export SELECT_AI_OBJECT_STORAGE_CREDENTIAL='OBJECT_STORE_CRED' + export SELECT_AI_PROVIDER_CREDENTIAL='my_oci_ai_profile_key' + +Replace ``REGION``, ``NAMESPACE``, and ``BUCKET`` with your storage values. +Use the common database connection variables from :ref:`Connection `. +The provider credential and storage credential can be the same credential +when it has both permissions. Optionally set ``SELECT_AI_OCI_REGION`` and +``SELECT_AI_EMBEDDING_MODEL`` for your OCI configuration. The default region +is ``us-chicago-1`` and embedding model is ``cohere.embed-v4.0``; choose a model +available in your account and region. + +The creation samples replace ``test_vector_index`` and their named RAG +profile. Use a development schema, or edit these names before running if you +need to preserve existing definitions. The samples close their database +connections but leave the profile and index available for the next step. + +From the repository root, run ``python samples/vector_index_create.py``. +For async, use ``python samples/async/vector_index_create.py``. Both examples +wait for the initial load to complete. Run either workflow in its own schema, +or delete the existing sample index before switching between them. + .. literalinclude:: ../../../samples/vector_index_create.py :language: python :lines: 14- @@ -202,10 +242,8 @@ List vector index :language: python :lines: 15- -output:: - - Vector index TEST_VECTOR_INDEX - Vector index profile Profile(profile_name=oci_vector_ai_profile, attributes=ProfileAttributes(annotations=None, case_sensitive_values=None, comments=None, constraints=None, conversation=None, credential_name='my_oci_ai_profile_key', enable_sources=None, enable_source_offsets=None, enforce_object_list=None, max_tokens=1024, object_list=None, object_list_mode=None, provider=OCIGenAIProvider(embedding_model=None, model=None, provider_name='oci', provider_endpoint=None, region='us-chicago-1', oci_apiformat='GENERIC', oci_compartment_id=None, oci_endpoint_id=None, oci_runtimetype=None), seed=None, stop_tokens=None, streaming=None, temperature=None, vector_index_name='test_vector_index'), description=None) +The script prints each matching index name and its linked profile. Results +depend on the indexes visible to the connected user. .. latex:clearpage:: @@ -214,22 +252,31 @@ 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 :lines: 14- -output:: - - OracleVectorIndexAttributes(chunk_size=1024, chunk_overlap=128, location='https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph', match_limit=5, object_storage_credential_name='my_oci_ai_profile_key', profile_name='oci_vector_ai_profile', refresh_rate=1450, similarity_threshold=0.5, vector_distance_metric='COSINE', vector_db_endpoint=None, vector_db_credential_name=None, vector_db_provider=, vector_dimension=None, vector_table_name=None, pipeline_name='TEST_VECTOR_INDEX$VECPIPELINE') - - Profile(profile_name=oci_vector_ai_profile, attributes=ProfileAttributes(annotations=None, case_sensitive_values=None, comments=None, constraints=None, conversation=None, credential_name='my_oci_ai_profile_key', enable_custom_source_uri=None, enable_sources=None, enable_source_offsets=None, enforce_object_list=None, max_tokens=1024, object_list=None, object_list_mode=None, provider=OCIGenAIProvider(embedding_model='cohere.embed-english-v3.0', model=None, provider_name='oci', provider_endpoint=None, region='us-chicago-1', oci_apiformat='GENERIC', oci_compartment_id=None, oci_endpoint_id=None, oci_runtimetype=None), seed=None, stop_tokens=None, streaming=None, temperature=None, vector_index_name='test_vector_index'), description=MY OCI AI Profile) +The script prints the saved index attributes, its linked profile, and the next +refresh timestamp. The URI and credentials reflect your configuration; the +next refresh can be ``None`` when no refresh execution has been recorded. .. latex:clearpage:: + Update vector index attributes ++++++++++++++++++++++++++++++ @@ -241,12 +288,12 @@ and ``set_attributes()`` when updating several values together. :language: python :lines: 14- -output:: - - OracleVectorIndexAttributes(chunk_size=1024, chunk_overlap=128, location='https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph', match_limit=5, object_storage_credential_name='my_oci_ai_profile_key', profile_name='oci_vector_ai_profile', refresh_rate=1450, similarity_threshold=0.5, vector_distance_metric='COSINE', vector_db_endpoint=None, vector_db_credential_name=None, vector_db_provider=, vector_dimension=None, vector_table_name=None, pipeline_name='TEST_VECTOR_INDEX$VECPIPELINE') +The script sets ``refresh_rate`` to 1450 minutes and ``similarity_threshold`` +to 0.5, then prints the attributes held by the Python object. .. latex:clearpage:: + RAG using vector index ++++++++++++++++++++++ @@ -259,24 +306,23 @@ answer in the indexed content. :language: python :lines: 14- -output:: +The answer should identify the **30 calendar day** refund window. Exact +wording and source formatting depend on the provider and profile settings. +For example: - The conda environments in your object store are: - 1. fccenv - 2. myrenv - 3. fully-loaded-mlenv - 4. graphenv +output:: - These environments are listed in the provided data as separate JSON documents, each containing information about a specific conda environment. + Customers can request a refund within 30 calendar days of purchase. - Sources: - - fccenv-manifest.json (https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph/fccenv-manifest.json) - - myrenv-manifest.json (https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph/myrenv-manifest.json) - - fully-loaded-mlenv-manifest.json (https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph/fully-loaded-mlenv-manifest.json) - - graphenv-manifest.json (https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph/graphenv-manifest.json) +If no answer is grounded in the policy, verify that the uploaded URI and +storage credential are correct, that the initial load completed, and that +this profile is linked to ``test_vector_index``. Run the corresponding RAG +script after creating the index. See `Delete vector index`_ to remove the +sample index and its data; delete the sample profile separately when finished. .. latex:clearpage:: + Delete vector index +++++++++++++++++++ @@ -338,7 +384,7 @@ Async create vector index output:: - created vector index: test_vector_index + Created vector index: test_vector_index .. latex:clearpage:: @@ -350,11 +396,8 @@ Async list vector index :language: python :lines: 15- -output:: - - Vector index TEST_VECTOR_INDEX - Vector index profile AsyncProfile(profile_name=oci_vector_ai_profile, attributes=ProfileAttributes(annotations=None, case_sensitive_values=None, comments=None, constraints=None, conversation=None, credential_name='my_oci_ai_profile_key', enable_sources=None, enable_source_offsets=None, enforce_object_list=None, max_tokens=1024, object_list=None, object_list_mode=None, provider=OCIGenAIProvider(embedding_model=None, model=None, provider_name='oci', provider_endpoint=None, region='us-chicago-1', oci_apiformat='GENERIC', oci_compartment_id=None, oci_endpoint_id=None, oci_runtimetype=None), seed=None, stop_tokens=None, streaming=None, temperature=None, vector_index_name='test_vector_index'), description=None) - +The script prints matching index names and their linked profiles. Results +depend on your schema and configuration. .. latex:clearpage:: @@ -369,14 +412,12 @@ the class method ``AsyncVectorIndex.fetch(index_name)`` :language: python :lines: 14- -output:: - - OracleVectorIndexAttributes(chunk_size=1024, chunk_overlap=128, location='https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph', match_limit=5, object_storage_credential_name='my_oci_ai_profile_key', profile_name='oci_vector_ai_profile', refresh_rate=1450, similarity_threshold=0.5, vector_distance_metric='COSINE', vector_db_endpoint=None, vector_db_credential_name=None, vector_db_provider=, vector_dimension=None, vector_table_name=None, pipeline_name='TEST_VECTOR_INDEX$VECPIPELINE') - - AsyncProfile(profile_name=oci_vector_ai_profile, attributes=ProfileAttributes(annotations=None, case_sensitive_values=None, comments=None, constraints=None, conversation=None, credential_name='my_oci_ai_profile_key', enable_custom_source_uri=None, enable_sources=None, enable_source_offsets=None, enforce_object_list=None, max_tokens=1024, object_list=None, object_list_mode=None, provider=OCIGenAIProvider(embedding_model='cohere.embed-english-v3.0', model=None, provider_name='oci', provider_endpoint=None, region='us-chicago-1', oci_apiformat='GENERIC', oci_compartment_id=None, oci_endpoint_id=None, oci_runtimetype=None), seed=None, stop_tokens=None, streaming=None, temperature=None, vector_index_name='test_vector_index'), description=MY OCI AI Profile) +The script prints the saved attributes and linked profile. Source URIs, +credentials, and other values depend on the index you created. .. latex:clearpage:: + Async update vector index attributes ++++++++++++++++++++++++++++++++++++ @@ -387,12 +428,12 @@ To update attributes, use either ``async_vector_index.set_attribute()`` or :language: python :lines: 14- -output:: - - OracleVectorIndexAttributes(chunk_size=1024, chunk_overlap=128, location='https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph', match_limit=5, object_storage_credential_name='my_oci_ai_profile_key', profile_name='oci_vector_ai_profile', refresh_rate=1450, similarity_threshold=0.5, vector_distance_metric='COSINE', vector_db_endpoint=None, vector_db_credential_name=None, vector_db_provider=, vector_dimension=None, vector_table_name=None, pipeline_name='TEST_VECTOR_INDEX$VECPIPELINE') +The script updates the refresh rate and similarity threshold, then prints +the attributes held by the Python object. .. latex:clearpage:: + Async RAG using vector index ++++++++++++++++++++++++++++ @@ -400,18 +441,48 @@ Async RAG using vector index :language: python :lines: 15- +The answer should identify the **30 calendar day** refund window. Exact +wording and source formatting depend on the provider and profile settings. +For example: + output:: - The conda environments in your object store are: - 1. fccenv - 2. myrenv - 3. fully-loaded-mlenv - 4. graphenv + Customers can request a refund within 30 calendar days of purchase. + +If no answer is grounded in the policy, verify that the uploaded URI and +storage credential are correct, that the initial load completed, and that +this profile is linked to ``test_vector_index``. Run the corresponding RAG +script after creating the index. See `Delete vector index`_ to remove the +sample index and its data; delete the sample profile separately when finished. + +.. latex:clearpage:: + + +Share a vector index +==================== + +While connected as the index owner, grant access to an existing index: + +.. code-block:: python + + index = select_ai.VectorIndex.fetch("PRODUCT_DOCS") + index.grant_access("APP_USER") + +In a separate connection as the recipient, use +``VectorIndex.fetch("PRODUCT_DOCS", owner="APP_OWNER")``. It returns an object +whose ``qualified_name`` is ``APP_OWNER.PRODUCT_DOCS``. When access should end, +run ``index.revoke_access("APP_USER")`` as the owner. + +For an async application, use ``AsyncVectorIndex`` and await each operation: + +.. code-block:: python + + # Run as the index owner. + index = await select_ai.AsyncVectorIndex.fetch("PRODUCT_DOCS") + await index.grant_access("APP_USER") - These environments are listed in the provided data as separate JSON documents, each containing information about a specific conda environment. + # Run later as the index owner, when access should end. + await index.revoke_access("APP_USER") - Sources: - - fccenv-manifest.json (https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph/fccenv-manifest.json) - - myrenv-manifest.json (https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph/myrenv-manifest.json) - - fully-loaded-mlenv-manifest.json (https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph/fully-loaded-mlenv-manifest.json) - - graphenv-manifest.json (https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph/graphenv-manifest.json) +See :ref:`Sharing and ownership ` for the recipient's async fetch +example and connection setup. diff --git a/docs/_sources/user_guide/web_frameworks.rst.txt b/docs/_sources/user_guide/web_frameworks.rst.txt index 42baefb..73a0051 100644 --- a/docs/_sources/user_guide/web_frameworks.rst.txt +++ b/docs/_sources/user_guide/web_frameworks.rst.txt @@ -95,6 +95,8 @@ For production deployments, store these values in your deployment platform's secret manager or environment configuration. Do not hard-code database passwords, wallet passwords, or provider credentials in application source. +.. latex:clearpage:: + FastAPI synchronous endpoints ============================= @@ -162,9 +164,14 @@ Stop the server by pressing ``Ctrl+C`` in the terminal where ``uvicorn`` is running. FastAPI runs the lifespan shutdown hook and ``select_ai.disconnect()`` closes the pool. -This example creates the ``Profile`` proxy inside each handler. The proxy is -lightweight; the database connection is acquired from the pool only when the -profile method calls the database. +This example constructs ``Profile`` inside each handler. Construction reads +the saved profile description and attributes from the database, acquiring +connections from the pool before the action itself runs. This keeps metadata +current but adds database calls to each request. Reusing an initialized profile +avoids those reads; refresh it when saved profile settings change, and avoid +mutating a shared profile object while requests are using it. + +.. latex:clearpage:: FastAPI asynchronous endpoints ============================== @@ -219,6 +226,8 @@ Start and stop the server the same way: Press ``Ctrl+C`` to stop it. +.. latex:clearpage:: + Flask example ============= @@ -260,6 +269,8 @@ synchronous Select AI pool when the application is created. return app +.. latex:clearpage:: + Django example ============== @@ -291,6 +302,10 @@ pool once per process, then call ``Profile`` APIs inside views. increment=int(os.getenv("SELECT_AI_POOL_INCREMENT", "1")), ) +.. latex:clearpage:: + +Add the view in ``myapp/views.py``: + .. code-block:: python # myapp/views.py @@ -341,6 +356,8 @@ control what happens when all pooled connections are busy. .. code-block:: python + import oracledb + select_ai.create_pool( user=user, password=password, @@ -348,9 +365,17 @@ control what happens when all pooled connections are busy. min_size=2, max_size=8, increment=2, - wait_timeout=10, + getmode=oracledb.POOL_GETMODE_TIMEDWAIT, + wait_timeout=10_000, ) +``wait_timeout`` is in milliseconds and takes effect only with +``getmode=oracledb.POOL_GETMODE_TIMEDWAIT``. The example waits up to ten seconds +for a connection. The default ``POOL_GETMODE_WAIT`` waits without this limit. +Pool acquisition timeouts do not limit SQL execution or model generation time. +See the `python-oracledb pool parameters +`__. + Choose a timeout that matches your API latency budget. For public HTTP APIs, it is usually better to fail fast and return an application error than to let requests pile up until every worker is blocked. diff --git a/docs/_static/base-stemmer.js b/docs/_static/base-stemmer.js new file mode 100644 index 0000000..e6fa0c4 --- /dev/null +++ b/docs/_static/base-stemmer.js @@ -0,0 +1,476 @@ +// @ts-check + +/**@constructor*/ +BaseStemmer = function() { + /** @protected */ + this.current = ''; + this.cursor = 0; + this.limit = 0; + this.limit_backward = 0; + this.bra = 0; + this.ket = 0; + + /** + * @param {string} value + */ + this.setCurrent = function(value) { + this.current = value; + this.cursor = 0; + this.limit = this.current.length; + this.limit_backward = 0; + this.bra = this.cursor; + this.ket = this.limit; + }; + + /** + * @return {string} + */ + this.getCurrent = function() { + return this.current; + }; + + /** + * @param {BaseStemmer} other + */ + this.copy_from = function(other) { + /** @protected */ + this.current = other.current; + this.cursor = other.cursor; + this.limit = other.limit; + this.limit_backward = other.limit_backward; + this.bra = other.bra; + this.ket = other.ket; + }; + + /** + * @param {number[]} s + * @param {number} min + * @param {number} max + * @return {boolean} + */ + this.in_grouping = function(s, min, max) { + /** @protected */ + if (this.cursor >= this.limit) return false; + var ch = this.current.charCodeAt(this.cursor); + if (ch > max || ch < min) return false; + ch -= min; + if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0) return false; + this.cursor++; + return true; + }; + + /** + * @param {number[]} s + * @param {number} min + * @param {number} max + * @return {boolean} + */ + this.go_in_grouping = function(s, min, max) { + /** @protected */ + while (this.cursor < this.limit) { + var ch = this.current.charCodeAt(this.cursor); + if (ch > max || ch < min) + return true; + ch -= min; + if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0) + return true; + this.cursor++; + } + return false; + }; + + /** + * @param {number[]} s + * @param {number} min + * @param {number} max + * @return {boolean} + */ + this.in_grouping_b = function(s, min, max) { + /** @protected */ + if (this.cursor <= this.limit_backward) return false; + var ch = this.current.charCodeAt(this.cursor - 1); + if (ch > max || ch < min) return false; + ch -= min; + if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0) return false; + this.cursor--; + return true; + }; + + /** + * @param {number[]} s + * @param {number} min + * @param {number} max + * @return {boolean} + */ + this.go_in_grouping_b = function(s, min, max) { + /** @protected */ + while (this.cursor > this.limit_backward) { + var ch = this.current.charCodeAt(this.cursor - 1); + if (ch > max || ch < min) return true; + ch -= min; + if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0) return true; + this.cursor--; + } + return false; + }; + + /** + * @param {number[]} s + * @param {number} min + * @param {number} max + * @return {boolean} + */ + this.out_grouping = function(s, min, max) { + /** @protected */ + if (this.cursor >= this.limit) return false; + var ch = this.current.charCodeAt(this.cursor); + if (ch > max || ch < min) { + this.cursor++; + return true; + } + ch -= min; + if ((s[ch >>> 3] & (0X1 << (ch & 0x7))) == 0) { + this.cursor++; + return true; + } + return false; + }; + + /** + * @param {number[]} s + * @param {number} min + * @param {number} max + * @return {boolean} + */ + this.go_out_grouping = function(s, min, max) { + /** @protected */ + while (this.cursor < this.limit) { + var ch = this.current.charCodeAt(this.cursor); + if (ch <= max && ch >= min) { + ch -= min; + if ((s[ch >>> 3] & (0X1 << (ch & 0x7))) != 0) { + return true; + } + } + this.cursor++; + } + return false; + }; + + /** + * @param {number[]} s + * @param {number} min + * @param {number} max + * @return {boolean} + */ + this.out_grouping_b = function(s, min, max) { + /** @protected */ + if (this.cursor <= this.limit_backward) return false; + var ch = this.current.charCodeAt(this.cursor - 1); + if (ch > max || ch < min) { + this.cursor--; + return true; + } + ch -= min; + if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0) { + this.cursor--; + return true; + } + return false; + }; + + /** + * @param {number[]} s + * @param {number} min + * @param {number} max + * @return {boolean} + */ + this.go_out_grouping_b = function(s, min, max) { + /** @protected */ + while (this.cursor > this.limit_backward) { + var ch = this.current.charCodeAt(this.cursor - 1); + if (ch <= max && ch >= min) { + ch -= min; + if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) != 0) { + return true; + } + } + this.cursor--; + } + return false; + }; + + /** + * @param {string} s + * @return {boolean} + */ + this.eq_s = function(s) + { + /** @protected */ + if (this.limit - this.cursor < s.length) return false; + if (this.current.slice(this.cursor, this.cursor + s.length) != s) + { + return false; + } + this.cursor += s.length; + return true; + }; + + /** + * @param {string} s + * @return {boolean} + */ + this.eq_s_b = function(s) + { + /** @protected */ + if (this.cursor - this.limit_backward < s.length) return false; + if (this.current.slice(this.cursor - s.length, this.cursor) != s) + { + return false; + } + this.cursor -= s.length; + return true; + }; + + /** + * @param {Among[]} v + * @return {number} + */ + this.find_among = function(v) + { + /** @protected */ + var i = 0; + var j = v.length; + + var c = this.cursor; + var l = this.limit; + + var common_i = 0; + var common_j = 0; + + var first_key_inspected = false; + + while (true) + { + var k = i + ((j - i) >>> 1); + var diff = 0; + var common = common_i < common_j ? common_i : common_j; // smaller + // w[0]: string, w[1]: substring_i, w[2]: result, w[3]: function (optional) + var w = v[k]; + var i2; + for (i2 = common; i2 < w[0].length; i2++) + { + if (c + common == l) + { + diff = -1; + break; + } + diff = this.current.charCodeAt(c + common) - w[0].charCodeAt(i2); + if (diff != 0) break; + common++; + } + if (diff < 0) + { + j = k; + common_j = common; + } + else + { + i = k; + common_i = common; + } + if (j - i <= 1) + { + if (i > 0) break; // v->s has been inspected + if (j == i) break; // only one item in v + + // - but now we need to go round once more to get + // v->s inspected. This looks messy, but is actually + // the optimal approach. + + if (first_key_inspected) break; + first_key_inspected = true; + } + } + do { + var w = v[i]; + if (common_i >= w[0].length) + { + this.cursor = c + w[0].length; + if (w.length < 4) return w[2]; + var res = w[3](this); + this.cursor = c + w[0].length; + if (res) return w[2]; + } + i = w[1]; + } while (i >= 0); + return 0; + }; + + // find_among_b is for backwards processing. Same comments apply + /** + * @param {Among[]} v + * @return {number} + */ + this.find_among_b = function(v) + { + /** @protected */ + var i = 0; + var j = v.length + + var c = this.cursor; + var lb = this.limit_backward; + + var common_i = 0; + var common_j = 0; + + var first_key_inspected = false; + + while (true) + { + var k = i + ((j - i) >> 1); + var diff = 0; + var common = common_i < common_j ? common_i : common_j; + var w = v[k]; + var i2; + for (i2 = w[0].length - 1 - common; i2 >= 0; i2--) + { + if (c - common == lb) + { + diff = -1; + break; + } + diff = this.current.charCodeAt(c - 1 - common) - w[0].charCodeAt(i2); + if (diff != 0) break; + common++; + } + if (diff < 0) + { + j = k; + common_j = common; + } + else + { + i = k; + common_i = common; + } + if (j - i <= 1) + { + if (i > 0) break; + if (j == i) break; + if (first_key_inspected) break; + first_key_inspected = true; + } + } + do { + var w = v[i]; + if (common_i >= w[0].length) + { + this.cursor = c - w[0].length; + if (w.length < 4) return w[2]; + var res = w[3](this); + this.cursor = c - w[0].length; + if (res) return w[2]; + } + i = w[1]; + } while (i >= 0); + return 0; + }; + + /* to replace chars between c_bra and c_ket in this.current by the + * chars in s. + */ + /** + * @param {number} c_bra + * @param {number} c_ket + * @param {string} s + * @return {number} + */ + this.replace_s = function(c_bra, c_ket, s) + { + /** @protected */ + var adjustment = s.length - (c_ket - c_bra); + this.current = this.current.slice(0, c_bra) + s + this.current.slice(c_ket); + this.limit += adjustment; + if (this.cursor >= c_ket) this.cursor += adjustment; + else if (this.cursor > c_bra) this.cursor = c_bra; + return adjustment; + }; + + /** + * @return {boolean} + */ + this.slice_check = function() + { + /** @protected */ + if (this.bra < 0 || + this.bra > this.ket || + this.ket > this.limit || + this.limit > this.current.length) + { + return false; + } + return true; + }; + + /** + * @param {number} c_bra + * @return {boolean} + */ + this.slice_from = function(s) + { + /** @protected */ + var result = false; + if (this.slice_check()) + { + this.replace_s(this.bra, this.ket, s); + result = true; + } + return result; + }; + + /** + * @return {boolean} + */ + this.slice_del = function() + { + /** @protected */ + return this.slice_from(""); + }; + + /** + * @param {number} c_bra + * @param {number} c_ket + * @param {string} s + */ + this.insert = function(c_bra, c_ket, s) + { + /** @protected */ + var adjustment = this.replace_s(c_bra, c_ket, s); + if (c_bra <= this.bra) this.bra += adjustment; + if (c_bra <= this.ket) this.ket += adjustment; + }; + + /** + * @return {string} + */ + this.slice_to = function() + { + /** @protected */ + var result = ''; + if (this.slice_check()) + { + result = this.current.slice(this.bra, this.ket); + } + return result; + }; + + /** + * @return {string} + */ + this.assign_to = function() + { + /** @protected */ + return this.current.slice(0, this.limit); + }; +}; diff --git a/docs/_static/doctools.js b/docs/_static/doctools.js index 0398ebb..807cdb1 100644 --- a/docs/_static/doctools.js +++ b/docs/_static/doctools.js @@ -59,7 +59,7 @@ const Documentation = { Object.assign(Documentation.TRANSLATIONS, catalog.messages); Documentation.PLURAL_EXPR = new Function( "n", - `return (${catalog.plural_expr})` + `return (${catalog.plural_expr})`, ); Documentation.LOCALE = catalog.locale; }, @@ -89,7 +89,7 @@ const Documentation = { const togglerElements = document.querySelectorAll("img.toggler"); togglerElements.forEach((el) => - el.addEventListener("click", (event) => toggler(event.currentTarget)) + el.addEventListener("click", (event) => toggler(event.currentTarget)), ); togglerElements.forEach((el) => (el.style.display = "")); if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); @@ -98,14 +98,15 @@ const Documentation = { initOnKeyListeners: () => { // only install a listener if it is really needed if ( - !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && - !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS + && !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS ) return; document.addEventListener("keydown", (event) => { // bail for input elements - if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) + return; // bail with special keys if (event.altKey || event.ctrlKey || event.metaKey) return; diff --git a/docs/_static/documentation_options.js b/docs/_static/documentation_options.js index 45421df..e2c9d2f 100644 --- a/docs/_static/documentation_options.js +++ b/docs/_static/documentation_options.js @@ -1,5 +1,5 @@ const DOCUMENTATION_OPTIONS = { - VERSION: '1.4.0', + VERSION: '1.5.0', LANGUAGE: 'en', COLLAPSE_INDEX: false, BUILDER: 'html', diff --git a/docs/_static/english-stemmer.js b/docs/_static/english-stemmer.js new file mode 100644 index 0000000..056760e --- /dev/null +++ b/docs/_static/english-stemmer.js @@ -0,0 +1,1066 @@ +// Generated from english.sbl by Snowball 3.0.1 - https://snowballstem.org/ + +/**@constructor*/ +var EnglishStemmer = function() { + var base = new BaseStemmer(); + + /** @const */ var a_0 = [ + ["arsen", -1, -1], + ["commun", -1, -1], + ["emerg", -1, -1], + ["gener", -1, -1], + ["later", -1, -1], + ["organ", -1, -1], + ["past", -1, -1], + ["univers", -1, -1] + ]; + + /** @const */ var a_1 = [ + ["'", -1, 1], + ["'s'", 0, 1], + ["'s", -1, 1] + ]; + + /** @const */ var a_2 = [ + ["ied", -1, 2], + ["s", -1, 3], + ["ies", 1, 2], + ["sses", 1, 1], + ["ss", 1, -1], + ["us", 1, -1] + ]; + + /** @const */ var a_3 = [ + ["succ", -1, 1], + ["proc", -1, 1], + ["exc", -1, 1] + ]; + + /** @const */ var a_4 = [ + ["even", -1, 2], + ["cann", -1, 2], + ["inn", -1, 2], + ["earr", -1, 2], + ["herr", -1, 2], + ["out", -1, 2], + ["y", -1, 1] + ]; + + /** @const */ var a_5 = [ + ["", -1, -1], + ["ed", 0, 2], + ["eed", 1, 1], + ["ing", 0, 3], + ["edly", 0, 2], + ["eedly", 4, 1], + ["ingly", 0, 2] + ]; + + /** @const */ var a_6 = [ + ["", -1, 3], + ["bb", 0, 2], + ["dd", 0, 2], + ["ff", 0, 2], + ["gg", 0, 2], + ["bl", 0, 1], + ["mm", 0, 2], + ["nn", 0, 2], + ["pp", 0, 2], + ["rr", 0, 2], + ["at", 0, 1], + ["tt", 0, 2], + ["iz", 0, 1] + ]; + + /** @const */ var a_7 = [ + ["anci", -1, 3], + ["enci", -1, 2], + ["ogi", -1, 14], + ["li", -1, 16], + ["bli", 3, 12], + ["abli", 4, 4], + ["alli", 3, 8], + ["fulli", 3, 9], + ["lessli", 3, 15], + ["ousli", 3, 10], + ["entli", 3, 5], + ["aliti", -1, 8], + ["biliti", -1, 12], + ["iviti", -1, 11], + ["tional", -1, 1], + ["ational", 14, 7], + ["alism", -1, 8], + ["ation", -1, 7], + ["ization", 17, 6], + ["izer", -1, 6], + ["ator", -1, 7], + ["iveness", -1, 11], + ["fulness", -1, 9], + ["ousness", -1, 10], + ["ogist", -1, 13] + ]; + + /** @const */ var a_8 = [ + ["icate", -1, 4], + ["ative", -1, 6], + ["alize", -1, 3], + ["iciti", -1, 4], + ["ical", -1, 4], + ["tional", -1, 1], + ["ational", 5, 2], + ["ful", -1, 5], + ["ness", -1, 5] + ]; + + /** @const */ var a_9 = [ + ["ic", -1, 1], + ["ance", -1, 1], + ["ence", -1, 1], + ["able", -1, 1], + ["ible", -1, 1], + ["ate", -1, 1], + ["ive", -1, 1], + ["ize", -1, 1], + ["iti", -1, 1], + ["al", -1, 1], + ["ism", -1, 1], + ["ion", -1, 2], + ["er", -1, 1], + ["ous", -1, 1], + ["ant", -1, 1], + ["ent", -1, 1], + ["ment", 15, 1], + ["ement", 16, 1] + ]; + + /** @const */ var a_10 = [ + ["e", -1, 1], + ["l", -1, 2] + ]; + + /** @const */ var a_11 = [ + ["andes", -1, -1], + ["atlas", -1, -1], + ["bias", -1, -1], + ["cosmos", -1, -1], + ["early", -1, 5], + ["gently", -1, 3], + ["howe", -1, -1], + ["idly", -1, 2], + ["news", -1, -1], + ["only", -1, 6], + ["singly", -1, 7], + ["skies", -1, 1], + ["sky", -1, -1], + ["ugly", -1, 4] + ]; + + /** @const */ var /** Array */ g_aeo = [17, 64]; + + /** @const */ var /** Array */ g_v = [17, 65, 16, 1]; + + /** @const */ var /** Array */ g_v_WXY = [1, 17, 65, 208, 1]; + + /** @const */ var /** Array */ g_valid_LI = [55, 141, 2]; + + var /** boolean */ B_Y_found = false; + var /** number */ I_p2 = 0; + var /** number */ I_p1 = 0; + + + /** @return {boolean} */ + function r_prelude() { + B_Y_found = false; + /** @const */ var /** number */ v_1 = base.cursor; + lab0: { + base.bra = base.cursor; + if (!(base.eq_s("'"))) + { + break lab0; + } + base.ket = base.cursor; + if (!base.slice_del()) + { + return false; + } + } + base.cursor = v_1; + /** @const */ var /** number */ v_2 = base.cursor; + lab1: { + base.bra = base.cursor; + if (!(base.eq_s("y"))) + { + break lab1; + } + base.ket = base.cursor; + if (!base.slice_from("Y")) + { + return false; + } + B_Y_found = true; + } + base.cursor = v_2; + /** @const */ var /** number */ v_3 = base.cursor; + lab2: { + while(true) + { + /** @const */ var /** number */ v_4 = base.cursor; + lab3: { + golab4: while(true) + { + /** @const */ var /** number */ v_5 = base.cursor; + lab5: { + if (!(base.in_grouping(g_v, 97, 121))) + { + break lab5; + } + base.bra = base.cursor; + if (!(base.eq_s("y"))) + { + break lab5; + } + base.ket = base.cursor; + base.cursor = v_5; + break golab4; + } + base.cursor = v_5; + if (base.cursor >= base.limit) + { + break lab3; + } + base.cursor++; + } + if (!base.slice_from("Y")) + { + return false; + } + B_Y_found = true; + continue; + } + base.cursor = v_4; + break; + } + } + base.cursor = v_3; + return true; + }; + + /** @return {boolean} */ + function r_mark_regions() { + I_p1 = base.limit; + I_p2 = base.limit; + /** @const */ var /** number */ v_1 = base.cursor; + lab0: { + lab1: { + /** @const */ var /** number */ v_2 = base.cursor; + lab2: { + if (base.find_among(a_0) == 0) + { + break lab2; + } + break lab1; + } + base.cursor = v_2; + if (!base.go_out_grouping(g_v, 97, 121)) + { + break lab0; + } + base.cursor++; + if (!base.go_in_grouping(g_v, 97, 121)) + { + break lab0; + } + base.cursor++; + } + I_p1 = base.cursor; + if (!base.go_out_grouping(g_v, 97, 121)) + { + break lab0; + } + base.cursor++; + if (!base.go_in_grouping(g_v, 97, 121)) + { + break lab0; + } + base.cursor++; + I_p2 = base.cursor; + } + base.cursor = v_1; + return true; + }; + + /** @return {boolean} */ + function r_shortv() { + lab0: { + /** @const */ var /** number */ v_1 = base.limit - base.cursor; + lab1: { + if (!(base.out_grouping_b(g_v_WXY, 89, 121))) + { + break lab1; + } + if (!(base.in_grouping_b(g_v, 97, 121))) + { + break lab1; + } + if (!(base.out_grouping_b(g_v, 97, 121))) + { + break lab1; + } + break lab0; + } + base.cursor = base.limit - v_1; + lab2: { + if (!(base.out_grouping_b(g_v, 97, 121))) + { + break lab2; + } + if (!(base.in_grouping_b(g_v, 97, 121))) + { + break lab2; + } + if (base.cursor > base.limit_backward) + { + break lab2; + } + break lab0; + } + base.cursor = base.limit - v_1; + if (!(base.eq_s_b("past"))) + { + return false; + } + } + return true; + }; + + /** @return {boolean} */ + function r_R1() { + return I_p1 <= base.cursor; + }; + + /** @return {boolean} */ + function r_R2() { + return I_p2 <= base.cursor; + }; + + /** @return {boolean} */ + function r_Step_1a() { + var /** number */ among_var; + /** @const */ var /** number */ v_1 = base.limit - base.cursor; + lab0: { + base.ket = base.cursor; + if (base.find_among_b(a_1) == 0) + { + base.cursor = base.limit - v_1; + break lab0; + } + base.bra = base.cursor; + if (!base.slice_del()) + { + return false; + } + } + base.ket = base.cursor; + among_var = base.find_among_b(a_2); + if (among_var == 0) + { + return false; + } + base.bra = base.cursor; + switch (among_var) { + case 1: + if (!base.slice_from("ss")) + { + return false; + } + break; + case 2: + lab1: { + /** @const */ var /** number */ v_2 = base.limit - base.cursor; + lab2: { + { + /** @const */ var /** number */ c1 = base.cursor - 2; + if (c1 < base.limit_backward) + { + break lab2; + } + base.cursor = c1; + } + if (!base.slice_from("i")) + { + return false; + } + break lab1; + } + base.cursor = base.limit - v_2; + if (!base.slice_from("ie")) + { + return false; + } + } + break; + case 3: + if (base.cursor <= base.limit_backward) + { + return false; + } + base.cursor--; + if (!base.go_out_grouping_b(g_v, 97, 121)) + { + return false; + } + base.cursor--; + if (!base.slice_del()) + { + return false; + } + break; + } + return true; + }; + + /** @return {boolean} */ + function r_Step_1b() { + var /** number */ among_var; + base.ket = base.cursor; + among_var = base.find_among_b(a_5); + base.bra = base.cursor; + lab0: { + /** @const */ var /** number */ v_1 = base.limit - base.cursor; + lab1: { + switch (among_var) { + case 1: + /** @const */ var /** number */ v_2 = base.limit - base.cursor; + lab2: { + lab3: { + /** @const */ var /** number */ v_3 = base.limit - base.cursor; + lab4: { + if (base.find_among_b(a_3) == 0) + { + break lab4; + } + if (base.cursor > base.limit_backward) + { + break lab4; + } + break lab3; + } + base.cursor = base.limit - v_3; + if (!r_R1()) + { + break lab2; + } + if (!base.slice_from("ee")) + { + return false; + } + } + } + base.cursor = base.limit - v_2; + break; + case 2: + break lab1; + case 3: + among_var = base.find_among_b(a_4); + if (among_var == 0) + { + break lab1; + } + switch (among_var) { + case 1: + /** @const */ var /** number */ v_4 = base.limit - base.cursor; + if (!(base.out_grouping_b(g_v, 97, 121))) + { + break lab1; + } + if (base.cursor > base.limit_backward) + { + break lab1; + } + base.cursor = base.limit - v_4; + base.bra = base.cursor; + if (!base.slice_from("ie")) + { + return false; + } + break; + case 2: + if (base.cursor > base.limit_backward) + { + break lab1; + } + break; + } + break; + } + break lab0; + } + base.cursor = base.limit - v_1; + /** @const */ var /** number */ v_5 = base.limit - base.cursor; + if (!base.go_out_grouping_b(g_v, 97, 121)) + { + return false; + } + base.cursor--; + base.cursor = base.limit - v_5; + if (!base.slice_del()) + { + return false; + } + base.ket = base.cursor; + base.bra = base.cursor; + /** @const */ var /** number */ v_6 = base.limit - base.cursor; + among_var = base.find_among_b(a_6); + switch (among_var) { + case 1: + if (!base.slice_from("e")) + { + return false; + } + return false; + case 2: + { + /** @const */ var /** number */ v_7 = base.limit - base.cursor; + lab5: { + if (!(base.in_grouping_b(g_aeo, 97, 111))) + { + break lab5; + } + if (base.cursor > base.limit_backward) + { + break lab5; + } + return false; + } + base.cursor = base.limit - v_7; + } + break; + case 3: + if (base.cursor != I_p1) + { + return false; + } + /** @const */ var /** number */ v_8 = base.limit - base.cursor; + if (!r_shortv()) + { + return false; + } + base.cursor = base.limit - v_8; + if (!base.slice_from("e")) + { + return false; + } + return false; + } + base.cursor = base.limit - v_6; + base.ket = base.cursor; + if (base.cursor <= base.limit_backward) + { + return false; + } + base.cursor--; + base.bra = base.cursor; + if (!base.slice_del()) + { + return false; + } + } + return true; + }; + + /** @return {boolean} */ + function r_Step_1c() { + base.ket = base.cursor; + lab0: { + /** @const */ var /** number */ v_1 = base.limit - base.cursor; + lab1: { + if (!(base.eq_s_b("y"))) + { + break lab1; + } + break lab0; + } + base.cursor = base.limit - v_1; + if (!(base.eq_s_b("Y"))) + { + return false; + } + } + base.bra = base.cursor; + if (!(base.out_grouping_b(g_v, 97, 121))) + { + return false; + } + lab2: { + if (base.cursor > base.limit_backward) + { + break lab2; + } + return false; + } + if (!base.slice_from("i")) + { + return false; + } + return true; + }; + + /** @return {boolean} */ + function r_Step_2() { + var /** number */ among_var; + base.ket = base.cursor; + among_var = base.find_among_b(a_7); + if (among_var == 0) + { + return false; + } + base.bra = base.cursor; + if (!r_R1()) + { + return false; + } + switch (among_var) { + case 1: + if (!base.slice_from("tion")) + { + return false; + } + break; + case 2: + if (!base.slice_from("ence")) + { + return false; + } + break; + case 3: + if (!base.slice_from("ance")) + { + return false; + } + break; + case 4: + if (!base.slice_from("able")) + { + return false; + } + break; + case 5: + if (!base.slice_from("ent")) + { + return false; + } + break; + case 6: + if (!base.slice_from("ize")) + { + return false; + } + break; + case 7: + if (!base.slice_from("ate")) + { + return false; + } + break; + case 8: + if (!base.slice_from("al")) + { + return false; + } + break; + case 9: + if (!base.slice_from("ful")) + { + return false; + } + break; + case 10: + if (!base.slice_from("ous")) + { + return false; + } + break; + case 11: + if (!base.slice_from("ive")) + { + return false; + } + break; + case 12: + if (!base.slice_from("ble")) + { + return false; + } + break; + case 13: + if (!base.slice_from("og")) + { + return false; + } + break; + case 14: + if (!(base.eq_s_b("l"))) + { + return false; + } + if (!base.slice_from("og")) + { + return false; + } + break; + case 15: + if (!base.slice_from("less")) + { + return false; + } + break; + case 16: + if (!(base.in_grouping_b(g_valid_LI, 99, 116))) + { + return false; + } + if (!base.slice_del()) + { + return false; + } + break; + } + return true; + }; + + /** @return {boolean} */ + function r_Step_3() { + var /** number */ among_var; + base.ket = base.cursor; + among_var = base.find_among_b(a_8); + if (among_var == 0) + { + return false; + } + base.bra = base.cursor; + if (!r_R1()) + { + return false; + } + switch (among_var) { + case 1: + if (!base.slice_from("tion")) + { + return false; + } + break; + case 2: + if (!base.slice_from("ate")) + { + return false; + } + break; + case 3: + if (!base.slice_from("al")) + { + return false; + } + break; + case 4: + if (!base.slice_from("ic")) + { + return false; + } + break; + case 5: + if (!base.slice_del()) + { + return false; + } + break; + case 6: + if (!r_R2()) + { + return false; + } + if (!base.slice_del()) + { + return false; + } + break; + } + return true; + }; + + /** @return {boolean} */ + function r_Step_4() { + var /** number */ among_var; + base.ket = base.cursor; + among_var = base.find_among_b(a_9); + if (among_var == 0) + { + return false; + } + base.bra = base.cursor; + if (!r_R2()) + { + return false; + } + switch (among_var) { + case 1: + if (!base.slice_del()) + { + return false; + } + break; + case 2: + lab0: { + /** @const */ var /** number */ v_1 = base.limit - base.cursor; + lab1: { + if (!(base.eq_s_b("s"))) + { + break lab1; + } + break lab0; + } + base.cursor = base.limit - v_1; + if (!(base.eq_s_b("t"))) + { + return false; + } + } + if (!base.slice_del()) + { + return false; + } + break; + } + return true; + }; + + /** @return {boolean} */ + function r_Step_5() { + var /** number */ among_var; + base.ket = base.cursor; + among_var = base.find_among_b(a_10); + if (among_var == 0) + { + return false; + } + base.bra = base.cursor; + switch (among_var) { + case 1: + lab0: { + lab1: { + if (!r_R2()) + { + break lab1; + } + break lab0; + } + if (!r_R1()) + { + return false; + } + { + /** @const */ var /** number */ v_1 = base.limit - base.cursor; + lab2: { + if (!r_shortv()) + { + break lab2; + } + return false; + } + base.cursor = base.limit - v_1; + } + } + if (!base.slice_del()) + { + return false; + } + break; + case 2: + if (!r_R2()) + { + return false; + } + if (!(base.eq_s_b("l"))) + { + return false; + } + if (!base.slice_del()) + { + return false; + } + break; + } + return true; + }; + + /** @return {boolean} */ + function r_exception1() { + var /** number */ among_var; + base.bra = base.cursor; + among_var = base.find_among(a_11); + if (among_var == 0) + { + return false; + } + base.ket = base.cursor; + if (base.cursor < base.limit) + { + return false; + } + switch (among_var) { + case 1: + if (!base.slice_from("sky")) + { + return false; + } + break; + case 2: + if (!base.slice_from("idl")) + { + return false; + } + break; + case 3: + if (!base.slice_from("gentl")) + { + return false; + } + break; + case 4: + if (!base.slice_from("ugli")) + { + return false; + } + break; + case 5: + if (!base.slice_from("earli")) + { + return false; + } + break; + case 6: + if (!base.slice_from("onli")) + { + return false; + } + break; + case 7: + if (!base.slice_from("singl")) + { + return false; + } + break; + } + return true; + }; + + /** @return {boolean} */ + function r_postlude() { + if (!B_Y_found) + { + return false; + } + while(true) + { + /** @const */ var /** number */ v_1 = base.cursor; + lab0: { + golab1: while(true) + { + /** @const */ var /** number */ v_2 = base.cursor; + lab2: { + base.bra = base.cursor; + if (!(base.eq_s("Y"))) + { + break lab2; + } + base.ket = base.cursor; + base.cursor = v_2; + break golab1; + } + base.cursor = v_2; + if (base.cursor >= base.limit) + { + break lab0; + } + base.cursor++; + } + if (!base.slice_from("y")) + { + return false; + } + continue; + } + base.cursor = v_1; + break; + } + return true; + }; + + this.stem = /** @return {boolean} */ function() { + lab0: { + /** @const */ var /** number */ v_1 = base.cursor; + lab1: { + if (!r_exception1()) + { + break lab1; + } + break lab0; + } + base.cursor = v_1; + lab2: { + { + /** @const */ var /** number */ v_2 = base.cursor; + lab3: { + { + /** @const */ var /** number */ c1 = base.cursor + 3; + if (c1 > base.limit) + { + break lab3; + } + base.cursor = c1; + } + break lab2; + } + base.cursor = v_2; + } + break lab0; + } + base.cursor = v_1; + r_prelude(); + r_mark_regions(); + base.limit_backward = base.cursor; base.cursor = base.limit; + /** @const */ var /** number */ v_3 = base.limit - base.cursor; + r_Step_1a(); + base.cursor = base.limit - v_3; + /** @const */ var /** number */ v_4 = base.limit - base.cursor; + r_Step_1b(); + base.cursor = base.limit - v_4; + /** @const */ var /** number */ v_5 = base.limit - base.cursor; + r_Step_1c(); + base.cursor = base.limit - v_5; + /** @const */ var /** number */ v_6 = base.limit - base.cursor; + r_Step_2(); + base.cursor = base.limit - v_6; + /** @const */ var /** number */ v_7 = base.limit - base.cursor; + r_Step_3(); + base.cursor = base.limit - v_7; + /** @const */ var /** number */ v_8 = base.limit - base.cursor; + r_Step_4(); + base.cursor = base.limit - v_8; + /** @const */ var /** number */ v_9 = base.limit - base.cursor; + r_Step_5(); + base.cursor = base.limit - v_9; + base.cursor = base.limit_backward; + /** @const */ var /** number */ v_10 = base.cursor; + r_postlude(); + base.cursor = v_10; + } + return true; + }; + + /**@return{string}*/ + this['stemWord'] = function(/**string*/word) { + base.setCurrent(word); + this.stem(); + return base.getCurrent(); + }; +}; diff --git a/docs/_static/fonts/Vazirmatn-NL-VF.woff2 b/docs/_static/fonts/Vazirmatn-NL-VF.woff2 new file mode 100644 index 0000000..949ad26 Binary files /dev/null and b/docs/_static/fonts/Vazirmatn-NL-VF.woff2 differ diff --git a/docs/_static/language_data.js b/docs/_static/language_data.js index a5ea78e..5776786 100644 --- a/docs/_static/language_data.js +++ b/docs/_static/language_data.js @@ -1,191 +1,13 @@ /* * This script contains the language-specific data used by searchtools.js, - * namely the list of stopwords, stemmer, scorer and splitter. + * namely the set of stopwords, stemmer, scorer and splitter. */ -var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"]; +const stopwords = new Set(["a", "about", "above", "after", "again", "against", "all", "am", "an", "and", "any", "are", "aren't", "as", "at", "be", "because", "been", "before", "being", "below", "between", "both", "but", "by", "can't", "cannot", "could", "couldn't", "did", "didn't", "do", "does", "doesn't", "doing", "don't", "down", "during", "each", "few", "for", "from", "further", "had", "hadn't", "has", "hasn't", "have", "haven't", "having", "he", "he'd", "he'll", "he's", "her", "here", "here's", "hers", "herself", "him", "himself", "his", "how", "how's", "i", "i'd", "i'll", "i'm", "i've", "if", "in", "into", "is", "isn't", "it", "it's", "its", "itself", "let's", "me", "more", "most", "mustn't", "my", "myself", "no", "nor", "not", "of", "off", "on", "once", "only", "or", "other", "ought", "our", "ours", "ourselves", "out", "over", "own", "same", "shan't", "she", "she'd", "she'll", "she's", "should", "shouldn't", "so", "some", "such", "than", "that", "that's", "the", "their", "theirs", "them", "themselves", "then", "there", "there's", "these", "they", "they'd", "they'll", "they're", "they've", "this", "those", "through", "to", "too", "under", "until", "up", "very", "was", "wasn't", "we", "we'd", "we'll", "we're", "we've", "were", "weren't", "what", "what's", "when", "when's", "where", "where's", "which", "while", "who", "who's", "whom", "why", "why's", "with", "won't", "would", "wouldn't", "you", "you'd", "you'll", "you're", "you've", "your", "yours", "yourself", "yourselves"]); +window.stopwords = stopwords; // Export to global scope -/* Non-minified version is copied as a separate JS file, if available */ - -/** - * Porter Stemmer - */ -var Stemmer = function() { - - var step2list = { - ational: 'ate', - tional: 'tion', - enci: 'ence', - anci: 'ance', - izer: 'ize', - bli: 'ble', - alli: 'al', - entli: 'ent', - eli: 'e', - ousli: 'ous', - ization: 'ize', - ation: 'ate', - ator: 'ate', - alism: 'al', - iveness: 'ive', - fulness: 'ful', - ousness: 'ous', - aliti: 'al', - iviti: 'ive', - biliti: 'ble', - logi: 'log' - }; - - var step3list = { - icate: 'ic', - ative: '', - alize: 'al', - iciti: 'ic', - ical: 'ic', - ful: '', - ness: '' - }; - - var c = "[^aeiou]"; // consonant - var v = "[aeiouy]"; // vowel - var C = c + "[^aeiouy]*"; // consonant sequence - var V = v + "[aeiou]*"; // vowel sequence - - var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 - var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 - var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 - var s_v = "^(" + C + ")?" + v; // vowel in stem - - this.stemWord = function (w) { - var stem; - var suffix; - var firstch; - var origword = w; - - if (w.length < 3) - return w; - - var re; - var re2; - var re3; - var re4; - - firstch = w.substr(0,1); - if (firstch == "y") - w = firstch.toUpperCase() + w.substr(1); - - // Step 1a - re = /^(.+?)(ss|i)es$/; - re2 = /^(.+?)([^s])s$/; - - if (re.test(w)) - w = w.replace(re,"$1$2"); - else if (re2.test(w)) - w = w.replace(re2,"$1$2"); - - // Step 1b - re = /^(.+?)eed$/; - re2 = /^(.+?)(ed|ing)$/; - if (re.test(w)) { - var fp = re.exec(w); - re = new RegExp(mgr0); - if (re.test(fp[1])) { - re = /.$/; - w = w.replace(re,""); - } - } - else if (re2.test(w)) { - var fp = re2.exec(w); - stem = fp[1]; - re2 = new RegExp(s_v); - if (re2.test(stem)) { - w = stem; - re2 = /(at|bl|iz)$/; - re3 = new RegExp("([^aeiouylsz])\\1$"); - re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); - if (re2.test(w)) - w = w + "e"; - else if (re3.test(w)) { - re = /.$/; - w = w.replace(re,""); - } - else if (re4.test(w)) - w = w + "e"; - } - } - - // Step 1c - re = /^(.+?)y$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - re = new RegExp(s_v); - if (re.test(stem)) - w = stem + "i"; - } - - // Step 2 - re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - suffix = fp[2]; - re = new RegExp(mgr0); - if (re.test(stem)) - w = stem + step2list[suffix]; - } - - // Step 3 - re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - suffix = fp[2]; - re = new RegExp(mgr0); - if (re.test(stem)) - w = stem + step3list[suffix]; - } - - // Step 4 - re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; - re2 = /^(.+?)(s|t)(ion)$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - re = new RegExp(mgr1); - if (re.test(stem)) - w = stem; - } - else if (re2.test(w)) { - var fp = re2.exec(w); - stem = fp[1] + fp[2]; - re2 = new RegExp(mgr1); - if (re2.test(stem)) - w = stem; - } - - // Step 5 - re = /^(.+?)e$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - re = new RegExp(mgr1); - re2 = new RegExp(meq1); - re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); - if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) - w = stem; - } - re = /ll$/; - re2 = new RegExp(mgr1); - if (re.test(w) && re2.test(w)) { - re = /.$/; - w = w.replace(re,""); - } - - // and turn initial Y back to y - if (firstch == "y") - w = firstch.toLowerCase() + w.substr(1); - return w; - } -} +/* Non-minified versions are copied as separate JavaScript files, if available */ +BaseStemmer=function(){this.current="",this.cursor=0,this.limit=0,this.limit_backward=0,this.bra=0,this.ket=0,this.setCurrent=function(t){this.current=t,this.cursor=0,this.limit=this.current.length,this.limit_backward=0,this.bra=this.cursor,this.ket=this.limit},this.getCurrent=function(){return this.current},this.copy_from=function(t){this.current=t.current,this.cursor=t.cursor,this.limit=t.limit,this.limit_backward=t.limit_backward,this.bra=t.bra,this.ket=t.ket},this.in_grouping=function(t,r,i){return!(this.cursor>=this.limit||i<(i=this.current.charCodeAt(this.cursor))||i>>3]&1<<(7&i))||(this.cursor++,0))},this.go_in_grouping=function(t,r,i){for(;this.cursor>>3]&1<<(7&s)))return!0;this.cursor++}return!1},this.in_grouping_b=function(t,r,i){return!(this.cursor<=this.limit_backward||i<(i=this.current.charCodeAt(this.cursor-1))||i>>3]&1<<(7&i))||(this.cursor--,0))},this.go_in_grouping_b=function(t,r,i){for(;this.cursor>this.limit_backward;){var s=this.current.charCodeAt(this.cursor-1);if(i>>3]&1<<(7&s)))return!0;this.cursor--}return!1},this.out_grouping=function(t,r,i){return!(this.cursor>=this.limit)&&(i<(i=this.current.charCodeAt(this.cursor))||i>>3]&1<<(7&i)))&&(this.cursor++,!0)},this.go_out_grouping=function(t,r,i){for(;this.cursor>>3]&1<<(7&s)))return!0;this.cursor++}return!1},this.out_grouping_b=function(t,r,i){return!(this.cursor<=this.limit_backward)&&(i<(i=this.current.charCodeAt(this.cursor-1))||i>>3]&1<<(7&i)))&&(this.cursor--,!0)},this.go_out_grouping_b=function(t,r,i){for(;this.cursor>this.limit_backward;){var s=this.current.charCodeAt(this.cursor-1);if(s<=i&&r<=s&&0!=(t[(s-=r)>>>3]&1<<(7&s)))return!0;this.cursor--}return!1},this.eq_s=function(t){return!(this.limit-this.cursor>>1),o=0,a=e=(l=t[r])[0].length){if(this.cursor=s+l[0].length,l.length<4)return l[2];var g=l[3](this);if(this.cursor=s+l[0].length,g)return l[2]}}while(0<=(r=l[1]));return 0},this.find_among_b=function(t){for(var r=0,i=t.length,s=this.cursor,h=this.limit_backward,e=0,n=0,c=!1;;){for(var u,o=r+(i-r>>1),a=0,l=e=(u=t[r])[0].length){if(this.cursor=s-u[0].length,u.length<4)return u[2];var g=u[3](this);if(this.cursor=s-u[0].length,g)return u[2]}}while(0<=(r=u[1]));return 0},this.replace_s=function(t,r,i){var s=i.length-(r-t);return this.current=this.current.slice(0,t)+i+this.current.slice(r),this.limit+=s,this.cursor>=r?this.cursor+=s:this.cursor>t&&(this.cursor=t),s},this.slice_check=function(){return!(this.bra<0||this.bra>this.ket||this.ket>this.limit||this.limit>this.current.length)},this.slice_from=function(t){var r=!1;return this.slice_check()&&(this.replace_s(this.bra,this.ket,t),r=!0),r},this.slice_del=function(){return this.slice_from("")},this.insert=function(t,r,i){r=this.replace_s(t,r,i);t<=this.bra&&(this.bra+=r),t<=this.ket&&(this.ket+=r)},this.slice_to=function(){var t="";return t=this.slice_check()?this.current.slice(this.bra,this.ket):t},this.assign_to=function(){return this.current.slice(0,this.limit)}}; +var EnglishStemmer=function(){var a=new BaseStemmer,c=[["arsen",-1,-1],["commun",-1,-1],["emerg",-1,-1],["gener",-1,-1],["later",-1,-1],["organ",-1,-1],["past",-1,-1],["univers",-1,-1]],o=[["'",-1,1],["'s'",0,1],["'s",-1,1]],u=[["ied",-1,2],["s",-1,3],["ies",1,2],["sses",1,1],["ss",1,-1],["us",1,-1]],t=[["succ",-1,1],["proc",-1,1],["exc",-1,1]],l=[["even",-1,2],["cann",-1,2],["inn",-1,2],["earr",-1,2],["herr",-1,2],["out",-1,2],["y",-1,1]],n=[["",-1,-1],["ed",0,2],["eed",1,1],["ing",0,3],["edly",0,2],["eedly",4,1],["ingly",0,2]],f=[["",-1,3],["bb",0,2],["dd",0,2],["ff",0,2],["gg",0,2],["bl",0,1],["mm",0,2],["nn",0,2],["pp",0,2],["rr",0,2],["at",0,1],["tt",0,2],["iz",0,1]],_=[["anci",-1,3],["enci",-1,2],["ogi",-1,14],["li",-1,16],["bli",3,12],["abli",4,4],["alli",3,8],["fulli",3,9],["lessli",3,15],["ousli",3,10],["entli",3,5],["aliti",-1,8],["biliti",-1,12],["iviti",-1,11],["tional",-1,1],["ational",14,7],["alism",-1,8],["ation",-1,7],["ization",17,6],["izer",-1,6],["ator",-1,7],["iveness",-1,11],["fulness",-1,9],["ousness",-1,10],["ogist",-1,13]],m=[["icate",-1,4],["ative",-1,6],["alize",-1,3],["iciti",-1,4],["ical",-1,4],["tional",-1,1],["ational",5,2],["ful",-1,5],["ness",-1,5]],b=[["ic",-1,1],["ance",-1,1],["ence",-1,1],["able",-1,1],["ible",-1,1],["ate",-1,1],["ive",-1,1],["ize",-1,1],["iti",-1,1],["al",-1,1],["ism",-1,1],["ion",-1,2],["er",-1,1],["ous",-1,1],["ant",-1,1],["ent",-1,1],["ment",15,1],["ement",16,1]],k=[["e",-1,1],["l",-1,2]],g=[["andes",-1,-1],["atlas",-1,-1],["bias",-1,-1],["cosmos",-1,-1],["early",-1,5],["gently",-1,3],["howe",-1,-1],["idly",-1,2],["news",-1,-1],["only",-1,6],["singly",-1,7],["skies",-1,1],["sky",-1,-1],["ugly",-1,4]],d=[17,64],v=[17,65,16,1],i=[1,17,65,208,1],w=[55,141,2],p=!1,y=0,h=0;function q(){var r=a.limit-a.cursor;return!!(a.out_grouping_b(i,89,121)&&a.in_grouping_b(v,97,121)&&a.out_grouping_b(v,97,121)||(a.cursor=a.limit-r,a.out_grouping_b(v,97,121)&&a.in_grouping_b(v,97,121)&&!(a.cursor>a.limit_backward))||(a.cursor=a.limit-r,a.eq_s_b("past")))}function z(){return h<=a.cursor}function Y(){return y<=a.cursor}this.stem=function(){var r=a.cursor;if(!(()=>{var r;if(a.bra=a.cursor,0!=(r=a.find_among(g))&&(a.ket=a.cursor,!(a.cursora.limit)a.cursor=i;else{a.cursor=e,a.cursor=r,(()=>{p=!1;var r=a.cursor;if(a.bra=a.cursor,!a.eq_s("'")||(a.ket=a.cursor,a.slice_del())){a.cursor=r;r=a.cursor;if(a.bra=a.cursor,a.eq_s("y")){if(a.ket=a.cursor,!a.slice_from("Y"))return;p=!0}a.cursor=r;for(r=a.cursor;;){var i=a.cursor;r:{for(;;){var e=a.cursor;if(a.in_grouping(v,97,121)&&(a.bra=a.cursor,a.eq_s("y"))){a.ket=a.cursor,a.cursor=e;break}if(a.cursor=e,a.cursor>=a.limit)break r;a.cursor++}if(!a.slice_from("Y"))return;p=!0;continue}a.cursor=i;break}a.cursor=r}})(),h=a.limit,y=a.limit;i=a.cursor;r:{var s=a.cursor;if(0==a.find_among(c)){if(a.cursor=s,!a.go_out_grouping(v,97,121))break r;if(a.cursor++,!a.go_in_grouping(v,97,121))break r;a.cursor++}h=a.cursor,a.go_out_grouping(v,97,121)&&(a.cursor++,a.go_in_grouping(v,97,121))&&(a.cursor++,y=a.cursor)}a.cursor=i,a.limit_backward=a.cursor,a.cursor=a.limit;var e=a.limit-a.cursor,r=((()=>{var r=a.limit-a.cursor;if(a.ket=a.cursor,0==a.find_among_b(o))a.cursor=a.limit-r;else if(a.bra=a.cursor,!a.slice_del())return;if(a.ket=a.cursor,0!=(r=a.find_among_b(u)))switch(a.bra=a.cursor,r){case 1:if(a.slice_from("ss"))break;return;case 2:r:{var i=a.limit-a.cursor,e=a.cursor-2;if(!(e{a.ket=a.cursor,o=a.find_among_b(n),a.bra=a.cursor;r:{var r=a.limit-a.cursor;i:{switch(o){case 1:var i=a.limit-a.cursor;e:{var e=a.limit-a.cursor;if(0==a.find_among_b(t)||a.cursor>a.limit_backward){if(a.cursor=a.limit-e,!z())break e;if(!a.slice_from("ee"))return}}a.cursor=a.limit-i;break;case 2:break i;case 3:if(0==(o=a.find_among_b(l)))break i;switch(o){case 1:var s=a.limit-a.cursor;if(!a.out_grouping_b(v,97,121))break i;if(a.cursor>a.limit_backward)break i;if(a.cursor=a.limit-s,a.bra=a.cursor,a.slice_from("ie"))break;return;case 2:if(a.cursor>a.limit_backward)break i}}break r}a.cursor=a.limit-r;var c=a.limit-a.cursor;if(!a.go_out_grouping_b(v,97,121))return;if(a.cursor--,a.cursor=a.limit-c,!a.slice_del())return;a.ket=a.cursor,a.bra=a.cursor;var o,c=a.limit-a.cursor;switch(o=a.find_among_b(f)){case 1:return a.slice_from("e");case 2:var u=a.limit-a.cursor;if(a.in_grouping_b(d,97,111)&&!(a.cursor>a.limit_backward))return;a.cursor=a.limit-u;break;case 3:return a.cursor!=h||(u=a.limit-a.cursor,q()&&(a.cursor=a.limit-u,a.slice_from("e")))}if(a.cursor=a.limit-c,a.ket=a.cursor,a.cursor<=a.limit_backward)return;if(a.cursor--,a.bra=a.cursor,!a.slice_del())return}})(),a.cursor=a.limit-r,a.limit-a.cursor),r=(a.ket=a.cursor,e=a.limit-a.cursor,(a.eq_s_b("y")||(a.cursor=a.limit-e,a.eq_s_b("Y")))&&(a.bra=a.cursor,a.out_grouping_b(v,97,121))&&a.cursor>a.limit_backward&&a.slice_from("i"),a.cursor=a.limit-i,a.limit-a.cursor),e=((()=>{var r;if(a.ket=a.cursor,0!=(r=a.find_among_b(_))&&(a.bra=a.cursor,z()))switch(r){case 1:if(a.slice_from("tion"))break;return;case 2:if(a.slice_from("ence"))break;return;case 3:if(a.slice_from("ance"))break;return;case 4:if(a.slice_from("able"))break;return;case 5:if(a.slice_from("ent"))break;return;case 6:if(a.slice_from("ize"))break;return;case 7:if(a.slice_from("ate"))break;return;case 8:if(a.slice_from("al"))break;return;case 9:if(a.slice_from("ful"))break;return;case 10:if(a.slice_from("ous"))break;return;case 11:if(a.slice_from("ive"))break;return;case 12:if(a.slice_from("ble"))break;return;case 13:if(a.slice_from("og"))break;return;case 14:if(!a.eq_s_b("l"))return;if(a.slice_from("og"))break;return;case 15:if(a.slice_from("less"))break;return;case 16:if(!a.in_grouping_b(w,99,116))return;if(a.slice_del())break}})(),a.cursor=a.limit-r,a.limit-a.cursor),i=((()=>{var r;if(a.ket=a.cursor,0!=(r=a.find_among_b(m))&&(a.bra=a.cursor,z()))switch(r){case 1:if(a.slice_from("tion"))break;return;case 2:if(a.slice_from("ate"))break;return;case 3:if(a.slice_from("al"))break;return;case 4:if(a.slice_from("ic"))break;return;case 5:if(a.slice_del())break;return;case 6:if(!Y())return;if(a.slice_del())break}})(),a.cursor=a.limit-e,a.limit-a.cursor),r=((()=>{var r;if(a.ket=a.cursor,0!=(r=a.find_among_b(b))&&(a.bra=a.cursor,Y()))switch(r){case 1:if(a.slice_del())break;return;case 2:var i=a.limit-a.cursor;if(!a.eq_s_b("s")&&(a.cursor=a.limit-i,!a.eq_s_b("t")))return;if(a.slice_del())break}})(),a.cursor=a.limit-i,a.limit-a.cursor),e=((()=>{var r;if(a.ket=a.cursor,0!=(r=a.find_among_b(k)))switch(a.bra=a.cursor,r){case 1:if(!Y()){if(!z())return;var i=a.limit-a.cursor;if(q())return;a.cursor=a.limit-i}if(a.slice_del())break;return;case 2:if(!Y())return;if(!a.eq_s_b("l"))return;if(a.slice_del())break}})(),a.cursor=a.limit-r,a.cursor=a.limit_backward,a.cursor);(()=>{if(p)for(;;){var r=a.cursor;r:{for(;;){var i=a.cursor;if(a.bra=a.cursor,a.eq_s("Y")){a.ket=a.cursor,a.cursor=i;break}if(a.cursor=i,a.cursor>=a.limit)break r;a.cursor++}if(a.slice_from("y"))continue;return}a.cursor=r;break}})(),a.cursor=e}}return!0},this.stemWord=function(r){return a.setCurrent(r),this.stem(),a.getCurrent()}}; +window.Stemmer = EnglishStemmer; diff --git a/docs/_static/pydoctheme.css b/docs/_static/pydoctheme.css index aa9622d..930ee2a 100644 --- a/docs/_static/pydoctheme.css +++ b/docs/_static/pydoctheme.css @@ -6,6 +6,7 @@ --middle-border: rgb(244, 227, 76); --bad-color: rgb(159 49 51); --bad-border: rgb(244, 76, 78); + --sans-serif-font-stack: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif; } /* unset some styles from the classic stylesheet */ @@ -108,7 +109,7 @@ div.related a:hover { } .switchers > div { - margin-right: 5px; + margin-inline-end: 5px; } div.related ul::after { @@ -117,6 +118,17 @@ div.related ul::after { display: block; } +div.related ul { + padding: 0; + padding-inline-start: 10px; +} + +body[dir='rtl'] div.related ul li.right { + float: left; + margin-right: 0; + margin-left: 5px; +} + .inline-search, form.inline-search input { display: inline; @@ -142,6 +154,9 @@ span.pre { div.sphinxsidebar { display: flex; width: min(25vw, 350px); + flex-shrink: 0; + margin: 0; + order: 0; float: none; position: sticky; top: 0; @@ -158,8 +173,14 @@ div.sphinxsidebar h4 { margin-top: 1.5em; } -div.bodywrapper { - margin-left: min(25vw, 350px); +div.document div.bodywrapper { + margin: 0; +} + +div.documentwrapper { + flex: 1; + min-width: 0; + order: 1; } div.sphinxsidebarwrapper { @@ -179,6 +200,11 @@ div.sphinxsidebarwrapper > ul > li > ul > li { margin-bottom: 0.4em; } +div.sphinxsidebar ul ul { + margin-left: 0; + margin-inline-start: 20px; +} + div.sphinxsidebar a:hover { color: #0095c4; } @@ -186,7 +212,7 @@ div.sphinxsidebar a:hover { form.inline-search input, div.sphinxsidebar input, div.related input { - font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif; + font-family: var(--sans-serif-font-stack); border: 1px solid #999999; font-size: smaller; border-radius: 3px; @@ -206,16 +232,23 @@ div.sphinxsidebar input[type='text'] { border-left: none; } +body[dir='rtl'] #sidebarbutton { + border-radius: 5px 0 0 5px; + border-right: none; + border-left: none; +} + #sidebarbutton:hover { background-color: #AAAAAA; } div.body { - padding: 0 0 0 1.2em; + padding: 0; + padding-inline-start: 1.2em; } div.body p, div.body dd, div.body li, div.body blockquote { - text-align: left; + text-align: start; line-height: 1.6; } div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 { @@ -225,6 +258,10 @@ div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 { } div.body dl.field-list > dt { line-height: 1.6; + padding-left: 0; + padding-right: 0; + padding-inline-start: 0.5em; + padding-inline-end: 5px; } div.body hr { @@ -233,6 +270,45 @@ div.body hr { height: 1px; } +/* Persian (fa) uses Vazirmatn, since the default system font stack renders + Arabic-script text poorly on many platforms. */ +@font-face { + font-family: "Vazirmatn NL"; + src: url("fonts/Vazirmatn-NL-VF.woff2") format("woff2"); + font-weight: 100 900; + font-style: normal; + font-display: swap; +} + +:lang(fa) { + :is(div.body, div.sphinxsidebar, .menu-wrapper, div.footer), + :is(div.body, div.sphinxsidebar, .menu-wrapper) :is(h1, h2, h3, h4, h5, h6), + a, + input, + select, + textarea, + button:not(.copybutton), + label { + font-family: "Vazirmatn NL", var(--sans-serif-font-stack); + } +} + +body[dir='rtl'] { + & pre, + & code, + & div.highlight, + & dl > dt span ~ em, + & .sig, + & .sig-param { + direction: ltr; + unicode-bidi: isolate; + } +} + +body[dir='rtl'] .sig { + text-align: right; +} + div.body pre { border-radius: 3px; border: 1px solid #ac9; @@ -402,7 +478,7 @@ table.docutils th { table p, table li { - text-align: left !important; + text-align: start !important; } table.docutils th { @@ -414,11 +490,37 @@ table.footnote td { border: 0 !important; } +body[dir='rtl'] div#search-results, +body[dir='rtl'] div#search-results ul.search li { + direction: rtl; + text-align: right; + unicode-bidi: isolate; +} + +body[dir='rtl'] div#search-results ul.search li p { + text-align: right; +} + +body[dir='rtl'] div.citation > span { + float: right; +} + +body[dir='rtl'] aside.footnote > span { + float: right; + padding-right: 0; + padding-left: 0.5rem; +} + +body[dir='rtl'] aside.footnote > p { + margin-left: 0; + margin-right: 2rem; +} + div.footer { line-height: 150%; - text-align: right; + text-align: end; width: auto; - margin-right: 10px; + margin-inline-end: 10px; } div.footer a { @@ -476,18 +578,17 @@ div.footer a:hover { color: var(--threadsafety-atomic); } - dl > dt span ~ em, .sig { font-family: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace; } .toctree-wrapper ul { - padding-left: 20px; + padding-inline-start: 20px; } .theme-selector { - margin-left: .5em; + margin-inline-start: .5em; } div.genindex-jumpbox, @@ -506,17 +607,17 @@ div.genindex-jumpbox a { cursor: pointer; position: absolute; top: 0; - right: 0; + right: 0; /* physical right; always, even in RTL, since code is LTR */ font-family: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace; font-size: 80%; padding-left: .5em; padding-right: .5em; height: 100%; max-height: min(100%, 2.4em); - border-radius: 0 3px 0 0; + border-radius: 0 3px 0 0; /* physical — top-right corner, always */ color: #000; background-color: #fff; - border: 1px solid #ac9; /* follows div.body pre */ + border: 1px solid #ac9; display: none; } @@ -564,10 +665,16 @@ div.genindex-jumpbox a { margin-inline-start: 1rem; margin-inline-end: 0; } - /* Remove sidebar and top related bar */ - div.related, div.sphinxsidebar { + /* Remove sidebar and desktop-only related controls */ + div.related li.right, div.related li.switchers, div.sphinxsidebar { + display: none; + } + div.related .root-icon, div.related .root-icon + li { display: none; } + div.related { + line-height: 1.3; + } /* Anchorlinks are not hidden by fixed-positioned navbar when scrolled to */ html { scroll-padding-top: 40px; @@ -583,7 +690,7 @@ div.genindex-jumpbox a { width: 100%; position: fixed; top: 0; - left: 0; + inset-inline-start: 0; box-shadow: rgba(0, 0, 0, 0.25) 0 0 2px 0; z-index: 1; } @@ -605,7 +712,7 @@ div.genindex-jumpbox a { align-items: stretch; } .nav-logo { - margin-right: 1rem; + margin-inline-end: 1rem; flex-shrink: 0; align-self: center; } @@ -614,7 +721,7 @@ div.genindex-jumpbox a { width: 20px; } .version_switcher_placeholder { - margin-right: 1rem; + margin-inline-end: 1rem; } .version_switcher_placeholder > select { height: 100%; @@ -624,10 +731,11 @@ div.genindex-jumpbox a { flex: auto; border: 1px solid #a9a9a9; align-items: stretch; + position: relative; } .nav-content .search input[type=search] { border: 0; - padding-left: 24px; + padding-inline-start: 24px; width: 100%; flex: 1; } @@ -635,14 +743,15 @@ div.genindex-jumpbox a { height: 100%; box-shadow: none; border: 0; - border-left: 1px solid #a9a9a9; + border-inline-start: 1px solid #a9a9a9; cursor: pointer; - margin-right: 0; + margin-inline-end: 0; } .nav-content .search svg { position: absolute; - align-self: center; - padding-left: 4px; + top: 50%; + transform: translateY(-50%); + inset-inline-start: 4px; } .toggler__input { display: none; @@ -674,7 +783,7 @@ div.genindex-jumpbox a { width: 100%; background: inherit; position: absolute; - left: 0; + inset-inline-start: 0; top: -8px; } .toggler__label > span::after { @@ -695,7 +804,7 @@ div.genindex-jumpbox a { } .toggler__input:checked ~ .menu-wrapper { visibility: visible; - left: 0; + transform: translateX(0); } /* Sliding side menu */ @@ -703,8 +812,9 @@ div.genindex-jumpbox a { display: block; position: fixed; top: 0; - transition: left 400ms ease; - left: -310px; + inset-inline-start: 0; + transition: transform 400ms ease; + transform: translateX(calc(-100% - 10px)); width: 300px; height: 100%; background-color: #eee; @@ -712,12 +822,18 @@ div.genindex-jumpbox a { box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); overflow-y: auto; } + body[dir='rtl'] .menu-wrapper { + transform: translateX(100%); + } .menu-wrapper.open { visibility: visible; - left: 0; + transform: translateX(0); } .menu { - padding: 40px 10px 30px 20px; + padding-block-start: 40px; + padding-inline-end: 10px; + padding-block-end: 30px; + padding-inline-start: 20px; } .menu-wrapper h3, .menu-wrapper h4 { @@ -742,10 +858,10 @@ div.genindex-jumpbox a { list-style: none; line-height: 1.4; overflow-wrap: break-word; - padding-left: 0; + padding-inline-start: 0; } .menu ul ul { - margin-left: 20px; + margin-inline-start: 20px; list-style: square; } .menu ul li { @@ -801,26 +917,26 @@ div.versionadded, div.versionchanged, div.deprecated, div.deprecated-removed { - border-left: 3px solid; + border-inline-start: 3px solid; padding: 0 1rem; } div.versionadded { - border-left-color: var(--versionadded-border); + border-inline-start-color: var(--versionadded-border); } div.versionchanged { - border-left-color: var(--versionchanged-border); + border-inline-start-color: var(--versionchanged-border); } div.deprecated, div.deprecated-removed, div.versionremoved { - border-left-color: var(--deprecated-border); + border-inline-start-color: var(--deprecated-border); } div.soft-deprecated { - border-left-color: var(--soft-deprecated-border); + border-inline-start-color: var(--soft-deprecated-border); } div.versionadded .versionmodified { diff --git a/docs/_static/pydoctheme_dark.css b/docs/_static/pydoctheme_dark.css index df91a2a..2139108 100644 --- a/docs/_static/pydoctheme_dark.css +++ b/docs/_static/pydoctheme_dark.css @@ -49,7 +49,7 @@ div.body a { } div.body a:visited { - color: #09e; + color: #bf95f9; } a.headerlink:hover { diff --git a/docs/_static/pygments_dark.css b/docs/_static/pygments_dark.css index 60e43ba..409989e 100644 --- a/docs/_static/pygments_dark.css +++ b/docs/_static/pygments_dark.css @@ -24,14 +24,14 @@ span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: .highlight .gd { color: #FF4689 } /* Generic.Deleted */ .highlight .ge { color: #F8F8F2; font-style: italic } /* Generic.Emph */ .highlight .ges { color: #F8F8F2; font-weight: bold; font-style: italic } /* Generic.EmphStrong */ -.highlight .gr { color: #F8F8F2 } /* Generic.Error */ +.highlight .gr { color: #FF4689 } /* Generic.Error */ .highlight .gh { color: #F8F8F2 } /* Generic.Heading */ .highlight .gi { color: #A6E22E } /* Generic.Inserted */ .highlight .go { color: #66D9EF } /* Generic.Output */ .highlight .gp { color: #FF4689; font-weight: bold } /* Generic.Prompt */ .highlight .gs { color: #F8F8F2; font-weight: bold } /* Generic.Strong */ .highlight .gu { color: #959077 } /* Generic.Subheading */ -.highlight .gt { color: #F8F8F2 } /* Generic.Traceback */ +.highlight .gt { color: #66D9EF } /* Generic.Traceback */ .highlight .kc { color: #66D9EF } /* Keyword.Constant */ .highlight .kd { color: #66D9EF } /* Keyword.Declaration */ .highlight .kn { color: #FF4689 } /* Keyword.Namespace */ @@ -42,7 +42,7 @@ span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: .highlight .m { color: #AE81FF } /* Literal.Number */ .highlight .s { color: #E6DB74 } /* Literal.String */ .highlight .na { color: #A6E22E } /* Name.Attribute */ -.highlight .nb { color: #F8F8F2 } /* Name.Builtin */ +.highlight .nb { color: #A6E22E } /* Name.Builtin */ .highlight .nc { color: #A6E22E } /* Name.Class */ .highlight .no { color: #66D9EF } /* Name.Constant */ .highlight .nd { color: #A6E22E } /* Name.Decorator */ @@ -76,7 +76,7 @@ span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: .highlight .sr { color: #E6DB74 } /* Literal.String.Regex */ .highlight .s1 { color: #E6DB74 } /* Literal.String.Single */ .highlight .ss { color: #E6DB74 } /* Literal.String.Symbol */ -.highlight .bp { color: #F8F8F2 } /* Name.Builtin.Pseudo */ +.highlight .bp { color: #A6E22E } /* Name.Builtin.Pseudo */ .highlight .fm { color: #A6E22E } /* Name.Function.Magic */ .highlight .vc { color: #F8F8F2 } /* Name.Variable.Class */ .highlight .vg { color: #F8F8F2 } /* Name.Variable.Global */ diff --git a/docs/_static/searchtools.js b/docs/_static/searchtools.js index 91f4be5..e29b1c7 100644 --- a/docs/_static/searchtools.js +++ b/docs/_static/searchtools.js @@ -41,11 +41,12 @@ if (typeof Scorer === "undefined") { } // Global search result kind enum, used by themes to style search results. +// prettier-ignore class SearchResultKind { - static get index() { return "index"; } - static get object() { return "object"; } - static get text() { return "text"; } - static get title() { return "title"; } + static get index() { return "index"; } + static get object() { return "object"; } + static get text() { return "text"; } + static get title() { return "title"; } } const _removeChildren = (element) => { @@ -58,6 +59,15 @@ const _removeChildren = (element) => { const _escapeRegExp = (string) => string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string +const _escapeHTML = (text) => { + return text + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +}; + const _displayItem = (item, searchTerms, highlightTerms) => { const docBuilder = DOCUMENTATION_OPTIONS.BUILDER; const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX; @@ -90,25 +100,30 @@ const _displayItem = (item, searchTerms, highlightTerms) => { let linkEl = listItem.appendChild(document.createElement("a")); linkEl.href = linkUrl + anchor; linkEl.dataset.score = score; - linkEl.innerHTML = title; + linkEl.innerHTML = _escapeHTML(title); if (descr) { listItem.appendChild(document.createElement("span")).innerHTML = - " (" + descr + ")"; + ` (${_escapeHTML(descr)})`; // highlight search terms in the description - if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js - highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); - } - else if (showSearchSummary) + if (SPHINX_HIGHLIGHT_ENABLED) + // SPHINX_HIGHLIGHT_ENABLED is set in sphinx_highlight.js + highlightTerms.forEach((term) => + _highlightText(listItem, term, "highlighted"), + ); + } else if (showSearchSummary) fetch(requestUrl) .then((responseData) => responseData.text()) .then((data) => { if (data) listItem.appendChild( - Search.makeSearchSummary(data, searchTerms, anchor) + Search.makeSearchSummary(data, searchTerms, anchor), ); // highlight search terms in the summary - if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js - highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + if (SPHINX_HIGHLIGHT_ENABLED) + // SPHINX_HIGHLIGHT_ENABLED is set in sphinx_highlight.js + highlightTerms.forEach((term) => + _highlightText(listItem, term, "highlighted"), + ); }); Search.output.appendChild(listItem); }; @@ -117,14 +132,14 @@ const _finishSearch = (resultCount) => { Search.title.innerText = _("Search Results"); if (!resultCount) Search.status.innerText = Documentation.gettext( - "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories." + "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories.", ); else Search.status.innerText = Documentation.ngettext( "Search finished, found one page matching the search query.", "Search finished, found ${resultCount} pages matching the search query.", resultCount, - ).replace('${resultCount}', resultCount); + ).replace("${resultCount}", resultCount); }; const _displayNextItem = ( results, @@ -138,7 +153,7 @@ const _displayNextItem = ( _displayItem(results.pop(), searchTerms, highlightTerms); setTimeout( () => _displayNextItem(results, resultCount, searchTerms, highlightTerms), - 5 + 5, ); } // search finished, update title and status message @@ -170,9 +185,10 @@ const _orderResultsByScoreThenName = (a, b) => { * This is the same as ``\W+`` in Python, preserving the surrogate pair area. */ if (typeof splitQuery === "undefined") { - var splitQuery = (query) => query + var splitQuery = (query) => + query .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu) - .filter(term => term) // remove remaining empty strings + .filter((term) => term); // remove remaining empty strings } /** @@ -184,16 +200,23 @@ const Search = { _pulse_status: -1, htmlToText: (htmlString, anchor) => { - const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html'); + const htmlElement = new DOMParser().parseFromString( + htmlString, + "text/html", + ); for (const removalQuery of [".headerlink", "script", "style"]) { - htmlElement.querySelectorAll(removalQuery).forEach((el) => { el.remove() }); + htmlElement.querySelectorAll(removalQuery).forEach((el) => { + el.remove(); + }); } if (anchor) { - const anchorContent = htmlElement.querySelector(`[role="main"] ${anchor}`); + const anchorContent = htmlElement.querySelector( + `[role="main"] ${anchor}`, + ); if (anchorContent) return anchorContent.textContent; console.warn( - `Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.` + `Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.`, ); } @@ -202,7 +225,7 @@ const Search = { if (docContent) return docContent.textContent; console.warn( - "Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template." + "Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template.", ); return ""; }, @@ -287,12 +310,8 @@ const Search = { const queryTermLower = queryTerm.toLowerCase(); // maybe skip this "word" - // stopwords array is from language_data.js - if ( - stopwords.indexOf(queryTermLower) !== -1 || - queryTerm.match(/^\d+$/) - ) - return; + // stopwords set is from language_data.js + if (stopwords.has(queryTermLower) || queryTerm.match(/^\d+$/)) return; // stem the word let word = stemmer.stemWord(queryTermLower); @@ -304,8 +323,12 @@ const Search = { } }); - if (SPHINX_HIGHLIGHT_ENABLED) { // set in sphinx_highlight.js - localStorage.setItem("sphinx_highlight_terms", [...highlightTerms].join(" ")) + if (SPHINX_HIGHLIGHT_ENABLED) { + // SPHINX_HIGHLIGHT_ENABLED is set in sphinx_highlight.js + localStorage.setItem( + "sphinx_highlight_terms", + [...highlightTerms].join(" "), + ); } // console.debug("SEARCH: searching for:"); @@ -318,7 +341,13 @@ const Search = { /** * execute search (requires search index to be loaded) */ - _performSearch: (query, searchTerms, excludedTerms, highlightTerms, objectTerms) => { + _performSearch: ( + query, + searchTerms, + excludedTerms, + highlightTerms, + objectTerms, + ) => { const filenames = Search._index.filenames; const docNames = Search._index.docnames; const titles = Search._index.titles; @@ -334,10 +363,15 @@ const Search = { const queryLower = query.toLowerCase().trim(); for (const [title, foundTitles] of Object.entries(allTitles)) { - if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) { + if ( + title.toLowerCase().trim().includes(queryLower) + && queryLower.length >= title.length / 2 + ) { for (const [file, id] of foundTitles) { - const score = Math.round(Scorer.title * queryLower.length / title.length); - const boost = titles[file] === title ? 1 : 0; // add a boost for document titles + const score = Math.round( + (Scorer.title * queryLower.length) / title.length, + ); + const boost = titles[file] === title ? 1 : 0; // add a boost for document titles normalResults.push([ docNames[file], titles[file] !== title ? `${titles[file]} > ${title}` : title, @@ -353,9 +387,9 @@ const Search = { // search for explicit entries in index directives for (const [entry, foundEntries] of Object.entries(indexEntries)) { - if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) { + if (entry.includes(queryLower) && queryLower.length >= entry.length / 2) { for (const [file, id, isMain] of foundEntries) { - const score = Math.round(100 * queryLower.length / entry.length); + const score = Math.round((100 * queryLower.length) / entry.length); const result = [ docNames[file], titles[file], @@ -376,11 +410,13 @@ const Search = { // lookup as object objectTerms.forEach((term) => - normalResults.push(...Search.performObjectSearch(term, objectTerms)) + normalResults.push(...Search.performObjectSearch(term, objectTerms)), ); // lookup as search terms in fulltext - normalResults.push(...Search.performTermsSearch(searchTerms, excludedTerms)); + normalResults.push( + ...Search.performTermsSearch(searchTerms, excludedTerms), + ); // let the scorer override scores with a custom scoring function if (Scorer.score) { @@ -401,7 +437,11 @@ const Search = { // note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept let seen = new Set(); results = results.reverse().reduce((acc, result) => { - let resultStr = result.slice(0, 4).concat([result[5]]).map(v => String(v)).join(','); + let resultStr = result + .slice(0, 4) + .concat([result[5]]) + .map((v) => String(v)) + .join(","); if (!seen.has(resultStr)) { acc.push(result); seen.add(resultStr); @@ -413,8 +453,20 @@ const Search = { }, query: (query) => { - const [searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms] = Search._parseQuery(query); - const results = Search._performSearch(searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms); + const [ + searchQuery, + searchTerms, + excludedTerms, + highlightTerms, + objectTerms, + ] = Search._parseQuery(query); + const results = Search._performSearch( + searchQuery, + searchTerms, + excludedTerms, + highlightTerms, + objectTerms, + ); // for debugging //Search.lastresults = results.slice(); // a copy @@ -437,7 +489,7 @@ const Search = { const results = []; const objectSearchCallback = (prefix, match) => { - const name = match[4] + const name = match[4]; const fullname = (prefix ? prefix + "." : "") + name; const fullnameLower = fullname.toLowerCase(); if (fullnameLower.indexOf(object) < 0) return; @@ -489,9 +541,7 @@ const Search = { ]); }; Object.keys(objects).forEach((prefix) => - objects[prefix].forEach((array) => - objectSearchCallback(prefix, array) - ) + objects[prefix].forEach((array) => objectSearchCallback(prefix, array)), ); return results; }, @@ -516,8 +566,14 @@ const Search = { // find documents, if any, containing the query word in their text/title term indices // use Object.hasOwnProperty to avoid mismatching against prototype properties const arr = [ - { files: terms.hasOwnProperty(word) ? terms[word] : undefined, score: Scorer.term }, - { files: titleTerms.hasOwnProperty(word) ? titleTerms[word] : undefined, score: Scorer.title }, + { + files: terms.hasOwnProperty(word) ? terms[word] : undefined, + score: Scorer.term, + }, + { + files: titleTerms.hasOwnProperty(word) ? titleTerms[word] : undefined, + score: Scorer.title, + }, ]; // add support for partial matches if (word.length > 2) { @@ -558,7 +614,8 @@ const Search = { // create the mapping files.forEach((file) => { if (!fileMap.has(file)) fileMap.set(file, [word]); - else if (fileMap.get(file).indexOf(word) === -1) fileMap.get(file).push(word); + else if (fileMap.get(file).indexOf(word) === -1) + fileMap.get(file).push(word); }); }); @@ -569,11 +626,11 @@ const Search = { // as search terms with length < 3 are discarded const filteredTermCount = [...searchTerms].filter( - (term) => term.length > 2 + (term) => term.length > 2, ).length; if ( - wordList.length !== searchTerms.size && - wordList.length !== filteredTermCount + wordList.length !== searchTerms.size + && wordList.length !== filteredTermCount ) continue; @@ -581,10 +638,10 @@ const Search = { if ( [...excludedTerms].some( (term) => - terms[term] === file || - titleTerms[term] === file || - (terms[term] || []).includes(file) || - (titleTerms[term] || []).includes(file) + terms[term] === file + || titleTerms[term] === file + || (terms[term] || []).includes(file) + || (titleTerms[term] || []).includes(file), ) ) break; @@ -626,7 +683,8 @@ const Search = { let summary = document.createElement("p"); summary.classList.add("context"); - summary.textContent = top + text.substr(startWithContext, 240).trim() + tail; + summary.textContent = + top + text.substr(startWithContext, 240).trim() + tail; return summary; }, diff --git a/docs/_static/sphinx_highlight.js b/docs/_static/sphinx_highlight.js index 8a96c69..a74e103 100644 --- a/docs/_static/sphinx_highlight.js +++ b/docs/_static/sphinx_highlight.js @@ -1,7 +1,7 @@ /* Highlighting utilities for Sphinx HTML documentation. */ "use strict"; -const SPHINX_HIGHLIGHT_ENABLED = true +const SPHINX_HIGHLIGHT_ENABLED = true; /** * highlight a given string on a node by wrapping it in @@ -13,9 +13,9 @@ const _highlight = (node, addItems, text, className) => { const parent = node.parentNode; const pos = val.toLowerCase().indexOf(text); if ( - pos >= 0 && - !parent.classList.contains(className) && - !parent.classList.contains("nohighlight") + pos >= 0 + && !parent.classList.contains(className) + && !parent.classList.contains("nohighlight") ) { let span; @@ -30,13 +30,7 @@ const _highlight = (node, addItems, text, className) => { span.appendChild(document.createTextNode(val.substr(pos, text.length))); const rest = document.createTextNode(val.substr(pos + text.length)); - parent.insertBefore( - span, - parent.insertBefore( - rest, - node.nextSibling - ) - ); + parent.insertBefore(span, parent.insertBefore(rest, node.nextSibling)); node.nodeValue = val.substr(0, pos); /* There may be more occurrences of search term in this node. So call this * function recursively on the remaining fragment. @@ -46,7 +40,7 @@ const _highlight = (node, addItems, text, className) => { if (isInSVG) { const rect = document.createElementNS( "http://www.w3.org/2000/svg", - "rect" + "rect", ); const bbox = parent.getBBox(); rect.x.baseVal.value = bbox.x; @@ -65,7 +59,7 @@ const _highlightText = (thisNode, text, className) => { let addItems = []; _highlight(thisNode, addItems, text, className); addItems.forEach((obj) => - obj.parent.insertAdjacentElement("beforebegin", obj.target) + obj.parent.insertAdjacentElement("beforebegin", obj.target), ); }; @@ -73,25 +67,31 @@ const _highlightText = (thisNode, text, className) => { * Small JavaScript module for the documentation. */ const SphinxHighlight = { - /** * highlight the search words provided in localstorage in the text */ highlightSearchWords: () => { - if (!SPHINX_HIGHLIGHT_ENABLED) return; // bail if no highlight + if (!SPHINX_HIGHLIGHT_ENABLED) return; // bail if no highlight // get and clear terms from localstorage const url = new URL(window.location); const highlight = - localStorage.getItem("sphinx_highlight_terms") - || url.searchParams.get("highlight") - || ""; - localStorage.removeItem("sphinx_highlight_terms") - url.searchParams.delete("highlight"); - window.history.replaceState({}, "", url); + localStorage.getItem("sphinx_highlight_terms") + || url.searchParams.get("highlight") + || ""; + localStorage.removeItem("sphinx_highlight_terms"); + // Update history only if '?highlight' is present; otherwise it + // clears text fragments (not set in window.location by the browser) + if (url.searchParams.has("highlight")) { + url.searchParams.delete("highlight"); + window.history.replaceState({}, "", url); + } // get individual terms from highlight string - const terms = highlight.toLowerCase().split(/\s+/).filter(x => x); + const terms = highlight + .toLowerCase() + .split(/\s+/) + .filter((x) => x); if (terms.length === 0) return; // nothing to do // There should never be more than one element matching "div.body" @@ -107,11 +107,11 @@ const SphinxHighlight = { document .createRange() .createContextualFragment( - '" - ) + '", + ), ); }, @@ -125,7 +125,7 @@ const SphinxHighlight = { document .querySelectorAll("span.highlighted") .forEach((el) => el.classList.remove("highlighted")); - localStorage.removeItem("sphinx_highlight_terms") + localStorage.removeItem("sphinx_highlight_terms"); }, initEscapeListener: () => { @@ -134,10 +134,15 @@ const SphinxHighlight = { document.addEventListener("keydown", (event) => { // bail for input elements - if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) + return; // bail with special keys - if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return; - if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) { + if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) + return; + if ( + DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + && event.key === "Escape" + ) { SphinxHighlight.hideSearchWords(); event.preventDefault(); } diff --git a/docs/genindex.html b/docs/genindex.html index 0d172ee..da06dbd 100644 --- a/docs/genindex.html +++ b/docs/genindex.html @@ -4,16 +4,16 @@ - Index — Select AI for Python 1.4.0 documentation + Index — Select AI for Python 1.5.0 documentation + - - + - - - + + + @@ -77,7 +77,10 @@

Navigation

  • index
  • -
  • Python logo
  • +
  • + modules |
  • +
  • Python logo
  • Python »
  • @@ -85,7 +88,7 @@

    Navigation

  • - Select AI for Python 1.4.0 documentation » + Select AI for Python 1.5.0 documentation »
  • @@ -131,23 +134,66 @@

    Index

    | G | H | I + | J + | K | L + | M | N | O | P + | Q | R | S | T + | U | V + | W + | Z

    A

    +
  • add_tag() (select_ai.AsyncConversation method) + +
  • +
  • additional_instructions (select_ai.ProfileAttributes attribute) +
  • Agent (class in select_ai.agent)
  • +
  • agent_name (select_ai.agent.history.TaskHistoryEvent attribute) + +
  • AgentAttributes (class in select_ai.agent)
  • -
  • AnthropicProvider (class in select_ai) +
  • AgentAttributesEmptyError
  • -
  • AsyncAgent (class in select_ai.agent) +
  • AgentNotFoundError +
  • +
  • agents (select_ai.agent.TeamAttributes attribute) +
  • +
  • AgentTaskAttributesEmptyError +
  • +
  • AgentTaskNotFoundError +
  • +
  • AgentTeamAttributesEmptyError +
  • +
  • AgentTeamNotFoundError +
  • +
  • AgentToolAttributesEmptyError
  • GoogleProvider (class in select_ai)
  • +
  • grant_access() (select_ai.agent.AsyncTeam method) + +
  • H

    +
    @@ -544,234 +1126,1646 @@

    I

  • (select_ai.agent.Team class method)
  • - - - -

    L

    - - -
    - -

    N

    - - -
    +
  • instruction (select_ai.agent.TaskAttributes attribute) -

    O

    - - - -
    +
  • InvalidSQLError
  • -
  • OracleVectorIndexAttributes (class in select_ai) +
  • invocation_id (select_ai.agent.history.ToolHistoryEvent attribute)
  • -
    +
  • isalnum() (select_ai.action.Action method) -

    P

    - - - -
    - -

    R

    - - - -
    - -

    S

    - - -
    +
  • (select_ai.vector_index.VectorDistanceMetric method) +
  • + +
  • isnumeric() (select_ai.action.Action method) -

    T

    - - +
    + +

    J

    + + +
    + +

    K

    + + +
    + +

    L

    + + + +
    + +

    M

    + + + +
    + +

    N

    + + + +
    + +

    O

    + + + +
    + +

    P

    + + + +
    + +

    Q

    + + +
    + +

    R

    + + + +
    + +

    S

    + + + +
    + +

    T

    + + +
    + +

    U

    + + +

    V

    +
    + +

    W

    + + +
    + +

    Z

    + +
    @@ -803,7 +2797,10 @@

    Navigation

  • index
  • -
  • Python logo
  • +
  • + modules |
  • +
  • Python logo
  • Python »
  • @@ -811,7 +2808,7 @@

    Navigation

  • - Select AI for Python 1.4.0 documentation » + Select AI for Python 1.5.0 documentation »
  • @@ -856,7 +2853,7 @@

    Navigation


    - Created using Sphinx 8.2.3. + Created using Sphinx 9.1.0. diff --git a/docs/index.html b/docs/index.html index 572c59f..d2d7c65 100644 --- a/docs/index.html +++ b/docs/index.html @@ -5,16 +5,16 @@ - select_ai documentation — Select AI for Python 1.4.0 documentation + select_ai documentation — Select AI for Python 1.5.0 documentation + - - + - - - + + + @@ -68,6 +68,7 @@

    Table of Contents

  • Privileges
  • Provider
  • Credential
  • +
  • Sharing and ownership
  • Profile Attributes
  • Profile
  • Async Profile
  • @@ -77,9 +78,11 @@

    Table of Contents

  • Summary
  • AI Agent
  • Async AI Agent
  • +
  • A2A Integration
  • Command Line Interface
  • Web Frameworks
  • Concurrent Prompt Processing
  • +
  • API Reference
  • @@ -115,10 +118,13 @@

    Navigation

  • index
  • +
  • + modules |
  • next |
  • -
  • Python logo
  • +
  • Python logo
  • Python »
  • @@ -126,7 +132,7 @@

    Navigation

  • - Select AI for Python 1.4.0 documentation » + Select AI for Python 1.5.0 documentation »
  • @@ -191,6 +197,19 @@

    Getting Started3.5. Wallet connections +
  • 4. Run your first SQL query +
  • +
  • 5. Compatibility and requirements
  • +
  • 6. What’s new in 1.5.0 +
  • @@ -266,9 +285,15 @@

    Provider
  • 1.11.2. Enable using Async API
  • -
  • 1.12. Disable AI service provider @@ -281,13 +306,30 @@

    Credential + + +
    +

    Sharing and ownership

    +

  • 1.2. Attribute groups
  • 1.3. Object list examples
  • -
  • 1.4. Generation controls
  • @@ -356,19 +408,31 @@

    Async Profile2.2. Async Profile creation
  • 2.3. Reuse Async Profile
  • -
  • 2.4. Update Async Profile
  • -
  • 2.5. Delete Async Profile
  • -
  • 2.6. Async explain SQL
  • -
  • 2.7. Async run SQL
  • -
  • 2.8. Async show SQL
  • -
  • 2.9. Async show prompt
  • -
  • 2.10. Async concurrent SQL
  • -
  • 2.11. Async chat
  • -
  • 2.12. Async streaming chat
  • -
  • 2.13. Summarize
  • -
  • 2.14. Translate
  • -
  • 2.15. Async pipeline
  • -
  • 2.16. List profiles asynchronously
  • +
  • 2.4. Enable and disable AsyncProfile
  • +
  • 2.5. Update Async Profile
  • +
  • 2.6. Delete Async Profile
  • +
  • 2.7. Async explain SQL
  • +
  • 2.8. Async run SQL
  • +
  • 2.9. Async show SQL
  • +
  • 2.10. Async show prompt
  • +
  • 2.11. Async concurrent SQL
  • +
  • 2.12. Async chat
  • +
  • 2.13. Async streaming chat
  • +
  • 2.14. Async request-level profile attribute overrides
  • +
  • 2.15. Summarize
  • +
  • 2.16. Translate
  • +
  • 2.17. Async pipeline +
  • +
  • 2.18. Share a profile asynchronously
  • +
  • 2.19. Give async feedback on generated SQL
  • +
  • 2.20. List profiles asynchronously +
  • @@ -380,37 +444,65 @@

    Conversation1. Conversation Object model
  • 2. ConversationAttributes
  • 3. Conversation API
  • 4. AsyncConversation API
  • @@ -423,7 +515,29 @@

    Vector Index1. Vector Index
  • 2. VectorIndex Object Model
  • 3. VectorIndexAttributes @@ -465,7 +582,10 @@

    Vector IndexAsyncVectorIndex.get_attributes()

  • AsyncVectorIndex.get_next_refresh_timestamp()
  • AsyncVectorIndex.get_profile()
  • +
  • AsyncVectorIndex.grant_access()
  • AsyncVectorIndex.list()
  • +
  • AsyncVectorIndex.qualified_name
  • +
  • AsyncVectorIndex.revoke_access()
  • AsyncVectorIndex.set_attribute()
  • AsyncVectorIndex.set_attributes()
  • @@ -474,7 +594,10 @@

    Vector Index5.2. Async list vector index
  • 5.3. Async fetch vector index
  • 5.4. Async update vector index attributes
  • -
  • 5.5. Async RAG using vector index
  • +
  • 5.5. Async RAG using vector index +
  • @@ -555,10 +678,20 @@

    AI Agent
  • 1.3. Create Tool
  • 1.4. List Tools
  • +
  • 1.5. Inspect and run a tool
  • 2. Task +
  • +
  • 5. Async agent history
  • -
  • 5. Async AI agent examples + + +
    +

    A2A Integration

    +
  • 1.3. SQL commands
  • 1.4. Profile commands
  • -
  • 1.5. Command summary
  • +
  • 1.5. A2A commands +
  • +
  • 1.6. Command summary
  • @@ -767,6 +1001,54 @@

    Concurrent Prompt Processing +

    API Reference

    + @@ -785,6 +1067,7 @@

    Table of Contents

  • Privileges
  • Provider
  • Credential
  • +
  • Sharing and ownership
  • Profile Attributes
  • Profile
  • Async Profile
  • @@ -794,9 +1077,11 @@

    Table of Contents

  • Summary
  • AI Agent
  • Async AI Agent
  • +
  • A2A Integration
  • Command Line Interface
  • Web Frameworks
  • Concurrent Prompt Processing
  • +
  • API Reference
  • @@ -832,10 +1117,13 @@

    Navigation

  • index
  • +
  • + modules |
  • next |
  • -
  • Python logo
  • +
  • Python logo
  • Python »
  • @@ -843,7 +1131,7 @@

    Navigation

  • - Select AI for Python 1.4.0 documentation » + Select AI for Python 1.5.0 documentation »
  • @@ -888,7 +1176,7 @@

    Navigation


    - Created using Sphinx 8.2.3. + Created using Sphinx 9.1.0. diff --git a/docs/license.html b/docs/license.html index 53749f5..d7287fe 100644 --- a/docs/license.html +++ b/docs/license.html @@ -5,16 +5,16 @@ - <no title> — Select AI for Python 1.4.0 documentation + <no title> — Select AI for Python 1.5.0 documentation + - - + - - - + + + @@ -85,7 +85,7 @@

    Navigation

  • index
  • -
  • Python logo
  • +
  • Python logo
  • Python »
  • @@ -93,7 +93,7 @@

    Navigation

  • - Select AI for Python 1.4.0 documentation » + Select AI for Python 1.5.0 documentation »
  • @@ -194,7 +194,7 @@

    Navigation

  • index
  • -
  • Python logo
  • +
  • Python logo
  • Python »
  • @@ -202,7 +202,7 @@

    Navigation

  • - Select AI for Python 1.4.0 documentation » + Select AI for Python 1.5.0 documentation »
  • @@ -247,7 +247,7 @@

    Navigation


    - Created using Sphinx 8.2.3. + Created using Sphinx 9.1.0. diff --git a/docs/objects.inv b/docs/objects.inv index fa15d2a..53cd01b 100644 Binary files a/docs/objects.inv and b/docs/objects.inv differ diff --git a/docs/py-modindex.html b/docs/py-modindex.html new file mode 100644 index 0000000..46e2f10 --- /dev/null +++ b/docs/py-modindex.html @@ -0,0 +1,238 @@ + + + + + + + Python Module Index — Select AI for Python 1.5.0 documentation + + + + + + + + + + + + + + + + + + + + + + +
    + + + +
    + + + +
    +
    +
    +
    + + +

    Python Module Index

    + +
    + s +
    + + + + + + + + + + +
     
    + s
    + select_ai +
        + select_ai.errors +
    + + +
    +
    +
    +
    + +
    +
    + + + + + diff --git a/docs/release_notes.html b/docs/release_notes.html new file mode 100644 index 0000000..133c05c --- /dev/null +++ b/docs/release_notes.html @@ -0,0 +1,340 @@ + + + + + + + + 6. What’s new in 1.5.0 — Select AI for Python 1.5.0 documentation + + + + + + + + + + + + + + + + + + + + + +
    + + + +
    + + + +
    +
    +
    +
    + +
    +

    6. What’s new in 1.5.0

    +

    Version 1.5.0 adds A2A integration, object sharing, request-level profile +overrides, and typed agent history APIs. The changes below are relative to +1.4.1.

    +
    +

    6.1. New and expanded APIs

    +
      +
    • Expose an agent team with the optional A2A server. Choose fixed +or dynamic connections and standalone or clustered deployment. A2A includes +Oracle-backed task/context storage, task polling, A2UI connection forms, and +A2A 1.0/v0.3 JSON-RPC compatibility.

    • +
    • Share profiles, vector indexes, teams, and credentials +with database users or roles. Profile and vector-index fetch/list APIs +accept owner and expose qualified_name.

    • +
    • Pass an attributes mapping to profile and session actions for +request-only overrides. Async pipelines +can apply the same override mapping to every operation.

    • +
    • Inspect agent execution with TeamHistory, TaskHistory, and ToolHistory and their async equivalents.

    • +
    • Enable and disable profiles without deleting them. Configure +additional_instructions, integer seed, and translation language +defaults with ProfileAttributes. Per-call translation languages are +optional and can use the saved defaults.

    • +
    • Configure supervised teams with AgentAttributes.supervisor and +TeamAttributes.supervisor_agent; inspect the database-generated +supervisor_task after creation.

    • +
    +
    +
    +

    6.2. Upgrade considerations

    +
      +
    • Python 3.11 is now the minimum. Python 3.9 and 3.10 are no longer +supported. Supported versions are 3.11 through 3.14.

    • +
    • pandas is now pinned to 3.0.3, replacing 2.2.3. Check applications that +consume DataFrames for pandas compatibility when upgrading.

    • +
    • ProfileAttributes.seed is now typed as an integer. Supply an integer +rather than a numeric string.

    • +
    • Unspecified SyntheticDataParams values are omitted so the database can +apply its defaults. Set values explicitly if your application relies on +particular generation settings.

    • +
    • Install select_ai[a2a] to use A2A commands; select_ai[cli] covers +the other CLI commands. Prepare A2A database storage privileges as described +in A2A prerequisites.

    • +
    • --require-oauth requires a bearer token and derives an owner scope. +Token signatures and claims must be validated by a trusted upstream service; +the SDK flag alone does not verify identity. See A2A authentication.

    • +
    +

    Upgrade in your application’s virtual environment:

    +
    python -m pip install --upgrade 'select_ai==1.5.0'
    +# If you use A2A:
    +python -m pip install --upgrade 'select_ai[a2a]==1.5.0'
    +
    +
    +

    See Compatibility for the feature requirements and +Run your first SQL query for a complete example.

    +
    +
    + + +
    +
    +
    +
    + +
    +
    + + + + + diff --git a/docs/search.html b/docs/search.html index 6943c58..f72eeba 100644 --- a/docs/search.html +++ b/docs/search.html @@ -4,17 +4,17 @@ - Search — Select AI for Python 1.4.0 documentation + Search — Select AI for Python 1.5.0 documentation + - - + - - - + + + @@ -67,7 +67,10 @@

    Navigation

  • index
  • -
  • Python logo
  • +
  • + modules |
  • +
  • Python logo
  • Python »
  • @@ -75,7 +78,7 @@

    Navigation

  • - Select AI for Python 1.4.0 documentation » + Select AI for Python 1.5.0 documentation »
  • @@ -141,7 +144,10 @@

    Navigation

  • index
  • -
  • Python logo
  • +
  • + modules |
  • +
  • Python logo
  • Python »
  • @@ -149,7 +155,7 @@

    Navigation

  • - Select AI for Python 1.4.0 documentation » + Select AI for Python 1.5.0 documentation »
  • @@ -183,7 +189,7 @@

    Navigation


    - Created using Sphinx 8.2.3. + Created using Sphinx 9.1.0. diff --git a/docs/searchindex.js b/docs/searchindex.js index af63835..3193ea5 100644 --- a/docs/searchindex.js +++ b/docs/searchindex.js @@ -1 +1 @@ -Search.setIndex({"alltitles":{"AI Agent":[[0,"ai-agent"]],"AI agent examples":[[3,"ai-agent-examples"]],"AI provider and expected credential format":[[10,null]],"AWSProvider":[[16,"awsprovider"]],"Action methods":[[2,"action-methods"]],"Action to method mapping":[[2,"id2"]],"Actions":[[0,"actions"]],"Agent":[[3,"id1"]],"AnthropicProvider":[[16,"anthropicprovider"]],"Async AI Agent":[[0,"async-ai-agent"]],"Async AI agent examples":[[4,"async-ai-agent-examples"]],"Async API":[[10,"async-api"],[10,"id2"]],"Async Profile":[[0,"async-profile"]],"Async Profile creation":[[5,"async-profile-creation"]],"Async RAG using vector index":[[19,"async-rag-using-vector-index"]],"Async chat":[[5,"async-chat"]],"Async chat session":[[9,"async-chat-session"]],"Async concurrent SQL":[[5,"async-concurrent-sql"]],"Async create vector index":[[19,"async-create-vector-index"]],"Async explain SQL":[[5,"async-explain-sql"]],"Async fetch vector index":[[19,"async-fetch-vector-index"]],"Async list conversations":[[9,"async-list-conversations"]],"Async list vector index":[[19,"async-list-vector-index"]],"Async pipeline":[[5,"async-pipeline"]],"Async profile actions":[[5,"async-profile-actions"]],"Async profile lifecycle":[[5,null]],"Async run SQL":[[5,"async-run-sql"]],"Async show SQL":[[5,"async-show-sql"]],"Async show prompt":[[5,"async-show-prompt"]],"Async streaming chat":[[5,"async-streaming-chat"]],"Async summary":[[17,"async-summary"]],"Async update vector index attributes":[[19,"async-update-vector-index-attributes"]],"AsyncAgent":[[4,"asyncagent"]],"AsyncConversation API":[[9,"asyncconversation-api"]],"AsyncProfile API":[[5,"asyncprofile-api"]],"AsyncTask":[[4,"asynctask"]],"AsyncTeam":[[4,"asyncteam"]],"AsyncTool":[[4,"asynctool"]],"AsyncVectorIndex API":[[19,"asyncvectorindex-api"]],"Asynchronous connection":[[8,"asynchronous-connection"]],"Attribute groups":[[15,"attribute-groups"]],"AzureProvider":[[16,"azureprovider"]],"Base Profile API":[[14,"base-profile-api"]],"CLI command summary":[[6,"id3"]],"Chat":[[14,"chat"]],"Chat REPL commands":[[6,"id2"]],"Chat session":[[9,"chat-session"]],"Choosing an action":[[2,"choosing-an-action"]],"ChunkProcessingMethod":[[17,"chunkprocessingmethod"]],"CohereProvider":[[16,"cohereprovider"]],"Command Line Interface":[[0,"command-line-interface"]],"Command line interface":[[6,null]],"Command summary":[[6,"command-summary"]],"Common async profile actions":[[5,"id1"]],"Common profile actions":[[14,"id1"]],"Common required attributes":[[15,"common-required-attributes"]],"Concurrent Prompt Processing":[[0,"concurrent-prompt-processing"]],"Concurrent prompt processing":[[7,null]],"Connecting to Oracle Database":[[8,null]],"Connection Pool":[[8,"connection-pool"]],"Connection health":[[8,"connection-health"]],"Connection options":[[6,"connection-options"],[6,"id1"]],"Connection smoke test":[[11,"connection-smoke-test"]],"Content from a URI":[[17,"content-from-a-uri"]],"Content from object storage":[[17,"content-from-object-storage"]],"Conversation":[[0,"conversation"]],"Conversation API":[[9,"conversation-api"]],"Conversation Object model":[[9,null]],"ConversationAttributes":[[9,"conversationattributes"]],"Core concepts":[[12,"core-concepts"]],"Create Agent":[[3,"create-agent"],[4,"create-agent"]],"Create Profile":[[14,"create-profile"]],"Create Task":[[3,"create-task"],[4,"create-task"]],"Create Tool":[[3,"create-tool"],[4,"create-tool"]],"Create conversation":[[9,"create-conversation"]],"Create credential":[[10,"create-credential"]],"Create vector index":[[19,"create-vector-index"]],"Credential":[[0,"credential"]],"Delete Async Profile":[[5,"delete-async-profile"]],"Delete Profile":[[14,"delete-profile"]],"Delete conversation":[[9,"delete-conversation"]],"Delete credential":[[10,"delete-credential"]],"Delete vector index":[[19,"delete-vector-index"]],"Disable AI service provider":[[16,"disable-ai-service-provider"]],"Disable using Async API":[[16,"disable-using-async-api"]],"Disable using Sync API":[[16,"disable-using-sync-api"]],"Django example":[[20,"django-example"]],"Enable AI service provider":[[16,"enable-ai-service-provider"]],"Enable using Async API":[[16,"enable-using-async-api"]],"Enable using Sync API":[[16,"enable-using-sync-api"]],"Environment variables":[[7,"environment-variables"]],"Examples":[[2,"examples"],[16,"examples"]],"Explain SQL":[[14,"explain-sql"]],"Export and Import Team":[[3,"export-and-import-team"],[4,"export-and-import-team"]],"ExtractivenessLevel":[[17,"extractivenesslevel"]],"FastAPI asynchronous endpoints":[[20,"fastapi-asynchronous-endpoints"]],"FastAPI synchronous endpoints":[[20,"fastapi-synchronous-endpoints"]],"Fetch vector index":[[19,"fetch-vector-index"]],"Flask example":[[20,"flask-example"]],"Framework patterns":[[20,"framework-patterns"]],"Generation controls":[[15,"generation-controls"]],"Generation modes":[[18,null]],"Generation parameters":[[18,"generation-parameters"]],"Getting Started":[[0,"getting-started"]],"GoogleProvider":[[16,"googleprovider"]],"Grant network access":[[13,"grant-network-access"]],"Grant privilege":[[13,null]],"HuggingFaceProvider":[[16,"huggingfaceprovider"]],"Inline content":[[17,null]],"Install dependencies":[[20,"install-dependencies"]],"Install documentation dependencies":[[11,"install-documentation-dependencies"]],"Installation requirements":[[11,"installation-requirements"]],"Installing select_ai":[[11,null]],"Interactive chat":[[6,"interactive-chat"]],"Introduction to Select AI for Python":[[12,null]],"Lifecycle helpers":[[3,"lifecycle-helpers"],[4,"lifecycle-helpers"]],"List Agents":[[4,"list-agents"]],"List Tasks":[[3,"list-tasks"],[4,"list-tasks"]],"List Teams":[[4,"list-teams"]],"List Tools":[[3,"list-tools"],[4,"list-tools"]],"List conversations":[[9,"list-conversations"]],"List profiles":[[14,"list-profiles"]],"List profiles asynchronously":[[5,"list-profiles-asynchronously"]],"List vector index":[[19,"list-vector-index"]],"Multi table Async API":[[18,"multi-table-async-api"]],"Multi table Sync API":[[18,"multi-table-sync-api"]],"Multi table synthetic data":[[18,"multi-table-synthetic-data"]],"Narrate":[[14,"narrate"]],"OCIGenAIProvider":[[16,"ocigenaiprovider"]],"Object list examples":[[15,"object-list-examples"]],"OpenAIProvider":[[16,"openaiprovider"]],"OracleVectorIndexAttributes":[[19,"oraclevectorindexattributes"]],"Pool sizing":[[7,"pool-sizing"],[20,"pool-sizing"]],"Pool wait behavior":[[20,"pool-wait-behavior"]],"Privileges":[[0,"privileges"]],"Profile":[[0,"profile"]],"Profile API":[[14,"profile-api"]],"Profile Attributes":[[0,"profile-attributes"]],"Profile Object Model":[[14,"profile-object-model"]],"Profile actions":[[14,"profile-actions"]],"Profile attribute groups":[[15,"id1"]],"Profile commands":[[6,"profile-commands"]],"Profile lifecycle":[[14,null]],"ProfileAttributes":[[15,null]],"Provider":[[0,"provider"],[16,null],[16,"id2"]],"Provider classes":[[16,"id3"]],"RAG using vector index":[[19,"rag-using-vector-index"]],"Recipe summary":[[7,"recipe-summary"]],"Request handling":[[20,"request-handling"]],"Reuse Async Profile":[[5,"reuse-async-profile"]],"Reuse Profile":[[14,"reuse-profile"]],"Revoke network access":[[13,"revoke-network-access"]],"Revoke privilege":[[13,"revoke-privilege"]],"Run SQL":[[14,"run-sql"]],"Run Team":[[3,"run-team"],[4,"run-team"]],"SQL commands":[[6,"sql-commands"]],"Select AI Actions":[[2,"id1"]],"Select AI Agent Tools":[[3,"id2"]],"Select AI Async Agent Tools":[[4,"id2"]],"Show Prompt":[[14,"show-prompt"]],"Show SQL":[[14,"show-sql"]],"Single Table Async API":[[18,"single-table-async-api"]],"Single Table Sync API":[[18,"single-table-sync-api"]],"Single table synthetic data":[[18,"single-table-synthetic-data"]],"Streaming":[[2,"streaming"]],"Streaming chat":[[14,"streaming-chat"]],"Summarize":[[5,"summarize"],[14,"summarize"]],"Summary":[[0,"summary"]],"Summary parameters":[[17,"summary-parameters"]],"SummaryParams":[[17,"summaryparams"]],"SummaryStyle":[[17,"summarystyle"]],"Supported Actions":[[2,null]],"Supported Python versions":[[12,"supported-python-versions"]],"Supported Tools":[[3,"supported-tools"]],"Sync API":[[10,"sync-api"],[10,"id1"]],"Sync and async APIs":[[18,"sync-and-async-apis"]],"Sync and async agent APIs":[[4,null]],"Synchronous and asynchronous APIs":[[12,"synchronous-and-asynchronous-apis"]],"Synchronous connection":[[8,"synchronous-connection"]],"Synthetic Data":[[0,"synthetic-data"]],"SyntheticDataAttributes":[[18,"syntheticdataattributes"]],"SyntheticDataParams":[[18,"syntheticdataparams"]],"Task":[[3,"task"]],"Team":[[3,"team"]],"Tool":[[3,null]],"Tool selection":[[3,"tool-selection"]],"Translate":[[5,"translate"],[14,"translate"]],"Update Async Profile":[[5,"update-async-profile"]],"Update Profile":[[14,"update-profile"]],"Update vector index attributes":[[19,"update-vector-index-attributes"]],"Using select_ai with Python web frameworks":[[20,null]],"Validation":[[17,"validation"]],"Vector Index":[[0,"vector-index"],[19,null]],"VectorIndex API":[[19,"vectorindex-api"]],"VectorIndex Object Model":[[19,"vectorindex-object-model"]],"VectorIndexAttributes":[[19,"vectorindexattributes"]],"Wallet connections":[[8,"wallet-connections"]],"Web Frameworks":[[0,"web-frameworks"]],"Web Search Agent using OpenAI\u2019s GPT model":[[3,"web-search-agent-using-openai-s-gpt-model"],[4,"web-search-agent-using-openai-s-gpt-model"]],"What you can build":[[12,"what-you-can-build"]],"When to use each tool":[[3,"id3"]],"async_as_completed.py":[[7,"async-as-completed-py"]],"async_gather.py":[[7,"async-gather-py"]],"async_pipeline.py":[[7,"async-pipeline-py"]],"async_queue_workers.py":[[7,"async-queue-workers-py"]],"select_ai documentation":[[0,null]],"select_ai installation":[[11,"select-ai-installation"]],"sync_ordered_results.py":[[7,"sync-ordered-results-py"]],"sync_queue_workers.py":[[7,"sync-queue-workers-py"]],"sync_thread_pool.py":[[7,"sync-thread-pool-py"]]},"docnames":["index","license","user_guide/actions","user_guide/agent","user_guide/async_agent","user_guide/async_profile","user_guide/cli","user_guide/concurrent_prompt_processing","user_guide/connection","user_guide/conversation","user_guide/credential","user_guide/installation","user_guide/introduction","user_guide/privileges","user_guide/profile","user_guide/profile_attributes","user_guide/provider","user_guide/summary","user_guide/synthetic_data","user_guide/vector_index","user_guide/web_frameworks"],"envversion":{"sphinx":65,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["index.rst","license.rst","user_guide/actions.rst","user_guide/agent.rst","user_guide/async_agent.rst","user_guide/async_profile.rst","user_guide/cli.rst","user_guide/concurrent_prompt_processing.rst","user_guide/connection.rst","user_guide/conversation.rst","user_guide/credential.rst","user_guide/installation.rst","user_guide/introduction.rst","user_guide/privileges.rst","user_guide/profile.rst","user_guide/profile_attributes.rst","user_guide/provider.rst","user_guide/summary.rst","user_guide/synthetic_data.rst","user_guide/vector_index.rst","user_guide/web_frameworks.rst"],"indexentries":{"add_negative_feedback() (select_ai.asyncprofile method)":[[5,"select_ai.AsyncProfile.add_negative_feedback",false]],"add_negative_feedback() (select_ai.profile method)":[[14,"select_ai.Profile.add_negative_feedback",false]],"add_positive_feedback() (select_ai.asyncprofile method)":[[5,"select_ai.AsyncProfile.add_positive_feedback",false]],"add_positive_feedback() (select_ai.profile method)":[[14,"select_ai.Profile.add_positive_feedback",false]],"agent (class in select_ai.agent)":[[3,"select_ai.agent.Agent",false]],"agentattributes (class in select_ai.agent)":[[3,"select_ai.agent.AgentAttributes",false]],"anthropicprovider (class in select_ai)":[[16,"select_ai.AnthropicProvider",false]],"asyncagent (class in select_ai.agent)":[[4,"select_ai.agent.AsyncAgent",false]],"asyncconversation (class in select_ai)":[[9,"select_ai.AsyncConversation",false]],"asyncprofile (class in select_ai)":[[5,"select_ai.AsyncProfile",false]],"asynctask (class in select_ai.agent)":[[4,"select_ai.agent.AsyncTask",false]],"asyncteam (class in select_ai.agent)":[[4,"select_ai.agent.AsyncTeam",false]],"asynctool (class in select_ai.agent)":[[4,"select_ai.agent.AsyncTool",false]],"asyncvectorindex (class in select_ai)":[[19,"select_ai.AsyncVectorIndex",false]],"awsprovider (class in select_ai)":[[16,"select_ai.AWSProvider",false]],"azureprovider (class in select_ai)":[[16,"select_ai.AzureProvider",false]],"baseprofile (class in select_ai)":[[14,"select_ai.BaseProfile",false]],"chat() (select_ai.asyncprofile method)":[[5,"select_ai.AsyncProfile.chat",false]],"chat() (select_ai.profile method)":[[14,"select_ai.Profile.chat",false]],"chat_session() (select_ai.asyncprofile method)":[[5,"select_ai.AsyncProfile.chat_session",false]],"chat_session() (select_ai.profile method)":[[14,"select_ai.Profile.chat_session",false]],"chunkprocessingmethod (class in select_ai.summary)":[[17,"select_ai.summary.ChunkProcessingMethod",false]],"cohereprovider (class in select_ai)":[[16,"select_ai.CohereProvider",false]],"conversation (class in select_ai)":[[9,"select_ai.Conversation",false]],"conversationattributes (class in select_ai)":[[9,"select_ai.ConversationAttributes",false]],"create() (select_ai.agent.agent method)":[[3,"select_ai.agent.Agent.create",false]],"create() (select_ai.agent.asyncagent method)":[[4,"select_ai.agent.AsyncAgent.create",false]],"create() (select_ai.agent.asynctask method)":[[4,"select_ai.agent.AsyncTask.create",false]],"create() (select_ai.agent.asyncteam method)":[[4,"select_ai.agent.AsyncTeam.create",false]],"create() (select_ai.agent.asynctool method)":[[4,"select_ai.agent.AsyncTool.create",false]],"create() (select_ai.agent.task method)":[[3,"select_ai.agent.Task.create",false]],"create() (select_ai.agent.team method)":[[3,"select_ai.agent.Team.create",false]],"create() (select_ai.asyncconversation method)":[[9,"select_ai.AsyncConversation.create",false]],"create() (select_ai.asyncprofile method)":[[5,"select_ai.AsyncProfile.create",false]],"create() (select_ai.asyncvectorindex method)":[[19,"select_ai.AsyncVectorIndex.create",false]],"create() (select_ai.conversation method)":[[9,"select_ai.Conversation.create",false]],"create() (select_ai.profile method)":[[14,"select_ai.Profile.create",false]],"create() (select_ai.vectorindex method)":[[19,"select_ai.VectorIndex.create",false]],"create_built_in_tool() (select_ai.agent.asynctool class method)":[[4,"select_ai.agent.AsyncTool.create_built_in_tool",false]],"create_built_in_tool() (select_ai.agent.tool class method)":[[3,"select_ai.agent.Tool.create_built_in_tool",false]],"create_email_notification_tool() (select_ai.agent.asynctool class method)":[[4,"select_ai.agent.AsyncTool.create_email_notification_tool",false]],"create_email_notification_tool() (select_ai.agent.tool class method)":[[3,"select_ai.agent.Tool.create_email_notification_tool",false]],"create_pl_sql_tool() (select_ai.agent.asynctool class method)":[[4,"select_ai.agent.AsyncTool.create_pl_sql_tool",false]],"create_pl_sql_tool() (select_ai.agent.tool class method)":[[3,"select_ai.agent.Tool.create_pl_sql_tool",false]],"create_rag_tool() (select_ai.agent.asynctool class method)":[[4,"select_ai.agent.AsyncTool.create_rag_tool",false]],"create_rag_tool() (select_ai.agent.tool class method)":[[3,"select_ai.agent.Tool.create_rag_tool",false]],"create_slack_notification_tool() (select_ai.agent.asynctool class method)":[[4,"select_ai.agent.AsyncTool.create_slack_notification_tool",false]],"create_slack_notification_tool() (select_ai.agent.tool class method)":[[3,"select_ai.agent.Tool.create_slack_notification_tool",false]],"create_sql_tool() (select_ai.agent.asynctool class method)":[[4,"select_ai.agent.AsyncTool.create_sql_tool",false]],"create_sql_tool() (select_ai.agent.tool class method)":[[3,"select_ai.agent.Tool.create_sql_tool",false]],"create_websearch_tool() (select_ai.agent.asynctool class method)":[[4,"select_ai.agent.AsyncTool.create_websearch_tool",false]],"create_websearch_tool() (select_ai.agent.tool class method)":[[3,"select_ai.agent.Tool.create_websearch_tool",false]],"delete() (select_ai.agent.agent method)":[[3,"select_ai.agent.Agent.delete",false]],"delete() (select_ai.agent.asyncagent method)":[[4,"select_ai.agent.AsyncAgent.delete",false]],"delete() (select_ai.agent.asynctask method)":[[4,"select_ai.agent.AsyncTask.delete",false]],"delete() (select_ai.agent.asyncteam method)":[[4,"select_ai.agent.AsyncTeam.delete",false]],"delete() (select_ai.agent.asynctool method)":[[4,"select_ai.agent.AsyncTool.delete",false]],"delete() (select_ai.agent.task method)":[[3,"select_ai.agent.Task.delete",false]],"delete() (select_ai.agent.team method)":[[3,"select_ai.agent.Team.delete",false]],"delete() (select_ai.agent.tool method)":[[3,"select_ai.agent.Tool.delete",false]],"delete() (select_ai.asyncconversation method)":[[9,"select_ai.AsyncConversation.delete",false]],"delete() (select_ai.asyncprofile method)":[[5,"select_ai.AsyncProfile.delete",false]],"delete() (select_ai.asyncvectorindex method)":[[19,"select_ai.AsyncVectorIndex.delete",false]],"delete() (select_ai.conversation method)":[[9,"select_ai.Conversation.delete",false]],"delete() (select_ai.profile method)":[[14,"select_ai.Profile.delete",false]],"delete() (select_ai.vectorindex method)":[[19,"select_ai.VectorIndex.delete",false]],"delete_agent() (select_ai.agent.agent class method)":[[3,"select_ai.agent.Agent.delete_agent",false]],"delete_agent() (select_ai.agent.asyncagent class method)":[[4,"select_ai.agent.AsyncAgent.delete_agent",false]],"delete_feedback() (select_ai.asyncprofile method)":[[5,"select_ai.AsyncProfile.delete_feedback",false]],"delete_feedback() (select_ai.profile method)":[[14,"select_ai.Profile.delete_feedback",false]],"delete_index() (select_ai.asyncvectorindex class method)":[[19,"select_ai.AsyncVectorIndex.delete_index",false]],"delete_index() (select_ai.vectorindex class method)":[[19,"select_ai.VectorIndex.delete_index",false]],"delete_profile() (select_ai.asyncprofile class method)":[[5,"select_ai.AsyncProfile.delete_profile",false]],"delete_profile() (select_ai.profile class method)":[[14,"select_ai.Profile.delete_profile",false]],"delete_task() (select_ai.agent.asynctask class method)":[[4,"select_ai.agent.AsyncTask.delete_task",false]],"delete_task() (select_ai.agent.task class method)":[[3,"select_ai.agent.Task.delete_task",false]],"delete_team() (select_ai.agent.asyncteam class method)":[[4,"select_ai.agent.AsyncTeam.delete_team",false]],"delete_team() (select_ai.agent.team class method)":[[3,"select_ai.agent.Team.delete_team",false]],"delete_tool() (select_ai.agent.asynctool class method)":[[4,"select_ai.agent.AsyncTool.delete_tool",false]],"delete_tool() (select_ai.agent.tool class method)":[[3,"select_ai.agent.Tool.delete_tool",false]],"disable() (select_ai.agent.agent method)":[[3,"select_ai.agent.Agent.disable",false]],"disable() (select_ai.agent.asyncagent method)":[[4,"select_ai.agent.AsyncAgent.disable",false]],"disable() (select_ai.agent.asynctask method)":[[4,"select_ai.agent.AsyncTask.disable",false]],"disable() (select_ai.agent.asyncteam method)":[[4,"select_ai.agent.AsyncTeam.disable",false]],"disable() (select_ai.agent.asynctool method)":[[4,"select_ai.agent.AsyncTool.disable",false]],"disable() (select_ai.agent.task method)":[[3,"select_ai.agent.Task.disable",false]],"disable() (select_ai.agent.team method)":[[3,"select_ai.agent.Team.disable",false]],"disable() (select_ai.agent.tool method)":[[3,"select_ai.agent.Tool.disable",false]],"disable() (select_ai.asyncvectorindex method)":[[19,"select_ai.AsyncVectorIndex.disable",false]],"disable() (select_ai.vectorindex method)":[[19,"select_ai.VectorIndex.disable",false]],"enable() (select_ai.agent.agent method)":[[3,"select_ai.agent.Agent.enable",false]],"enable() (select_ai.agent.asyncagent method)":[[4,"select_ai.agent.AsyncAgent.enable",false]],"enable() (select_ai.agent.asynctask method)":[[4,"select_ai.agent.AsyncTask.enable",false]],"enable() (select_ai.agent.asyncteam method)":[[4,"select_ai.agent.AsyncTeam.enable",false]],"enable() (select_ai.agent.asynctool method)":[[4,"select_ai.agent.AsyncTool.enable",false]],"enable() (select_ai.agent.task method)":[[3,"select_ai.agent.Task.enable",false]],"enable() (select_ai.agent.team method)":[[3,"select_ai.agent.Team.enable",false]],"enable() (select_ai.agent.tool method)":[[3,"select_ai.agent.Tool.enable",false]],"enable() (select_ai.asyncvectorindex method)":[[19,"select_ai.AsyncVectorIndex.enable",false]],"enable() (select_ai.vectorindex method)":[[19,"select_ai.VectorIndex.enable",false]],"explain_sql() (select_ai.asyncprofile method)":[[5,"select_ai.AsyncProfile.explain_sql",false]],"explain_sql() (select_ai.profile method)":[[14,"select_ai.Profile.explain_sql",false]],"export() (select_ai.agent.asyncteam method)":[[4,"select_ai.agent.AsyncTeam.export",false]],"export() (select_ai.agent.team method)":[[3,"select_ai.agent.Team.export",false]],"export_team() (select_ai.agent.asyncteam class method)":[[4,"select_ai.agent.AsyncTeam.export_team",false]],"export_team() (select_ai.agent.team class method)":[[3,"select_ai.agent.Team.export_team",false]],"extractivenesslevel (class in select_ai.summary)":[[17,"select_ai.summary.ExtractivenessLevel",false]],"fetch() (select_ai.agent.agent class method)":[[3,"select_ai.agent.Agent.fetch",false]],"fetch() (select_ai.agent.asyncagent class method)":[[4,"select_ai.agent.AsyncAgent.fetch",false]],"fetch() (select_ai.agent.asynctask class method)":[[4,"select_ai.agent.AsyncTask.fetch",false]],"fetch() (select_ai.agent.asyncteam class method)":[[4,"select_ai.agent.AsyncTeam.fetch",false]],"fetch() (select_ai.agent.asynctool class method)":[[4,"select_ai.agent.AsyncTool.fetch",false]],"fetch() (select_ai.agent.task class method)":[[3,"select_ai.agent.Task.fetch",false]],"fetch() (select_ai.agent.team class method)":[[3,"select_ai.agent.Team.fetch",false]],"fetch() (select_ai.agent.tool class method)":[[3,"select_ai.agent.Tool.fetch",false]],"fetch() (select_ai.asyncconversation class method)":[[9,"select_ai.AsyncConversation.fetch",false]],"fetch() (select_ai.asyncprofile class method)":[[5,"select_ai.AsyncProfile.fetch",false]],"fetch() (select_ai.asyncvectorindex class method)":[[19,"select_ai.AsyncVectorIndex.fetch",false]],"fetch() (select_ai.conversation class method)":[[9,"select_ai.Conversation.fetch",false]],"fetch() (select_ai.profile class method)":[[14,"select_ai.Profile.fetch",false]],"fetch() (select_ai.vectorindex class method)":[[19,"select_ai.VectorIndex.fetch",false]],"generate() (select_ai.asyncprofile method)":[[5,"select_ai.AsyncProfile.generate",false]],"generate() (select_ai.profile method)":[[14,"select_ai.Profile.generate",false]],"generate_synthetic_data() (select_ai.asyncprofile method)":[[5,"select_ai.AsyncProfile.generate_synthetic_data",false]],"generate_synthetic_data() (select_ai.profile method)":[[14,"select_ai.Profile.generate_synthetic_data",false]],"get_attributes() (select_ai.asyncconversation method)":[[9,"select_ai.AsyncConversation.get_attributes",false]],"get_attributes() (select_ai.asyncprofile method)":[[5,"select_ai.AsyncProfile.get_attributes",false]],"get_attributes() (select_ai.asyncvectorindex method)":[[19,"select_ai.AsyncVectorIndex.get_attributes",false]],"get_attributes() (select_ai.conversation method)":[[9,"select_ai.Conversation.get_attributes",false]],"get_attributes() (select_ai.profile method)":[[14,"select_ai.Profile.get_attributes",false]],"get_attributes() (select_ai.vectorindex method)":[[19,"select_ai.VectorIndex.get_attributes",false]],"get_next_refresh_timestamp() (select_ai.asyncvectorindex method)":[[19,"select_ai.AsyncVectorIndex.get_next_refresh_timestamp",false]],"get_next_refresh_timestamp() (select_ai.vectorindex method)":[[19,"select_ai.VectorIndex.get_next_refresh_timestamp",false]],"get_profile() (select_ai.asyncvectorindex method)":[[19,"select_ai.AsyncVectorIndex.get_profile",false]],"get_profile() (select_ai.vectorindex method)":[[19,"select_ai.VectorIndex.get_profile",false]],"googleprovider (class in select_ai)":[[16,"select_ai.GoogleProvider",false]],"huggingfaceprovider (class in select_ai)":[[16,"select_ai.HuggingFaceProvider",false]],"import_team() (select_ai.agent.asyncteam class method)":[[4,"select_ai.agent.AsyncTeam.import_team",false]],"import_team() (select_ai.agent.team class method)":[[3,"select_ai.agent.Team.import_team",false]],"list() (select_ai.agent.agent class method)":[[3,"select_ai.agent.Agent.list",false]],"list() (select_ai.agent.asyncagent class method)":[[4,"select_ai.agent.AsyncAgent.list",false]],"list() (select_ai.agent.asynctask class method)":[[4,"select_ai.agent.AsyncTask.list",false]],"list() (select_ai.agent.asyncteam class method)":[[4,"select_ai.agent.AsyncTeam.list",false]],"list() (select_ai.agent.asynctool class method)":[[4,"select_ai.agent.AsyncTool.list",false]],"list() (select_ai.agent.task class method)":[[3,"select_ai.agent.Task.list",false]],"list() (select_ai.agent.team class method)":[[3,"select_ai.agent.Team.list",false]],"list() (select_ai.agent.tool class method)":[[3,"select_ai.agent.Tool.list",false]],"list() (select_ai.asyncconversation class method)":[[9,"select_ai.AsyncConversation.list",false]],"list() (select_ai.asyncprofile class method)":[[5,"select_ai.AsyncProfile.list",false]],"list() (select_ai.asyncvectorindex class method)":[[19,"select_ai.AsyncVectorIndex.list",false]],"list() (select_ai.conversation class method)":[[9,"select_ai.Conversation.list",false]],"list() (select_ai.profile class method)":[[14,"select_ai.Profile.list",false]],"list() (select_ai.vectorindex class method)":[[19,"select_ai.VectorIndex.list",false]],"narrate() (select_ai.asyncprofile method)":[[5,"select_ai.AsyncProfile.narrate",false]],"narrate() (select_ai.profile method)":[[14,"select_ai.Profile.narrate",false]],"ocigenaiprovider (class in select_ai)":[[16,"select_ai.OCIGenAIProvider",false]],"openaiprovider (class in select_ai)":[[16,"select_ai.OpenAIProvider",false]],"oraclevectorindexattributes (class in select_ai)":[[19,"select_ai.OracleVectorIndexAttributes",false]],"profile (class in select_ai)":[[14,"select_ai.Profile",false]],"profileattributes (class in select_ai)":[[15,"select_ai.ProfileAttributes",false]],"provider (class in select_ai)":[[16,"select_ai.Provider",false]],"run() (select_ai.agent.asyncteam method)":[[4,"select_ai.agent.AsyncTeam.run",false]],"run() (select_ai.agent.team method)":[[3,"select_ai.agent.Team.run",false]],"run_pipeline() (select_ai.asyncprofile method)":[[5,"select_ai.AsyncProfile.run_pipeline",false]],"run_sql() (select_ai.asyncprofile method)":[[5,"select_ai.AsyncProfile.run_sql",false]],"run_sql() (select_ai.profile method)":[[14,"select_ai.Profile.run_sql",false]],"set_attribute() (select_ai.agent.agent method)":[[3,"select_ai.agent.Agent.set_attribute",false]],"set_attribute() (select_ai.agent.asyncagent method)":[[4,"select_ai.agent.AsyncAgent.set_attribute",false]],"set_attribute() (select_ai.agent.asynctask method)":[[4,"select_ai.agent.AsyncTask.set_attribute",false]],"set_attribute() (select_ai.agent.asyncteam method)":[[4,"select_ai.agent.AsyncTeam.set_attribute",false]],"set_attribute() (select_ai.agent.asynctool method)":[[4,"select_ai.agent.AsyncTool.set_attribute",false]],"set_attribute() (select_ai.agent.task method)":[[3,"select_ai.agent.Task.set_attribute",false]],"set_attribute() (select_ai.agent.team method)":[[3,"select_ai.agent.Team.set_attribute",false]],"set_attribute() (select_ai.agent.tool method)":[[3,"select_ai.agent.Tool.set_attribute",false]],"set_attribute() (select_ai.asyncprofile method)":[[5,"select_ai.AsyncProfile.set_attribute",false]],"set_attribute() (select_ai.asyncvectorindex method)":[[19,"select_ai.AsyncVectorIndex.set_attribute",false]],"set_attribute() (select_ai.profile method)":[[14,"select_ai.Profile.set_attribute",false]],"set_attribute() (select_ai.vectorindex method)":[[19,"select_ai.VectorIndex.set_attribute",false]],"set_attributes() (select_ai.agent.agent method)":[[3,"select_ai.agent.Agent.set_attributes",false]],"set_attributes() (select_ai.agent.asyncagent method)":[[4,"select_ai.agent.AsyncAgent.set_attributes",false]],"set_attributes() (select_ai.agent.asynctask method)":[[4,"select_ai.agent.AsyncTask.set_attributes",false]],"set_attributes() (select_ai.agent.asyncteam method)":[[4,"select_ai.agent.AsyncTeam.set_attributes",false]],"set_attributes() (select_ai.agent.asynctool method)":[[4,"select_ai.agent.AsyncTool.set_attributes",false]],"set_attributes() (select_ai.agent.task method)":[[3,"select_ai.agent.Task.set_attributes",false]],"set_attributes() (select_ai.agent.team method)":[[3,"select_ai.agent.Team.set_attributes",false]],"set_attributes() (select_ai.agent.tool method)":[[3,"select_ai.agent.Tool.set_attributes",false]],"set_attributes() (select_ai.asyncconversation method)":[[9,"select_ai.AsyncConversation.set_attributes",false]],"set_attributes() (select_ai.asyncprofile method)":[[5,"select_ai.AsyncProfile.set_attributes",false]],"set_attributes() (select_ai.asyncvectorindex method)":[[19,"select_ai.AsyncVectorIndex.set_attributes",false]],"set_attributes() (select_ai.conversation method)":[[9,"select_ai.Conversation.set_attributes",false]],"set_attributes() (select_ai.profile method)":[[14,"select_ai.Profile.set_attributes",false]],"set_attributes() (select_ai.vectorindex method)":[[19,"select_ai.VectorIndex.set_attributes",false]],"show_prompt() (select_ai.asyncprofile method)":[[5,"select_ai.AsyncProfile.show_prompt",false]],"show_prompt() (select_ai.profile method)":[[14,"select_ai.Profile.show_prompt",false]],"show_sql() (select_ai.asyncprofile method)":[[5,"select_ai.AsyncProfile.show_sql",false]],"show_sql() (select_ai.profile method)":[[14,"select_ai.Profile.show_sql",false]],"style (class in select_ai.summary)":[[17,"select_ai.summary.Style",false]],"summarize() (select_ai.asyncprofile method)":[[5,"select_ai.AsyncProfile.summarize",false]],"summarize() (select_ai.profile method)":[[14,"select_ai.Profile.summarize",false]],"summaryparams (class in select_ai.summary)":[[17,"select_ai.summary.SummaryParams",false]],"syntheticdataattributes (class in select_ai)":[[18,"select_ai.SyntheticDataAttributes",false]],"syntheticdataparams (class in select_ai)":[[18,"select_ai.SyntheticDataParams",false]],"task (class in select_ai.agent)":[[3,"select_ai.agent.Task",false]],"taskattributes (class in select_ai.agent)":[[3,"select_ai.agent.TaskAttributes",false]],"team (class in select_ai.agent)":[[3,"select_ai.agent.Team",false]],"teamattributes (class in select_ai.agent)":[[3,"select_ai.agent.TeamAttributes",false]],"tool (class in select_ai.agent)":[[3,"select_ai.agent.Tool",false]],"toolattributes (class in select_ai.agent)":[[3,"select_ai.agent.ToolAttributes",false]],"toolparams (class in select_ai.agent)":[[3,"select_ai.agent.ToolParams",false]],"translate() (select_ai.asyncprofile method)":[[5,"select_ai.AsyncProfile.translate",false]],"translate() (select_ai.profile method)":[[14,"select_ai.Profile.translate",false]],"vectorindex (class in select_ai)":[[19,"select_ai.VectorIndex",false]],"vectorindexattributes (class in select_ai)":[[19,"select_ai.VectorIndexAttributes",false]]},"objects":{"select_ai":[[16,0,1,"","AWSProvider"],[16,0,1,"","AnthropicProvider"],[9,0,1,"","AsyncConversation"],[5,0,1,"","AsyncProfile"],[19,0,1,"","AsyncVectorIndex"],[16,0,1,"","AzureProvider"],[14,0,1,"","BaseProfile"],[16,0,1,"","CohereProvider"],[9,0,1,"","Conversation"],[9,0,1,"","ConversationAttributes"],[16,0,1,"","GoogleProvider"],[16,0,1,"","HuggingFaceProvider"],[16,0,1,"","OCIGenAIProvider"],[16,0,1,"","OpenAIProvider"],[19,0,1,"","OracleVectorIndexAttributes"],[14,0,1,"","Profile"],[15,0,1,"","ProfileAttributes"],[16,0,1,"","Provider"],[18,0,1,"","SyntheticDataAttributes"],[18,0,1,"","SyntheticDataParams"],[19,0,1,"","VectorIndex"],[19,0,1,"","VectorIndexAttributes"]],"select_ai.AsyncConversation":[[9,1,1,"","create"],[9,1,1,"","delete"],[9,1,1,"","fetch"],[9,1,1,"","get_attributes"],[9,1,1,"","list"],[9,1,1,"","set_attributes"]],"select_ai.AsyncProfile":[[5,1,1,"","add_negative_feedback"],[5,1,1,"","add_positive_feedback"],[5,1,1,"","chat"],[5,1,1,"","chat_session"],[5,1,1,"","create"],[5,1,1,"","delete"],[5,1,1,"","delete_feedback"],[5,1,1,"","delete_profile"],[5,1,1,"","explain_sql"],[5,1,1,"","fetch"],[5,1,1,"","generate"],[5,1,1,"","generate_synthetic_data"],[5,1,1,"","get_attributes"],[5,1,1,"","list"],[5,1,1,"","narrate"],[5,1,1,"","run_pipeline"],[5,1,1,"","run_sql"],[5,1,1,"","set_attribute"],[5,1,1,"","set_attributes"],[5,1,1,"","show_prompt"],[5,1,1,"","show_sql"],[5,1,1,"","summarize"],[5,1,1,"","translate"]],"select_ai.AsyncVectorIndex":[[19,1,1,"","create"],[19,1,1,"","delete"],[19,1,1,"","delete_index"],[19,1,1,"","disable"],[19,1,1,"","enable"],[19,1,1,"","fetch"],[19,1,1,"","get_attributes"],[19,1,1,"","get_next_refresh_timestamp"],[19,1,1,"","get_profile"],[19,1,1,"","list"],[19,1,1,"","set_attribute"],[19,1,1,"","set_attributes"]],"select_ai.Conversation":[[9,1,1,"","create"],[9,1,1,"","delete"],[9,1,1,"","fetch"],[9,1,1,"","get_attributes"],[9,1,1,"","list"],[9,1,1,"","set_attributes"]],"select_ai.Profile":[[14,1,1,"","add_negative_feedback"],[14,1,1,"","add_positive_feedback"],[14,1,1,"","chat"],[14,1,1,"","chat_session"],[14,1,1,"","create"],[14,1,1,"","delete"],[14,1,1,"","delete_feedback"],[14,1,1,"","delete_profile"],[14,1,1,"","explain_sql"],[14,1,1,"","fetch"],[14,1,1,"","generate"],[14,1,1,"","generate_synthetic_data"],[14,1,1,"","get_attributes"],[14,1,1,"","list"],[14,1,1,"","narrate"],[14,1,1,"","run_sql"],[14,1,1,"","set_attribute"],[14,1,1,"","set_attributes"],[14,1,1,"","show_prompt"],[14,1,1,"","show_sql"],[14,1,1,"","summarize"],[14,1,1,"","translate"]],"select_ai.VectorIndex":[[19,1,1,"","create"],[19,1,1,"","delete"],[19,1,1,"","delete_index"],[19,1,1,"","disable"],[19,1,1,"","enable"],[19,1,1,"","fetch"],[19,1,1,"","get_attributes"],[19,1,1,"","get_next_refresh_timestamp"],[19,1,1,"","get_profile"],[19,1,1,"","list"],[19,1,1,"","set_attribute"],[19,1,1,"","set_attributes"]],"select_ai.agent":[[3,0,1,"","Agent"],[3,0,1,"","AgentAttributes"],[4,0,1,"","AsyncAgent"],[4,0,1,"","AsyncTask"],[4,0,1,"","AsyncTeam"],[4,0,1,"","AsyncTool"],[3,0,1,"","Task"],[3,0,1,"","TaskAttributes"],[3,0,1,"","Team"],[3,0,1,"","TeamAttributes"],[3,0,1,"","Tool"],[3,0,1,"","ToolAttributes"],[3,0,1,"","ToolParams"]],"select_ai.agent.Agent":[[3,1,1,"","create"],[3,1,1,"","delete"],[3,1,1,"","delete_agent"],[3,1,1,"","disable"],[3,1,1,"","enable"],[3,1,1,"","fetch"],[3,1,1,"","list"],[3,1,1,"","set_attribute"],[3,1,1,"","set_attributes"]],"select_ai.agent.AsyncAgent":[[4,1,1,"","create"],[4,1,1,"","delete"],[4,1,1,"","delete_agent"],[4,1,1,"","disable"],[4,1,1,"","enable"],[4,1,1,"","fetch"],[4,1,1,"","list"],[4,1,1,"","set_attribute"],[4,1,1,"","set_attributes"]],"select_ai.agent.AsyncTask":[[4,1,1,"","create"],[4,1,1,"","delete"],[4,1,1,"","delete_task"],[4,1,1,"","disable"],[4,1,1,"","enable"],[4,1,1,"","fetch"],[4,1,1,"","list"],[4,1,1,"","set_attribute"],[4,1,1,"","set_attributes"]],"select_ai.agent.AsyncTeam":[[4,1,1,"","create"],[4,1,1,"","delete"],[4,1,1,"","delete_team"],[4,1,1,"","disable"],[4,1,1,"","enable"],[4,1,1,"","export"],[4,1,1,"","export_team"],[4,1,1,"","fetch"],[4,1,1,"","import_team"],[4,1,1,"","list"],[4,1,1,"","run"],[4,1,1,"","set_attribute"],[4,1,1,"","set_attributes"]],"select_ai.agent.AsyncTool":[[4,1,1,"","create"],[4,1,1,"","create_built_in_tool"],[4,1,1,"","create_email_notification_tool"],[4,1,1,"","create_pl_sql_tool"],[4,1,1,"","create_rag_tool"],[4,1,1,"","create_slack_notification_tool"],[4,1,1,"","create_sql_tool"],[4,1,1,"","create_websearch_tool"],[4,1,1,"","delete"],[4,1,1,"","delete_tool"],[4,1,1,"","disable"],[4,1,1,"","enable"],[4,1,1,"","fetch"],[4,1,1,"","list"],[4,1,1,"","set_attribute"],[4,1,1,"","set_attributes"]],"select_ai.agent.Task":[[3,1,1,"","create"],[3,1,1,"","delete"],[3,1,1,"","delete_task"],[3,1,1,"","disable"],[3,1,1,"","enable"],[3,1,1,"","fetch"],[3,1,1,"","list"],[3,1,1,"","set_attribute"],[3,1,1,"","set_attributes"]],"select_ai.agent.Team":[[3,1,1,"","create"],[3,1,1,"","delete"],[3,1,1,"","delete_team"],[3,1,1,"","disable"],[3,1,1,"","enable"],[3,1,1,"","export"],[3,1,1,"","export_team"],[3,1,1,"","fetch"],[3,1,1,"","import_team"],[3,1,1,"","list"],[3,1,1,"","run"],[3,1,1,"","set_attribute"],[3,1,1,"","set_attributes"]],"select_ai.agent.Tool":[[3,1,1,"","create_built_in_tool"],[3,1,1,"","create_email_notification_tool"],[3,1,1,"","create_pl_sql_tool"],[3,1,1,"","create_rag_tool"],[3,1,1,"","create_slack_notification_tool"],[3,1,1,"","create_sql_tool"],[3,1,1,"","create_websearch_tool"],[3,1,1,"","delete"],[3,1,1,"","delete_tool"],[3,1,1,"","disable"],[3,1,1,"","enable"],[3,1,1,"","fetch"],[3,1,1,"","list"],[3,1,1,"","set_attribute"],[3,1,1,"","set_attributes"]],"select_ai.summary":[[17,0,1,"","ChunkProcessingMethod"],[17,0,1,"","ExtractivenessLevel"],[17,0,1,"","Style"],[17,0,1,"","SummaryParams"]]},"objnames":{"0":["py","class","Python class"],"1":["py","method","Python method"]},"objtypes":{"0":"py:class","1":"py:method"},"terms":{"":[0,1,5,9,10,11,12,14,16,18,19,20],"0":[1,5,9,14,15,16,19,20],"01":[3,4],"02":[3,4],"03":[3,4],"04":[3,4],"05":[3,4],"06":[3,4],"07":[3,4],"07b9":[3,4],"08":[3,4],"09":[3,4],"1":[1,3,4,5,7,9,14,15,16,17,19,20],"10":[3,4,8,9,18,20],"100":[3,4,18],"1024":[5,14,15,19],"11":[11,12],"11c8":9,"12":[11,12],"128":19,"128e":[3,4],"13":[11,12,18],"14":[9,11,12],"1440":19,"1450":19,"150":17,"151b000ad3b4":9,"17b":[3,4],"2":[3,4,5,19,20],"20":9,"20000":5,"2019":[3,4,18],"20240620":16,"2025":1,"2048":[5,14],"20i":20,"20oci":20,"25e23a25":[3,4],"3":[3,4,5,11,12,16,19],"300":18,"37ddc22e":9,"37df777f":9,"380a1910":9,"3ab2ed3":9,"3d49":9,"3f":20,"4":[3,4,5,7,16,19,20],"40":18,"400":[5,14],"4096":[2,5,14],"40e7":[3,4],"463a":[3,4],"4ed7":[3,4],"4o":16,"5":[3,4,5,7,8,16,18,19,20],"50":17,"503":[5,14],"5275a80":9,"55":[5,14],"587":13,"5bf2":9,"6":[3,4],"7":[3,4,9],"7e52":9,"8":[3,4,20],"80":11,"8000":[9,20],"8192":[5,14],"9":[3,4],"987c":[3,4],"A":[1,3,4,5,6,9,10,11,14,15,16,19,20],"AND":1,"AS":[1,5,14],"BE":1,"BUT":1,"By":[9,19],"FOR":1,"For":[3,5,7,8,9,11,12,13,14,15,16,18,19,20],"IN":1,"If":[3,4,6,9,10,11,19,20],"In":[3,4,7,8,10,19,20],"It":[0,5,6,7,9,12,14,17,18,19],"NO":1,"NOT":1,"No":[3,4],"OF":1,"OR":1,"On":[3,4,11],"One":8,"THE":1,"TO":1,"The":[1,2,3,4,5,6,7,8,9,10,12,13,14,15,16,17,18,19,20],"Then":[8,11],"There":[13,14],"These":[3,4,11,13,16,19],"To":[5,8,11,16,19],"WITH":1,"_":[1,7],"__name__":20,"_required_field":[3,4],"a290":9,"a489":[3,4],"abc":[3,4],"abil":9,"about":[3,4,5,6,9,12,14,19],"abov":1,"abstract":14,"accept":[3,4,6,10,13],"access":[0,3,4,5,10,11,14,15,16,17,19],"accomplish":[3,4],"accuraci":2,"acl":[12,13,16,19],"acquir":[5,8,14,20],"across":[9,19],"act":[2,3,4],"action":[1,3,4,7,12,19],"activ":[8,9,11,19],"actor":[3,4,18],"actual":[5,14],"ad":[6,19],"add":[3,13,16,20],"add_negative_feedback":[5,14],"add_positive_feedback":[5,14],"addit":[3,4,6,7,8,15,18],"addition":[5,14],"adjac":19,"admin":13,"admin_us":[13,16],"administr":[5,8,13],"advanc":[3,4],"adventur":[3,4],"affili":1,"after":[5,6,8,9,11,14,19,20],"after_serv":20,"after_server_stop":20,"again":[10,14],"against":[6,20],"agent":[8,10,12,16],"agent_attribut":[3,4],"agent_nam":[3,4],"agent_name_pattern":[3,4],"agentattribut":[0,3,4],"agentnotfounderror":[3,4],"agenttasknotfounderror":[3,4],"agentteamnotfounderror":[3,4],"agenttoolnotfounderror":[3,4],"agreement":1,"ai":[1,5,6,7,8,9,11,13,14,15,17,18,19,20],"alcohol":[5,14],"algorithm":[3,4],"alia":5,"all":[1,3,4,5,6,7,9,13,14,15,19,20],"allow":[3,4,5,13,16],"allowlist":20,"along":[8,19],"alreadi":[4,5,6,10,11,14,19,20],"also":[3,4,5,10,14,16,19,20],"amazon":14,"amazonaw":16,"amount":[5,14,19],"an":[0,1,3,4,5,6,7,8,9,10,11,13,14,15,16,17,19,20],"analysi":9,"analyst":[3,4],"analyt":[3,4,5,14],"analyz":[3,4,5],"analyze_movie_task":[3,4],"ancient":[5,14],"ani":[1,3,4,11,16],"anim":[3,4],"annot":[5,14,15,19],"anomali":[3,4],"anoth":[2,3,4,5,9,14,19],"answer":[2,3,4,5,6,7,9,12,14,19],"ant":10,"anthrop":[10,16],"anthropic_cr":10,"anthropicprovid":0,"api":[0,3,7,8,13,15,17,20],"app":20,"appconfig":20,"append_host_ac":13,"appli":20,"applic":[2,3,4,5,7,8,9,12,13,16,18,19,20],"appreci":9,"approach":20,"approxim":[7,17,20],"ar":[3,4,5,6,7,9,10,11,13,14,15,16,17,18,19,20],"arbitrari":20,"area":[5,14],"arg":[5,7,14,20],"argument":[3,4,8,13],"aris":1,"arrai":[3,15],"arriv":7,"articl":6,"artifici":[3,4,5],"as_complet":[5,7],"asgi":20,"ashburn":[17,19],"ask":[3,6,9,12],"assign":[3,15,18],"associ":[1,3,4,5,19],"astrobiologi":[5,14],"astronom":[5,14],"astronomi":[5,14,17],"async":[7,8,12,13,20],"async_as_complet":0,"async_connect":[4,5,8,9,10,13,16,18,19],"async_convers":9,"async_create_credenti":[4,10],"async_delete_credenti":10,"async_disconnect":[7,8,20],"async_gath":0,"async_grant_http_access":16,"async_grant_network_access":13,"async_is_connect":8,"async_oci_ai_profil":[5,7,9,14,17,18,20],"async_oci_vector_ai_profil":[14,19],"async_pipelin":0,"async_profil":[5,9,17,18,19],"async_queue_work":0,"async_revoke_http_access":16,"async_revoke_network_access":13,"async_sess":9,"async_vector_index":19,"asyncag":0,"asynccontextmanag":20,"asyncconvers":[0,5],"asyncgener":[4,5,9,19],"asynchron":[0,4,7,18],"asyncio":[4,5,7,9,10,12,13,16,18,19],"asyncprofil":[0,2,4,7,9,14,15,17,18,19,20],"asynctask":0,"asyncteam":0,"asynctool":0,"asyncvectorindex":0,"atexit":20,"attach":3,"attribut":[2,3,4,5,9,14,16,18],"attribute_nam":[3,4,5,14,19],"attribute_valu":[3,4,5,14,19],"audienc":[3,4],"augment":[12,15,19],"authent":[5,10,14,15,16,17],"author":1,"autom":[3,4,5,15],"automat":[3,9,11],"automl":[3,4],"autonom":[3,5],"autoreload":20,"avail":[3,6,7,8,11,17],"avoid":[8,20],"aw":[10,14,16],"await":[4,5,7,8,9,10,13,16,17,18,19,20],"awar":[5,6,9,12,14,15],"aws_access_key_id":10,"aws_apiformat":16,"aws_bedrock_cr":10,"aws_secret_access_kei":10,"awsprovid":0,"azur":[10,14,15,16],"azure_deployment_nam":16,"azure_embedding_deployment_nam":16,"azure_openai_api_kei":10,"azure_openai_cr":10,"azure_resource_nam":16,"azureprovid":0,"b":[1,6,17,19],"b2":[5,14],"back":[4,12,14],"background":[7,20],"base":[0,3,4,5,6,8,12,16,19,20],"baseprofil":[0,14,19],"batch":[7,20],"be11":[3,4],"be1a000a15b6":9,"becaus":[2,3,4,5,9],"becom":[5,14],"bedrock":[10,16],"been":[5,14],"beer":[5,14],"befor":[2,5,7,10,13,14,18,19,20],"before_serv":20,"before_server_start":20,"begin":15,"behav":15,"behavior":[0,2,3,9,15,16,19],"behind":11,"being":[5,14,16,19],"below":[1,8,18,19,20],"benefit":[3,4,8],"better":20,"between":[0,9,19],"bin":11,"biograph":[3,4],"biographi":[3,4],"block":[12,14,19,20],"blog":[8,20],"bool":[3,4,5,9,14,15,18,19],"both":[1,3,4,7,9,12,16,17,19,20],"bound":20,"boundari":[3,19],"branch":[5,14],"breakdown":5,"brew":[5,14],"bridg":[0,12],"bucket":[6,17,19],"budget":20,"build":[0,3,4,5,9,11,16,19],"built":[3,4,14],"burn":[5,14],"busi":[3,4,5,17,20],"byte":[5,14],"c":[1,5,20],"c2e3ff20":[3,4],"calcul":[5,19],"call":[3,5,7,8,9,10,12,13,16,17,19,20],"callabl":3,"caller":[5,14],"can":[0,2,3,4,5,6,7,8,9,11,13,14,15,16,17,19,20],"cancri":[5,14],"cannot":19,"capabl":[0,3,4,12],"capac":[7,8,20],"care":[18,20],"carefulli":[3,4],"case":[3,4,5,15,16],"case_sensitive_valu":[5,14,15,19],"cc72740c75a5":[3,4],"celesti":[5,14],"certain":3,"chain":3,"challeng":9,"chang":[5,14,19],"channel":[3,4],"charact":[5,6,14,19],"charg":1,"chat":[0,2,12,15,20],"chat_sess":[5,6,9,14],"chat_task":5,"chatgpt":[3,4],"check":[6,8,19,20],"chemistri":[5,14],"chicago":[3,4,5,14,15,16,19],"choic":19,"choos":[0,3,7,12,17,20],"chunk":[2,5,6,14,17,19],"chunk_overlap":19,"chunk_processing_method":17,"chunk_siz":[2,5,14,19],"chunkprocessingmethod":0,"churn":20,"civil":[5,14],"claim":1,"clarif":3,"clarifi":2,"class":[3,4,5,9,14,15,17,18,19,20],"classif":[3,4],"classmethod":[3,4,5,9,14,19],"claud":16,"cleanup":[3,4,5,10,14,20],"clear":[3,4,6],"cli":11,"client":[7,20],"climat":9,"clob":[5,6,14],"close":[7,8,12,17,20],"close_select_ai_pool":20,"cloud":[3,4,5,14,16],"clue":[5,14],"cluster":[3,4,16],"co":16,"code":[6,8,20],"coher":[10,16,19],"cohere_api_kei":10,"cohere_cr":10,"cohereprovid":0,"collect":[1,3,4,5,19],"column":[3,5,14,15],"com":[3,4,6,11,13,16,17,19],"combin":[9,12],"come":19,"comedi":[3,4],"comfort":6,"command":[7,11,12,16,19,20],"comment":[2,3,5,10,14,15,18,19],"common":[0,19],"compar":19,"compart":16,"compat":16,"compet":14,"complet":[1,7,18,19],"complianc":[3,4],"compliant":[3,4],"compon":[3,4,5],"component_typ":[3,4],"compos":3,"composit":[3,4,5,14],"comprehens":[9,14],"comput":[5,14],"concept":[0,16],"conceptu":19,"concis":[3,4],"concret":[16,20],"concurr":[8,12,19,20],"conda":19,"condit":1,"config":10,"config_dir":8,"configur":[3,6,8,15,18,19,20],"conflict":[3,4],"connect":[0,1,3,4,5,7,9,10,12,13,14,16,18,19,20],"consid":19,"constantli":[5,14],"constrain":15,"constraint":[2,5,14,15,19],"construct":19,"consum":[5,7,14,18],"contain":[3,4,5,14,17,19],"content":[0,2,3,4,5,6,9,12,14,19],"context":[3,5,6,9,12,14,15,19,20],"contextlib":20,"contextu":9,"continu":[3,4,5],"continue_on_error":[5,7],"contract":1,"contribut":1,"control":[0,5,6,7,9,14,17,18,19,20],"conveni":2,"convers":[2,3,4,5,6,12,14,15,19],"conversation_attribut":9,"conversation_id":[3,4,5,9,14],"conversation_length":9,"conversationattribut":0,"convert":[16,19],"cool":[5,14],"coordin":3,"copi":[1,18],"copyright":1,"core":0,"corpor":[5,14],"correct":5,"correctli":6,"correspond":[5,10,14],"cosin":19,"could":[3,4,5,14],"count":[5,6,7,14,18],"countri":16,"cover":[1,15],"creat":[0,1,5,7,8,11,12,13,15,16,20],"create_app":20,"create_built_in_tool":[0,3,4],"create_credenti":[3,10,16],"create_email_notification_tool":[0,3,4],"create_http_tool":[],"create_pl_sql_tool":[0,3,4],"create_pool":[7,8,9,20],"create_pool_async":[4,5,7,8,9,20],"create_rag_tool":[0,3,4],"create_slack_notification_tool":[0,3,4],"create_sql_tool":[0,3,4],"create_task":7,"create_tool":3,"create_websearch_tool":[0,3,4],"creation":[0,3,4,8,9,19],"credenti":[3,4,5,6,8,12,14,15,16,17,18,19,20],"credential_nam":[3,4,5,10,14,15,16,17,19],"ctrl":20,"cultur":9,"curat":6,"curl":20,"current":[3,8,9,19],"cust_email":5,"cust_first_nam":5,"cust_id":5,"cust_last_nam":5,"custom":[3,4,5,7,15,16,17,19],"customer_count":5,"d4328a7b2210":[3,4],"d81a000a3fda":9,"d81a000a53b":9,"d81a000a85f":9,"d917b055":[3,4],"da17":9,"dai":9,"damag":1,"dank":[5,14],"data":[1,2,3,4,5,12,14,16,19],"data_guard":[],"databas":[0,3,4,5,6,7,9,10,11,12,13,14,15,16,17,18,19,20],"databaseerror":[5,14,19],"datafram":[2,5,14],"date":[3,4,9,18],"datetim":[9,19],"db_admin":13,"db_admin_password":13,"db_connect_str":[6,7,8,11,13,20],"db_password":[6,8],"db_user":[6,8],"dba":5,"dbms_cloud":[10,13],"dbms_cloud_ai":[0,7,12,13],"dbms_cloud_ai_ag":[3,13],"dbms_cloud_pipelin":13,"dbms_network_acl_admin":13,"de":[5,14],"deal":1,"decid":[3,19],"dedic":[16,18],"deep":[],"def":[4,5,7,9,10,13,16,18,19,20],"default":[2,3,4,5,6,9,10,14,15,16,18,19],"default_config":10,"defin":[1,3,5,12,14,15,18,19],"definit":[3,4],"degre":[15,17],"delet":[0,3,4],"delete_ag":[0,3,4],"delete_credenti":10,"delete_feedback":[5,14],"delete_index":[0,19],"delete_profil":[5,14],"delete_task":[0,3,4],"delete_team":[0,3,4],"delete_tool":[0,3,4],"deliber":20,"demo":18,"denot":15,"depend":[0,3,5,15,16,19],"deploi":[3,4,5,16],"deploy":[3,4,7,8,15,16,20],"deriv":1,"describ":[3,4,5,12,14,15,16],"descript":[2,3,4,5,6,9,14,19],"design":[3,5],"despit":[5,14],"detail":[3,4,6,10,13,14,16,18,20],"detect":[3,4,5,14],"determin":17,"develop":[3,4,5,6,9,14,18,20],"df":[2,5,14],"dict":[3,4,5,14],"dictionari":3,"differ":[2,3,4,7],"dir_containing_tnsnam":13,"directli":[3,4,9,16,20],"director":[3,4,18],"disabl":[0,3,4,18,19],"disconnect":[7,8,20],"discoveri":[5,9,14],"displai":[1,2],"distanc":19,"distribut":1,"django":0,"do":[3,4,6,12,16,20],"doc":[11,19],"document":[1,12,13,17,19],"documentari":[3,4],"doe":[3,4,5,6,9,10,14,16,19],"doesn":5,"domain":3,"dot":19,"dotenv":13,"doubl":5,"down":20,"drama":[3,4],"drive":5,"driver":8,"drop":[3,4,9,10,19],"dsn":[3,4,5,6,7,8,9,10,11,13,14,16,18,19,20],"dtype":14,"due":[5,14],"dump":[3,4],"dure":[3,4,6,12,19,20],"dwcsdev":19,"dynam":[2,5,14],"e":[3,4,5,8,14,18,19],"e063":9,"e8a1":[3,4],"each":[1,6,7,8,19,20],"earli":[5,14],"earlier":19,"earliest":15,"easier":[5,14],"east":16,"ecosystem":[0,12],"effici":5,"either":[1,3,4,13,14,19,20],"elabor":9,"element":[5,14,19],"elig":15,"elimin":[3,4,5],"els":[5,14],"email":[3,4,5],"emb":19,"embed":[16,19],"embedding_model":[5,14,16,19],"empti":14,"en":[5,14,17],"enabl":[0,2,3,4,5,9,15,17,18,19],"enable_custom_source_uri":[15,19],"enable_human_tool":[3,4],"enable_sourc":[5,14,15,19],"enable_source_offset":[5,14,15,19],"enclos":5,"end":[1,2,5,9,14],"endpoint":[0,3,4,5,10,11,12,13,14,15,16,19],"enforce_object_list":[5,14,15,19],"english":[6,19],"enhanc":[3,4],"enough":[3,5,14],"ensur":[3,4,5],"enter":[],"entri":[3,13,16],"enum":2,"enumer":[5,7],"env":13,"environ":[0,3,4,6,8,11,13,19,20],"epic":[3,4],"error":[3,4,5,9,10,14,17,19,20],"especi":[9,20],"essenti":9,"euclidean":19,"eval":6,"evalu":[6,19],"even":[3,4,5,10,14],"event":[1,5,14],"everi":[8,9,10,15,20],"evolut":[5,14],"evolv":[5,6,14],"exact":19,"exactli":[17,18],"exampl":[0,5,6,8,9,10,11,13,14,18,19],"exce":17,"exchang":9,"exclude_nul":[5,14],"execut":[2,3,5,6,8,13,14,19],"executor":7,"exhaust":20,"exist":[3,4,5,6,9,10,14,18,19],"exit":[6,9],"exoplanet":[5,14],"expans":[5,14],"expect":[3,5,14,16,17,20],"experi":18,"expertis":[],"explain":[0,2,6,7,9],"explain_sql":[2,5,14],"explainsql":[2,5,7],"explan":[5,12,14],"explicit":2,"explicitli":5,"explor":[2,6],"export":[0,1,6,7,8,11,13,20],"export_import_movie_analyst":[3,4],"export_import_movie_task":[3,4],"export_import_movie_team":[3,4],"export_team":[0,3,4],"exported_spec":[3,4],"express":[1,3,4,5,6,14,19],"extern":[3,4,10,13,20],"extra":[6,11],"extract":17,"extractiveness_level":17,"extractivenesslevel":0,"extraterrestri":[5,14],"f":[5,7],"f084":9,"f3da":9,"f56d":[3,4],"f7a1":9,"f7e5e445d286":[3,4],"factori":[],"fail":20,"fals":[3,4,5,9,14,18,19],"famili":[3,4],"fantasi":[3,4],"fast":[6,16,20],"fastapi":[0,8],"faster":18,"fccenv":19,"featur":[3,4,12],"februari":[3,4],"feedback":[2,5,14],"feedback_cont":[5,14],"fetch":[0,3,4,5,9,14],"fetched_profil":[5,14],"fewer":[5,20],"fi":[3,4],"field":[3,5,10,14],"file":[1,5,6,10,11,13,14,17,19,20],"filenam":19,"film":[3,4],"filter":6,"final":7,"find":9,"fine":20,"fingerprint":10,"finish":7,"first":[5,14],"fit":1,"flask":0,"flexibl":[3,4],"float":[5,14,15,19],"focu":[5,17],"focus":[3,4,5,14],"follow":[1,2,3,4,5,9,10,13,14,17,19],"forc":[3,4,5,9,10,14,19],"forego":1,"foreign":15,"form":[5,14],"format":[5,14,15,16,17],"forth":1,"forti":20,"forward":[5,7,14],"found":[3,4,5,14],"four":20,"fp":10,"fp8":[3,4],"frame":12,"framework":[3,4,12],"free":1,"freeli":1,"fresh":6,"from":[0,1,2,3,4,5,7,8,9,10,11,13,14,15,16,18,19,20],"from_fil":10,"front":7,"fulfil":3,"full":[5,14],"fulli":[6,19],"function":[3,4,5,14,19],"further":9,"futur":7,"g":[5,14,18],"ga":[5,14],"gain":9,"galaxi":[5,14],"gamma":[5,14],"gap":0,"gatewai":20,"gather":7,"gcp":14,"gen":[5,10,14,16],"gener":[0,2,3,4,5,6,12,14,16,17,19,20],"generate_synthetic_data":[5,14,18],"generativelanguag":16,"genr":[3,4,6],"german":6,"get":[5,7,9,14,19,20],"get_attribut":[0,5,9,14,19],"get_next_refresh_timestamp":[0,19],"get_profil":[0,19],"getenv":[3,4,5,7,8,9,10,11,13,14,16,18,19,20],"getmod":20,"give":[5,12,14],"given":[3,4],"good":20,"googl":[10,14,16],"google_api_kei":10,"google_cr":10,"googleapi":16,"googleprovid":0,"govern":[3,4],"gpt":[0,16],"grant":[0,1,3,12,15],"grant_http_access":16,"grant_network_access":13,"grant_privileg":13,"graph":19,"graphenv":19,"gravit":[5,14],"grok":16,"ground":19,"group":[0,3,6],"grow":8,"guid":[3,5,6,14,17,18,19],"guidanc":4,"gunicorn":20,"ha":[5,9,10,13,14,18,19,20],"halo":[5,14],"ham":19,"handl":[0,7,8],"handler":20,"happen":20,"hard":20,"hardwar":1,"have":[1,5,9,16],"health":0,"heavi":19,"help":[3,4,6,9,12,15,19],"helper":[0,13,16,19],"here":[3,4,5,9,12],"herebi":1,"hereund":1,"hf":10,"hf_xxx":10,"high":[3,4,5,14,17,18],"higher":[5,12,18],"highlight":[3,4,16],"histor":[3,4],"histori":[5,9,12,14,15],"holder":1,"hook":20,"horror":[3,4],"host":[3,4,13,16,20],"hot":[5,14],"how":[2,3,4,5,6,7,14,15,16,17,18,19],"http":[3,4,5,6,11,13,14,16,17,19,20],"https_proxi":8,"https_proxy_port":8,"huggingfac":[10,16],"huggingface_cr":10,"huggingfaceprovid":0,"human":[3,4,9],"hyperparamet":[3,4],"i":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],"ibm":14,"ic":[5,14],"id":[3,4,5,9],"identifi":[3,4,5,9,14,18],"ignor":[5,9,14,19],"ii":1,"impact":9,"implement":16,"impli":1,"implic":17,"import":[0,1,5,7,8,9,10,11,13,14,16,18,19,20],"import_team":[0,3,4],"imported_analyze_movie_task":[3,4],"imported_movie_agent_team":[3,4],"imported_movie_analyst":[3,4],"improv":[2,3,4],"includ":[1,3,4,5,9,10,13,14,15,18,19],"include_data":19,"incorpor":15,"increas":20,"increment":[7,8,20],"indent":[3,4],"independ":7,"index":[3,4,6,7,8,10,11,12,14,15,16],"index_nam":19,"index_name_pattern":19,"indic":[15,16,19],"infer":16,"inform":[3,4,18,19],"infrar":[5,14],"infrastructur":[3,4,5,14],"inherit":[3,4],"initi":[5,19,20],"inlin":[0,2,3,4,5,6,14],"input":[3,4,7,17,19,20],"insert":[3,4,18],"insid":[3,4,6,19,20],"insight":[5,9,14],"inspect":[2,5,6,14,19],"inspir":[5,14],"instal":[0,6],"instanc":5,"instanti":[5,14,16],"instead":[2,3,4,6,16,20],"instruct":[2,3,4],"int":[5,7,9,14,15,17,18,19,20],"integr":[0,3,4,5,15],"intellectu":9,"intellig":[3,4,5],"intend":[3,6,13],"intention":[7,19],"interact":[0,5,9,12,14],"interfac":[4,5,11,12,14,20],"interpret":[16,20],"interv":19,"introduc":[],"introduct":0,"invalid":5,"invok":[3,4,12,16],"involv":20,"is_connect":[8,20],"isol":11,"item":[3,4,7,19],"iter":[3,4,5,9,14,19],"iterative_refin":17,"its":[1,3,4,5,7,8,9,11,14,19,20],"itself":5,"januari":[3,4],"job":[19,20],"join":7,"json":[3,4,15,19,20],"jsonifi":20,"jsonrespons":20,"keep":[3,4,6,7,8,9,12,19],"kei":[3,4,9,10,15,17],"kept":5,"key_cont":10,"key_fil":10,"keyword":[2,4,5,8,19],"kind":1,"knowledg":9,"known":[7,16],"kwarg":[5,14],"l2_squar":19,"languag":[2,3,4,5,6,9,12,14,15,16,19],"larg":[5,14],"larger":[1,5,19],"last":5,"latenc":20,"later":[5,10,14],"layer":[3,4],"learn":[3,4,5,9],"leav":[7,19,20],"led":[5,14],"len":7,"length":[6,9,17],"less":15,"let":[3,4,5,14,19,20],"level":[5,12,19],"leverag":[3,4,5],"li":9,"liabil":1,"liabl":1,"librari":[8,10,13,16],"licens":1,"licensor":1,"life":[5,14],"lifecycl":[0,9,19,20],"lifespan":20,"lightweight":20,"like":[3,4,8],"limit":[1,3,4,17,20],"line":[7,11,12,19,20],"link":[3,4,19],"list":[0,1,2,6,7,13,17,18],"listen":[],"live":20,"llama":[3,4,16],"llama_4_maverick":[3,4],"llm":[2,3,4,5,9,14,15,16,17,18],"llm_chat_tool":[3,4],"llm_config":[3,4],"load":[3,4,8,19,20],"local":[5,8,11,14,20],"localhost":20,"locat":[3,4,6,17,19],"location_uri":[5,14,17],"log":19,"logic":4,"long":[5,7,8,12,14,17,20],"longer":[5,9,10,14,19,20],"loop":6,"low":[17,18,20],"lower":15,"lower_port":13,"lrgrwrk":1,"m":[8,11,20],"machin":[3,4,5],"made":[1,7,9],"mai":[3,4,7,20],"mail":13,"main":[3,4,5,7,9,10,13,16,17,18,19],"maintain":[3,4],"major":14,"make":[1,5,10,13,14,18,19],"makefil":11,"manag":[5,9,13,15,17,19,20],"manhattan":19,"mani":[2,5,6,7,12,14,16,18],"manifest":19,"manipul":5,"manner":[5,9],"manual":[5,9,19],"map":[3,4,5,7,14,15,18],"map_reduc":17,"mapped_sql":[5,14],"match":[3,4,5,7,14,19,20],"match_limit":19,"materi":12,"mathemat":[5,14],"maverick":[3,4],"max_siz":[7,8,20],"max_token":[5,14,15,19],"max_word":17,"max_work":7,"maximum":[7,17,19],"mean":[15,19],"meant":3,"meanwhil":[5,14],"measur":20,"medium":[17,18],"memori":[5,14],"merchant":1,"merg":[5,14],"messag":[],"meta":[3,4],"metadata":[9,14,15,18,19,20],"method":[0,3,4,5,8,9,13,14,16,17,19,20],"metric":19,"microsoft":14,"milki":[5,14],"min_siz":[7,8,20],"min_word":17,"minimum":[1,17,19],"minut":19,"mirror":[4,9,19],"misconcept":9,"miss":[9,19],"mission":[3,4],"mistak":9,"mix":20,"ml":[3,4,5],"mlenv":19,"mode":0,"model":[0,5,12,15,16,17,18],"modul":[0,7,12],"more":[3,4,5,9,13,14,15,17,18,19,20],"most":[2,3,4,8,12,15,16],"move":[3,4],"movement":[3,4],"movi":[3,4,6,18],"movie_agent_team":[3,4],"movie_analyst":[3,4],"movie_sql_tool":[3,4],"mtl":20,"much":[5,14,15],"multi":[0,3,7,8,12,20],"multipl":[3,4,5,7,8,9,14,18,19],"multit":[],"music":[3,4],"must":[1,3,4,7,10,15,17,18,19],"my":[3,4,5,14,16,19],"my_oci_ai_profile_kei":[3,4,5,10,14,15,19],"myapp":20,"myappconfig":20,"myrenv":19,"mysteri":[3,4,5,14],"n":[6,17,19],"nad":5,"name":[3,4,5,6,7,10,13,14,15,16,18,19,20],"namespac":[6,17],"narr":12,"narrat":[0,2,5,6,9,12,19],"natur":[2,3,4,5,6,9,12,14,15,16,19],"need":[2,3,4,5,6,7,9,10,11,12,13,14,15,16,18,19],"neg":[5,14,15],"neither":17,"network":[0,3,4,5,10,11,12,14,16,19],"new":[3,4,5,8,9,14,19],"next":[5,6,7,19],"night":[3,4,5,14],"non":[5,6,14,15,16,19],"none":[3,4,5,7,9,14,15,16,17,18,19],"noninfring":1,"normal":20,"note":[5,6],"notebook":[8,12,19],"notic":1,"notif":[3,4],"notification_typ":[3,4],"notificationtyp":3,"nprompt":7,"null":[5,14],"number":[5,6,9,14,15,17,18,19],"number_of_promot":5,"numer":[5,14],"o":[3,4,5,6,7,8,9,10,11,13,14,16,17,18,19,20],"object":[0,2,3,4,5,6,8,10,12,16,18,20],"object_list":[3,4,5,14,15,16,18,19],"object_list_mod":[5,14,15,19],"object_nam":18,"object_storage_credential_nam":[3,4,19],"object_store_cr":[6,17],"object_store_credenti":19,"objectstor":19,"objectstorag":[6,17,19],"observ":[5,14],"obtain":1,"occur":[9,19],"oci":[2,3,4,5,6,10,14,15,16,19],"oci_ai_profil":[2,3,4,5,6,7,9,14,17,18,20],"oci_apiformat":[3,4,5,14,15,16,19],"oci_compartment_id":[5,14,16,19],"oci_endpoint_id":[5,14,16,19],"oci_genai_cr":10,"oci_runtimetyp":[5,14,16,19],"oci_vector_ai_profil":[5,14,19],"ocid":16,"ocigenaiprovid":[0,3,4,5,14,15,19],"off":[8,9],"offer":[1,3,4,5,14,16],"older":[5,14],"omf":5,"omit":[3,4,9,19],"oml":5,"oml4pi":5,"onc":[12,20],"one":[1,2,3,4,5,6,7,8,9,13,14,16,17,18,19,20],"onli":[3,5,6,14,15,19,20],"open":[3,4,8,10,20],"open_ai_api_kei":[3,4],"open_ai_credential_nam":[3,4],"open_ai_profile_nam":[3,4],"openai":[0,10,16],"openai_cr":[3,4,10],"openai_profil":[3,4],"openaiprovid":[0,3,4,15],"oper":[3,4,6,8,13,20],"operation":[3,4],"optic":[5,14],"option":[0,3,4,7,9,11,12,15,16,17,18,20],"ora":[5,13],"oracl":[0,1,3,4,5,10,11,12,13,14,16,19,20],"oraclecloud":[17,19],"oracledb":[5,8,11,14,19,20],"oraclevectorindexattribut":0,"orchestr":3,"order":[3,7],"ordereddict":[3,4],"ordinari":20,"org":[5,14,17],"organ":[3,4],"origin":[5,14,17,19],"ot":4,"other":[1,3,4,7,12,14,19,20],"otherwis":[1,3,4],"our":[5,14],"out":1,"outbound":13,"outlin":3,"output":[2,3,4,5,6,9,10,13,14,16,17,18,19],"outsid":[3,4],"over":[3,5,7,9,12,14,19],"overal":[5,14],"overlap":19,"overrid":6,"overwhelm":20,"own":[1,7,8,10,18,20],"owner":[3,4,5,14,15,16,18],"owner_nam":18,"p":[5,14],"packag":[0,3,6,10,11,12,13,20],"page":[3,4],"pair":[3,7],"panda":[2,5,11,12,14],"paragraph":17,"parallel":18,"param":[3,4,5,14,17,18],"paramet":[0,3,4,5,8,9,14,15,16,19,20],"part":7,"parti":12,"particular":1,"pass":[3,4,5,6,8,9,10,14,15,17,19,20],"password":[3,4,5,6,7,8,9,10,11,13,14,16,18,19,20],"past":9,"patent":1,"path":[5,8,13,14],"pattern":[0,3,4,5,6,7,12,14,19],"paus":[3,4,19],"pdb3":19,"per":[5,6,8,14,15,17,20],"perform":[1,2,3,4,5,14,18,19],"period":9,"permiss":[1,13],"permit":16,"persist":3,"person":1,"pformat":[3,4,5,14],"phenomena":[5,14],"physic":[5,14],"piec":1,"pile":20,"pint":[5,14],"pip":[6,11,20],"pipelin":[0,7,19],"pipeline_nam":19,"pitfal":9,"pl":[0,3,4,8,13,18],"place":20,"placehold":[3,4],"plan":[3,11],"platform":[14,20],"plu":16,"point":17,"pool":[0,5,12,19],"pool_incr":[7,20],"pool_max":[7,20],"pool_min":[7,20],"pop":20,"popul":[18,19],"port":[13,16,20],"portabl":[3,4],"portion":1,"posit":[5,14],"possibl":[7,18,20],"potenti":2,"power":5,"powershel":11,"pprint":[3,4,5,14],"prebuilt":[3,4],"predict":[3,4,15],"prefer":[9,19,20],"prepar":13,"present":17,"preserv":19,"press":20,"pressur":[5,14],"prevent":[5,14,20],"preview":12,"previou":[3,4,9],"previous":19,"primari":15,"primarili":[3,4],"print":[2,3,4,5,6,7,9,10,11,13,14,16,17,19],"prior":[3,9],"prioriti":18,"privaci":[3,4],"private_kei":10,"private_key_cont":10,"privileg":[3,5,10,11,12,14,16,18,19],"procedur":[3,4,19],"process":[3,4,8,12,16,17,19,20],"processor":7,"produc":7,"product":[3,4,5,7,15,16,18,19,20],"profil":[2,3,4,7,8,9,10,12,16,17,18,19,20],"profile_attribut":[3,4,5,14,19],"profile_nam":[2,3,4,5,7,9,14,16,17,18,19,20],"profile_name_pattern":[5,14],"profileattribut":[0,3,4,5,14,16,19],"profileemptyattributeserror":14,"profileexistserror":14,"profilenotfounderror":[5,14,19],"program":[5,12],"progress":[5,14],"project":11,"promo_id":[5,14],"promot":[2,3,4,5,7,14],"promotion_count":5,"prompt":[2,3,4,6,8,9,12,15,16,17,18,19,20],"prompt_spec":[5,14],"prompt_specif":[5,7],"properti":3,"protocol":13,"prototyp":18,"provid":[1,2,3,4,5,6,7,8,9,11,12,13,14,15,17,18,19,20],"provider_endpoint":[5,14,16,19],"provider_nam":[5,14,16,19],"providerattribut":[5,14],"proxi":[3,4,5,9,11,14,19,20],"ps1":11,"public":[3,4,19,20],"purpos":[1,3,6,12,15],"put":7,"py":[0,11,20],"pypi":11,"python":[0,1,3,4,5,6,7,8,11,14,19],"python3":11,"quart":20,"quarterli":9,"queri":[2,3,4,5,9,15,19],"question":[3,4,5,9,12],"queu":7,"queue":7,"quick":6,"quickli":8,"quit":6,"quot":5,"r":[3,4,11,16,19],"radio":[5,14],"rag":[0,3,4,6,12,20],"rai":[5,14],"rais":[3,4,5,10,14,17,19],"raise_error_if_exist":14,"raise_error_on_empty_attribut":14,"random":15,"rang":[3,4,5,7,13,14],"rate":19,"rather":19,"reach":13,"read":[3,4,5,6,8,9,10,13,14,19],"readabl":9,"readi":[7,20],"reason":[3,4,9,16],"receiv":[7,16],"recip":0,"recipi":[3,4],"recommend":11,"record":[18,19],"record_count":18,"recreat":[3,4,5,10,14,19],"reduc":[5,14,20],"refer":[1,5,9,10,16],"referenc":[2,3,4,5,14,18],"referenti":15,"refresh":19,"refresh_r":19,"regard":14,"regexp_lik":[3,4,5,14,19],"region":[3,4,5,12,14,15,16,19],"regist":[3,4,12,20],"regress":[3,4],"regular":[3,4,5,6,14,19],"relat":[3,4,19],"releas":[3,4,6,8,18,20],"relev":[9,15,19],"remain":[5,9,14],"remov":[5,9,13,14,16,19],"remove_host_ac":13,"repair":5,"rephras":17,"repl":[],"replac":[3,4,5,10,14,16,19],"replai":9,"replet":9,"report":[15,19],"repositori":11,"repres":[9,14,18,19],"request":[0,3,4,5,8,14,18,19],"requir":[0,3,4,5,6,10,12,13,14,17,19,20],"research":[3,4,5,9,14],"resid":[3,4,19],"resourc":[16,18],"respond":[3,4],"respons":[3,4,5,6,9,14,15,16,19,20],"rest":20,"restrict":[1,15],"result":[2,3,4,5,6,7,12,14,19],"resum":[3,4,19],"retain":[6,9],"retent":9,"retention_dai":9,"retriev":[5,12,15,19],"return":[2,3,4,5,6,7,9,12,14,15,19,20],"reus":[0,3,4],"reusabl":12,"revok":0,"revoke_http_access":16,"revoke_network_access":13,"revoke_privileg":13,"rich":0,"right":[1,3],"robust":[3,4],"role":[3,4],"romanc":[3,4],"round":[5,7],"roundtrip":5,"rout":20,"row":[5,18],"run":[0,6,7,8,9,10,11,12,13,16,18,19,20],"run_pipelin":[5,7],"run_sql":[2,5,14],"runsql":[2,7,14],"runtim":[2,16],"sagittariu":[5,14],"sale":[1,7,9,15,16],"same":[3,4,5,6,7,8,9,10,12,13,14,20],"sampl":[3,4,7,8,13,18],"sample_row":18,"sanic":20,"save":[5,6,14],"scalabl":[3,4,5],"scale":[3,4],"schedul":19,"schema":[3,4,5,10,13,14,15,18],"sci":[3,4],"scienc":[5,9,14],"scientif":9,"scientist":[3,4,9],"scope":[5,12,14],"score":19,"script":[5,7,8,11,12,13,19,20],"seamlessli":[3,4],"search":[0,5,14,19],"second":[5,9,14,20],"secret":[10,12,20],"secur":[3,4,5,10,12],"see":[3,5,11,12,14,16,18,19,20],"seed":[5,14,15,19],"seek":[5,14],"select":[0,1,5,6,8,9,10,11,13,14,15,16,17,18,19,20],"select_ai":[2,3,4,5,6,7,8,9,10,12,13,14,15,16,17,18,19],"select_ai_admin_password":[13,16],"select_ai_admin_us":[13,16],"select_ai_agent_nam":[3,4],"select_ai_connection_test":11,"select_ai_db_connect_str":[3,4,5,6,7,8,9,10,11,13,14,16,18,19,20],"select_ai_db_password":[7,11,13,20],"select_ai_db_us":[7,11,13,16,20],"select_ai_max_work":7,"select_ai_password":[3,4,5,6,7,8,9,10,11,13,14,18,19,20],"select_ai_pool_incr":[7,20],"select_ai_pool_max":[7,20],"select_ai_pool_min":[7,20],"select_ai_profile_nam":[3,4,7],"select_ai_task_nam":[3,4],"select_ai_team_nam":[3,4],"select_ai_tool_nam":[3,4],"select_ai_us":[3,4,5,6,7,8,9,10,11,13,14,16,18,19,20],"select_ai_wallet_loc":6,"select_ai_wallet_password":6,"select_ai_worker_count":7,"self":[5,20],"sell":1,"semant":19,"send":[3,5,7,14,15,18],"sender":[3,4],"sensit":[5,15],"sent":[2,3,4,5,7,14,15,16,18],"separ":[7,13,16,19],"sequenc":15,"sequenti":[3,4],"seri":[3,4,9],"serializ":[3,4],"serv":[3,4],"server":[3,4,13,20],"servic":[0,3,4,5,7,8,10,12,14,20],"session":[0,3,4,5,6,12,14],"set":[1,3,4,5,6,9,11,14,15,17,18,19,20],"set_attribut":[0,3,4,5,9,14,19],"setup":[3,13],"sever":[5,9,12,14,19],"sh":[5,14,15,16,18],"shall":1,"shape":9,"share":[3,4,6,18,20],"shell":6,"short":8,"shot":6,"should":[2,3,4,5,7,8,9,10,12,13,14,15,16,17,18,19,20],"show":[0,2,3,4,6,8,9,10,12,18],"show_prompt":[2,5,14],"show_sql":[2,5,7,9,14,16,20],"shown":8,"showprompt":2,"showsql":[2,5,7],"shut":20,"shutdown":[12,20],"side":18,"signific":9,"similar":[3,19],"similarity_threshold":19,"similarli":13,"simpl":[6,12,20],"simul":5,"sinc":[5,14],"sing":[],"singl":[0,3,4,5,7,13,14,19,20],"size":[0,6,8,19],"sk":10,"sky":[5,14],"slack":[3,4],"slack_channel":[],"slow":20,"small":20,"smoke":0,"smtp":[3,4,13],"smtp_host":[3,4],"so":[3,5,6,8,9,11,14,20],"social":9,"societi":9,"softwar":1,"sold":1,"solid":[5,14],"solver":[3,4],"some":[5,9,14],"sonnet":16,"soon":[5,7],"sourc":[3,4,5,6,11,12,13,14,15,17,19,20],"source_languag":[5,14],"source_team":[3,4],"spare":20,"special":[3,4],"specif":[3,4,6,12,13,14,15,16,19],"specifi":[3,4,5,13,14,15,16,17,19],"speed":[3,4],"sphinx":11,"split":[5,14,17,19],"sport":[3,4],"sql":[0,2,3,4,7,8,12,13,15,16,18,20],"sql_id":[5,14],"sql_task":5,"sql_tool":[3,4],"sqltoolparam":[3,4],"stabil":[],"stand":[5,14],"standalon":[8,12,20],"star":[5,14],"starlett":20,"start":[3,4,5,6,7,12,14,19,20],"startup":[12,20],"state":[3,4],"statement":[2,3,4,14],"statist":18,"step":[3,4,12,13,19],"still":19,"stop":[15,20],"stop_token":[5,14,15,19],"storag":[0,3,4,5,6,12,14,19,20],"store":[3,4,5,9,10,12,14,16,19,20],"stori":[3,4],"str":[2,3,4,5,7,9,14,15,16,18,19,20],"strategi":3,"stream":[0,6,15,19,20],"string":[3,4,5,15],"structur":[3,4],"studi":[5,9,14],"style":[0,5,12,17,20],"subcommand":6,"subject":[1,3,4],"sublicens":1,"submit":7,"subprogram":8,"substanti":1,"succe":[3,4,5,10,11,14,19],"summar":[0,2,6,12,17],"summari":[2,3,4,5,12,14],"summarize_movie_task":3,"summary_styl":17,"summaryparam":[0,5,14],"summarystyl":0,"sun":[5,14],"suppli":[3,17],"support":[0,4,5,6,7,14,15,16,17,18,19],"sure":[5,10,14,18,19],"sync":[0,7,8],"sync_ordered_result":0,"sync_queue_work":0,"sync_thread_pool":0,"synchron":[0,4,5,7,9,14,18,19],"synthet":[5,12,14],"synthetic_data_attribut":[5,14,18],"synthetic_data_param":18,"syntheticdataattribut":[0,5,14],"syntheticdataparam":0,"system":[9,11,19],"t":5,"tabl":[0,2,3,5,6,10,12,14,15,19],"table_statist":18,"tabular":2,"tail":20,"take":20,"taken":19,"target":[3,4,5,6,7,10,11,13,14,17,18],"target_languag":[5,14],"task":[0,5,7,8,12,18],"task_attribut":[3,4],"task_don":7,"task_nam":[3,4],"task_name_pattern":[3,4],"taskattribut":[0,3,4],"team":[0,12],"team_nam":[3,4],"team_name_pattern":[3,4],"teamattribut":[0,3,4],"teardown_appcontext":20,"technic":[],"technologi":[3,4,5,14],"temperatur":[5,14,15,19],"temporari":9,"tenanc":10,"tenancy_ocid":10,"tenant1":19,"term":1,"termin":[6,15,20],"test":[0,6,18,19],"test_vector_index":19,"text":[2,3,4,5,6,12,14,15,16,17,19,20],"than":[5,14,19,20],"thank":[5,6,14],"thei":7,"them":[3,5,6,10,14,20],"theoret":[5,14],"thi":[1,2,3,4,5,7,8,9,10,11,13,14,15,16,17,18,19,20],"thin":[3,8],"thing":5,"third":12,"those":8,"thread":[7,20],"threadpoolexecutor":7,"thriller":[3,4],"through":[3,6,9,11,20],"time":[3,4,6,7,9],"timedelta":9,"timeout":20,"timestamp":19,"titl":[3,4,9],"tl":8,"tns_admin":[13,20],"togeth":[3,4,19],"token":[15,17],"tool":[0,6,7,10,12,16,19,20],"tool_input":[3,4],"tool_nam":[3,4],"tool_name_pattern":[3,4],"tool_param":[3,4],"tool_typ":[3,4],"toolattribut":[0,3,4],"toolparam":[0,3,4],"tooltyp":[3,4],"top":7,"topic":[3,4,9],"tort":1,"total":[5,7,8,20],"touch":[5,14],"train":[3,4],"transcript":9,"transform":16,"translat":[0,2,3,4,6,12,15],"trillion":[5,14],"trip":[5,7],"true":[2,3,4,5,7,9,10,14,15,16,18,19],"trust":12,"try":[6,7],"tune":[2,3,4,5,14,15],"tupl":[5,14],"twenti":20,"two":[13,19],"txt":[1,6,11,17],"type":[2,3,4,5,6,16,19],"typic":[5,9,19],"u":[3,4,5,9,14,15,16,17,19],"ultraviolet":[5,14],"under":[1,3,4,6,16,20],"underli":[5,14,19],"understand":[5,9,14,19],"union":[5,14,19],"uniqu":[3,5,14],"unit":19,"univers":[1,5,14],"unless":[5,7,9,19,20],"unmodifi":1,"unset":19,"until":[9,20],"up":[3,4,7,9,20],"upcom":6,"updat":[0,9,15],"updated_attribut":19,"upgrad":11,"upl":1,"upper_port":13,"uri":[0,2,3,4,5,6,14,19],"url":[3,4,5,14,16,17],"us":[0,1,2,5,6,7,8,9,10,11,12,13,14,15,17,18],"usag":[5,9],"user":[2,3,4,5,6,7,8,9,10,11,13,14,16,18,19,20],"user_ocid":10,"user_prompt":18,"user_queri":[3,4],"usernam":[3,4,10,16],"usual":[3,5,9,14,19,20],"utc":19,"util":[3,4],"uuid":[3,4],"uuid4":[3,4],"uvicorn":20,"v":[5,14],"v1":16,"v3":19,"valid":[0,6,15,20],"validate_config":10,"valu":[3,4,5,6,8,9,10,14,15,16,17,18,19,20],"valuabl":9,"vari":16,"variabl":[0,3,4,6,8,11,13,20],"varieti":[3,4],"variou":[3,4,5,14],"vecpipelin":19,"vector":[3,4,6,8,10,12,15,16],"vector_db_credential_nam":19,"vector_db_endpoint":19,"vector_db_provid":19,"vector_dimens":19,"vector_distance_metr":19,"vector_index":19,"vector_index_attribut":19,"vector_index_nam":[5,14,15,19],"vector_table_nam":19,"vectordbprovid":19,"vectordistancemetr":19,"vectorindex":[0,3,4],"vectorindexattribut":0,"vectorindexnotfounderror":19,"venv":11,"verifi":11,"version":[0,1,11],"versu":17,"via":[5,14],"view":[5,6,14,15,20],"virtual":11,"visibl":[5,6,9,14,19],"wa":19,"wai":[5,14,20],"wait":[0,3,4,5,6,7,19],"wait_for_complet":19,"wait_timeout":20,"waiting_for_human":[3,4],"wallet":[0,6,20],"wallet_loc":8,"wallet_password":8,"want":[2,3,4,5,6,10,11,14,19],"war":[3,4],"warranti":1,"water":[5,14],"wave":[5,14],"we":[3,4,5,10],"web":[8,12,14],"web_search_ag":[3,4],"web_search_task":[3,4],"web_search_team":[3,4],"web_search_tool":[3,4],"websearch":[3,4],"websit":[3,4],"were":[9,13],"western":[3,4],"what":[0,2,3,4,5,6,9,14,15,17,20],"when":[2,4,5,6,7,9,10,13,14,15,16,17,18,19,20],"where":[3,4,5,7,9,11,14,19,20],"whether":[1,3,4,6,8,15,19],"which":[0,1,3,4,5,8,9,14,15,16,17,19],"while":[5,7,9,10,14,16,19],"while_serv":20,"who":[6,13,18],"whose":3,"why":9,"wide":[3,4,14,20],"wiki":[5,14,17],"wikipedia":[5,14,17],"window":11,"wire":3,"within":[3,4,5,19],"without":[1,2,3,4,5,6,7,8,9,10,14,19],"word":17,"work":[1,3,4,6,7,8,12,18,20],"worker":[7,8,20],"worker_count":7,"worker_exit":20,"workflow":[0,3,4,5,6,9,12,15],"workload":[3,4,7,12,13,20],"world":[3,4],"wrap":13,"write":[3,4,5,6,18],"written":[3,4],"wsgi":20,"www":[3,4],"x":[5,14,16],"xai":16,"xai_api_kei":16,"xai_credenti":16,"xai_profil":16,"xxx":10,"yield":20,"you":[0,2,3,4,5,6,8,9,10,11,14,15,16,17,19,20],"your":[3,4,7,10,11,16,19,20],"your_db_dsn":8,"your_db_password":8,"your_db_us":8,"zip":7,"\u00aa":1,"\u00b2":1,"\u00b3":1,"\u00b5":1,"\u00b9":1,"\u00ba":1,"\u00bc":1,"\u00bd":1,"\u00be":1,"\u03c9":1,"\u215b":1,"\u215c":1,"\u215d":1,"\u215e":1},"titles":["select_ai documentation","<no title>","1. Supported Actions","1. Tool","1. AsyncTool","1. Async profile lifecycle","1. Command line interface","1. Concurrent prompt processing","3. Connecting to Oracle Database","1. Conversation Object model","1. Create credential","2. Installing select_ai","1. Introduction to Select AI for Python","1. Grant privilege","1. Profile lifecycle","1. ProfileAttributes","1. Provider","1. Inline content","1. Generation modes","1. Vector Index","1. Using select_ai with Python web frameworks"],"titleterms":{"":[3,4],"access":13,"action":[0,2,5,14],"agent":[0,3,4],"ai":[0,2,3,4,10,12,16],"an":2,"anthropicprovid":16,"api":[4,5,9,10,12,14,16,18,19],"async":[0,4,5,9,10,16,17,18,19],"async_as_complet":7,"async_gath":7,"async_pipelin":7,"async_queue_work":7,"asyncag":4,"asyncconvers":9,"asynchron":[5,8,12,20],"asyncprofil":5,"asynctask":4,"asyncteam":4,"asynctool":4,"asyncvectorindex":19,"attribut":[0,15,19],"awsprovid":16,"azureprovid":16,"base":14,"behavior":20,"build":12,"can":12,"chat":[5,6,9,14],"choos":2,"chunkprocessingmethod":17,"class":16,"cli":6,"cohereprovid":16,"command":[0,6],"common":[5,14,15],"complet":[],"concept":12,"concurr":[0,5,7],"connect":[6,8,11],"content":17,"control":15,"convers":[0,9],"conversationattribut":9,"core":12,"creat":[3,4,9,10,14,19],"creation":5,"credenti":[0,10],"data":[0,18],"databas":8,"delet":[5,9,10,14,19],"depend":[11,20],"disabl":16,"django":20,"document":[0,11],"each":3,"enabl":16,"endpoint":20,"environ":7,"exampl":[2,3,4,15,16,20],"expect":10,"explain":[5,14],"export":[3,4],"extractivenesslevel":17,"fastapi":20,"fetch":19,"flask":20,"format":10,"framework":[0,20],"from":17,"gener":[15,18],"get":0,"googleprovid":16,"gpt":[3,4],"grant":13,"group":15,"handl":20,"health":8,"helper":[3,4],"huggingfaceprovid":16,"import":[3,4],"index":[0,19],"inlin":17,"input":[],"instal":[11,20],"interact":6,"interfac":[0,6],"introduct":12,"lifecycl":[3,4,5,14],"line":[0,6],"list":[3,4,5,9,14,15,19],"map":2,"method":2,"mode":18,"model":[3,4,9,14,19],"multi":18,"narrat":14,"network":13,"object":[9,14,15,17,19],"ocigenaiprovid":16,"openai":[3,4],"openaiprovid":16,"option":6,"oracl":8,"oraclevectorindexattribut":19,"order":[],"paramet":[17,18],"pattern":20,"pipelin":5,"pool":[7,8,20],"privileg":[0,13],"process":[0,7],"profil":[0,5,6,14,15],"profileattribut":15,"prompt":[0,5,7,14],"provid":[0,10,16],"py":7,"python":[12,20],"queue":[],"rag":19,"recip":7,"repl":6,"request":20,"requir":[11,15],"reus":[5,14],"revok":13,"run":[3,4,5,14],"search":[3,4],"select":[2,3,4,12],"select_ai":[0,11,20],"servic":16,"session":9,"show":[5,14],"sing":[],"singl":18,"size":[7,20],"smoke":11,"sql":[5,6,14],"start":0,"storag":17,"stream":[2,5,14],"summar":[5,14],"summari":[0,6,7,17],"summaryparam":17,"summarystyl":17,"support":[2,3,12],"sync":[4,10,16,18],"sync_ordered_result":7,"sync_queue_work":7,"sync_thread_pool":7,"synchron":[8,12,20],"synthet":[0,18],"syntheticdataattribut":18,"syntheticdataparam":18,"tabl":18,"task":[3,4],"team":[3,4],"test":11,"tool":[3,4],"translat":[5,14],"updat":[5,14,19],"uri":17,"us":[3,4,16,19,20],"valid":17,"variabl":7,"vector":[0,19],"vectorindex":19,"vectorindexattribut":19,"version":12,"wait":20,"wallet":8,"web":[0,3,4,20],"what":12,"when":3,"worker":[],"you":12}}) +Search.setIndex({"alltitles":{"A2A CLI commands":[[7,"id3"]],"A2A Integration":[[0,"a2a-integration"]],"A2A commands":[[7,"a2a-commands"]],"A2A deployment examples":[[7,"a2a-deployment-examples"]],"A2UI database-connection form":[[2,"a2ui-database-connection-form"]],"AI Agent":[[0,"ai-agent"]],"AI agent examples":[[3,"ai-agent-examples"]],"AI provider and expected credential format":[[11,null]],"API Reference":[[0,"api-reference"]],"AWS Bedrock":[[16,"aws-bedrock"]],"AWSProvider":[[16,"awsprovider"]],"Actions":[[0,"actions"]],"Additional instructions":[[15,"additional-instructions"]],"Agent":[[3,"id1"]],"Agent Card discovery":[[2,"agent-card-discovery"]],"Agent Card generation":[[7,"agent-card-generation"]],"Agent history":[[3,"agent-history"]],"Agent history results":[[4,"agent-history-results"]],"Agent2Agent (A2A)":[[2,null]],"AnthropicProvider":[[16,"anthropicprovider"]],"Async AI Agent":[[0,"async-ai-agent"]],"Async AI agent examples":[[5,"async-ai-agent-examples"]],"Async Profile":[[0,"async-profile"]],"Async Profile creation":[[6,"async-profile-creation"]],"Async RAG using vector index":[[18,"async-rag-using-vector-index"]],"Async agent history":[[5,"async-agent-history"]],"Async chat":[[6,"async-chat"]],"Async concurrent SQL":[[6,"async-concurrent-sql"]],"Async create vector index":[[18,"async-create-vector-index"]],"Async explain SQL":[[6,"async-explain-sql"]],"Async fetch vector index":[[18,"async-fetch-vector-index"]],"Async list vector index":[[18,"async-list-vector-index"]],"Async pipeline":[[6,"async-pipeline"]],"Async profile actions":[[6,"async-profile-actions"]],"Async profile lifecycle":[[6,null]],"Async request-level profile attribute overrides":[[6,"async-request-level-profile-attribute-overrides"]],"Async run SQL":[[6,"async-run-sql"]],"Async show SQL":[[6,"async-show-sql"]],"Async show prompt":[[6,"async-show-prompt"]],"Async streaming chat":[[6,"async-streaming-chat"]],"Async update vector index attributes":[[18,"async-update-vector-index-attributes"]],"AsyncAgent":[[5,"asyncagent"]],"AsyncProfile API":[[6,"asyncprofile-api"]],"AsyncTask":[[5,"asynctask"]],"AsyncTeam":[[5,"asyncteam"]],"AsyncTool":[[5,"asynctool"]],"AsyncVectorIndex API":[[18,"asyncvectorindex-api"]],"Asynchronous connection":[[10,"asynchronous-connection"]],"Attribute groups":[[15,"attribute-groups"]],"Authentication and TLS":[[7,"authentication-and-tls"]],"Azure OpenAI":[[16,"azure-openai"]],"AzureProvider":[[16,"azureprovider"]],"Base Profile API":[[14,"base-profile-api"]],"CLI command summary":[[7,"id4"]],"CLI-based deployment on any cloud":[[2,"cli-based-deployment-on-any-cloud"]],"Chat":[[14,"chat"]],"Chat REPL commands":[[7,"id2"]],"Choose a topology":[[2,"choose-a-topology"]],"Choose how connections are configured":[[2,"choose-how-connections-are-configured"]],"Cloud provider profile samples":[[16,"cloud-provider-profile-samples"]],"Clustered A2A gateway and worker":[[2,"clustered-a2a-gateway-and-worker"]],"Clustered Google Cloud deployment":[[2,"clustered-google-cloud-deployment"]],"Clustered deployment with a fixed connection":[[2,"clustered-deployment-with-a-fixed-connection"]],"Clustered gateway and worker":[[7,"clustered-gateway-and-worker"]],"Clustered option reference":[[7,"clustered-option-reference"]],"CohereProvider":[[16,"cohereprovider"]],"Command Line Interface":[[0,"command-line-interface"]],"Command line interface":[[7,null]],"Command summary":[[7,"command-summary"]],"Common A2A operations":[[2,"id6"]],"Common async profile actions":[[6,"id1"]],"Common profile actions":[[14,"id1"]],"Common required attributes":[[15,"common-required-attributes"]],"Compatibility and requirements":[[8,null]],"Concurrent Prompt Processing":[[0,"concurrent-prompt-processing"]],"Concurrent prompt processing":[[9,null]],"Connect and list your profiles":[[6,"connect-and-list-your-profiles"],[14,"connect-and-list-your-profiles"]],"Connecting to Oracle Database":[[10,null]],"Connection Pool":[[10,"connection-pool"]],"Connection health":[[10,"connection-health"]],"Connection options":[[7,"connection-options"],[7,"id1"]],"Connection smoke test":[[12,"connection-smoke-test"]],"Connection values and forms":[[7,"connection-values-and-forms"]],"Conversation":[[0,"conversation"]],"Core concepts":[[13,"core-concepts"]],"Create Agent":[[3,"create-agent"],[5,"create-agent"]],"Create Async API":[[11,"create-async-api"]],"Create Profile":[[14,"create-profile"]],"Create Sync API":[[11,"create-sync-api"]],"Create Task":[[3,"create-task"],[5,"create-task"]],"Create Tool":[[3,"create-tool"],[5,"create-tool"]],"Create credential":[[11,"create-credential"]],"Create vector index":[[18,"create-vector-index"]],"Credential":[[0,"credential"]],"Credential access":[[11,"credential-access"]],"Database call timeout":[[2,"database-call-timeout"]],"Delete Async API":[[11,"delete-async-api"]],"Delete Async Profile":[[6,"delete-async-profile"]],"Delete Profile":[[14,"delete-profile"]],"Delete Sync API":[[11,"delete-sync-api"]],"Delete credential":[[11,"delete-credential"]],"Delete vector index":[[18,"delete-vector-index"]],"Disable AI service provider":[[16,"disable-ai-service-provider"]],"Disable using Async API":[[16,"disable-using-async-api"]],"Disable using Sync API":[[16,"disable-using-sync-api"]],"Django example":[[19,"django-example"]],"Dynamic connection form":[[2,"id7"]],"Dynamic standalone connection":[[7,"dynamic-standalone-connection"]],"Dynamic standalone sessions":[[2,"dynamic-standalone-sessions"]],"Enable AI service provider":[[16,"enable-ai-service-provider"]],"Enable and disable AsyncProfile":[[6,"enable-and-disable-asyncprofile"]],"Enable and disable Profile":[[14,"enable-and-disable-profile"]],"Enable using Async API":[[16,"enable-using-async-api"]],"Enable using Sync API":[[16,"enable-using-sync-api"]],"End-user OAuth and session ownership":[[2,"end-user-oauth-and-session-ownership"]],"Enumerations":[[4,"enumerations"]],"Environment variables":[[9,"environment-variables"]],"Error handling":[[4,"id1"]],"Errors, sessions, and result objects":[[4,null]],"Examples":[[16,"examples"]],"Explain SQL":[[14,"explain-sql"]],"Export and Import Team":[[3,"export-and-import-team"],[5,"export-and-import-team"]],"FastAPI asynchronous endpoints":[[19,"fastapi-asynchronous-endpoints"]],"FastAPI synchronous endpoints":[[19,"fastapi-synchronous-endpoints"]],"Feature requirements":[[8,"id1"]],"Fetch vector index":[[18,"fetch-vector-index"]],"Filter by name":[[6,"filter-by-name"],[14,"filter-by-name"]],"Find and reuse a shared profile":[[6,"find-and-reuse-a-shared-profile"],[14,"find-and-reuse-a-shared-profile"]],"Fixed database connection":[[2,"fixed-database-connection"]],"Fixed standalone connection":[[7,"fixed-standalone-connection"]],"Flask example":[[19,"flask-example"]],"Framework patterns":[[19,"framework-patterns"]],"Gateway discovery":[[7,"gateway-discovery"]],"Gateway-to-worker HTTP and TLS":[[2,"gateway-to-worker-http-and-tls"]],"Gateway-to-worker TLS options":[[2,"id8"]],"Generation controls":[[15,"generation-controls"]],"Getting Started":[[0,"getting-started"]],"Give async feedback on generated SQL":[[6,"give-async-feedback-on-generated-sql"]],"Give feedback on generated SQL":[[14,"give-feedback-on-generated-sql"]],"Google Cloud A2A deployment files":[[2,"id9"]],"Google Cloud deployment":[[2,"google-cloud-deployment"]],"Google Gemini (GCP)":[[16,"google-gemini-gcp"]],"GoogleProvider":[[16,"googleprovider"]],"HuggingFaceProvider":[[16,"huggingfaceprovider"]],"Inspect a team":[[3,"inspect-a-team"],[5,"inspect-a-team"]],"Inspect and run a tool":[[3,"inspect-and-run-a-tool"],[5,"inspect-and-run-a-tool"]],"Install and configure Python":[[17,"install-and-configure-python"]],"Install dependencies":[[19,"install-dependencies"]],"Install documentation dependencies":[[12,"install-documentation-dependencies"]],"Installation and prerequisites":[[2,"installation-and-prerequisites"]],"Installation requirements":[[12,"installation-requirements"]],"Installing select_ai":[[12,null]],"Interactive chat":[[7,"interactive-chat"]],"Introduction to Select AI for Python":[[13,null]],"Lifecycle helpers":[[3,"lifecycle-helpers"],[5,"lifecycle-helpers"]],"List Agents":[[5,"list-agents"]],"List Tasks":[[3,"list-tasks"],[5,"list-tasks"]],"List Teams":[[5,"list-teams"]],"List Tools":[[3,"list-tools"],[5,"list-tools"]],"List profiles":[[14,"list-profiles"]],"List profiles asynchronously":[[6,"list-profiles-asynchronously"]],"List vector index":[[18,"list-vector-index"]],"Narrate":[[14,"narrate"]],"New and expanded APIs":[[1,"new-and-expanded-apis"]],"OCIGenAIProvider":[[16,"ocigenaiprovider"]],"Object definitions":[[3,"object-definitions"],[5,"object-definitions"]],"Object list examples":[[15,"object-list-examples"]],"OpenAIProvider":[[16,"openaiprovider"]],"OracleVectorIndexAttributes":[[18,"oraclevectorindexattributes"]],"Persistent Oracle-backed state":[[2,"persistent-oracle-backed-state"]],"Pipeline compatibility and results":[[6,"pipeline-compatibility-and-results"]],"Pool sizing":[[9,"pool-sizing"],[19,"pool-sizing"]],"Pool wait behavior":[[19,"pool-wait-behavior"]],"Prepare the database account":[[17,"prepare-the-database-account"]],"Private database CA certificates":[[2,"private-database-ca-certificates"]],"Privileges":[[0,"privileges"]],"Profile":[[0,"profile"]],"Profile API":[[14,"profile-api"]],"Profile Attributes":[[0,"profile-attributes"]],"Profile Object Model":[[14,"profile-object-model"]],"Profile actions":[[14,"profile-actions"]],"Profile attribute groups":[[15,"id1"]],"Profile commands":[[7,"profile-commands"]],"Profile lifecycle":[[14,null]],"ProfileAttributes":[[15,null]],"Protocol compatibility":[[2,"protocol-compatibility"]],"Provider":[[0,"provider"],[16,null],[16,"id2"]],"Provider classes":[[16,"id3"]],"Public gateway HTTPS":[[2,"public-gateway-https"]],"RAG using vector index":[[18,"rag-using-vector-index"]],"Recipe summary":[[9,"recipe-summary"]],"Request handling":[[19,"request-handling"]],"Request-level profile attribute overrides":[[14,"request-level-profile-attribute-overrides"]],"Reuse Async Profile":[[6,"reuse-async-profile"]],"Reuse Profile":[[14,"reuse-profile"]],"Run SQL":[[14,"run-sql"]],"Run Team":[[3,"run-team"],[5,"run-team"]],"Run the example":[[17,"run-the-example"]],"Run your first SQL query":[[17,null]],"SQL commands":[[7,"sql-commands"]],"Select AI Agent Tools":[[3,"id2"]],"Select AI Async Agent Tools":[[5,"id3"]],"Session isolation, routing, and cleanup":[[2,"session-isolation-routing-and-cleanup"]],"Session lifetime and timeouts":[[7,"session-lifetime-and-timeouts"]],"Session objects":[[4,"session-objects"]],"Share a profile":[[14,"share-a-profile"]],"Share a profile asynchronously":[[6,"share-a-profile-asynchronously"]],"Share a team":[[3,"share-a-team"]],"Share a vector index":[[18,"share-a-vector-index"]],"Share an async team":[[5,"share-an-async-team"]],"Sharing and ownership":[[0,"sharing-and-ownership"]],"Show Prompt":[[14,"show-prompt"]],"Show SQL":[[14,"show-sql"]],"Standalone A2A server":[[2,"standalone-a2a-server"]],"Standalone Google Cloud deployment":[[2,"standalone-google-cloud-deployment"]],"Streaming chat":[[14,"streaming-chat"]],"Summarize":[[6,"summarize"],[14,"summarize"]],"Summary":[[0,"summary"]],"Supervised teams":[[3,"supervised-teams"],[5,"supervised-teams"]],"Supported Python versions":[[13,"supported-python-versions"]],"Supported Tools":[[3,"supported-tools"]],"Supported combinations":[[2,"id3"]],"Sync and async agent APIs":[[5,null]],"Synchronous and asynchronous APIs":[[13,"synchronous-and-asynchronous-apis"]],"Synchronous connection":[[10,"synchronous-connection"]],"Synthetic Data":[[0,"synthetic-data"]],"Task":[[3,"task"]],"Tasks, polling, streaming, and cancellation":[[2,"tasks-polling-streaming-and-cancellation"]],"Team":[[3,"team"]],"Tool":[[3,null]],"Tool selection":[[3,"tool-selection"]],"Translate":[[6,"translate"],[14,"translate"]],"Troubleshooting and security notes":[[2,"troubleshooting-and-security-notes"]],"Update Async Profile":[[6,"update-async-profile"]],"Update Profile":[[14,"update-profile"]],"Update vector index attributes":[[18,"update-vector-index-attributes"]],"Upgrade considerations":[[1,"upgrade-considerations"]],"Using select_ai with Python web frameworks":[[19,null]],"Vector Index":[[0,"vector-index"],[18,null]],"VectorIndex API":[[18,"vectorindex-api"]],"VectorIndex Object Model":[[18,"vectorindex-object-model"]],"VectorIndexAttributes":[[18,"vectorindexattributes"]],"Waiting and timeouts":[[17,"waiting-and-timeouts"]],"Wallet connections":[[10,"wallet-connections"]],"Wallet-based database connections":[[2,"wallet-based-database-connections"]],"Web Frameworks":[[0,"web-frameworks"]],"Web Search Agent using OpenAI\u2019s GPT model":[[3,"web-search-agent-using-openai-s-gpt-model"],[5,"web-search-agent-using-openai-s-gpt-model"]],"What you can build":[[13,"what-you-can-build"]],"What\u2019s new in 1.5.0":[[1,null]],"When to use each tool":[[3,"id3"]],"Why clustered deployments use Consul":[[2,"why-clustered-deployments-use-consul"]],"Worker registration":[[7,"worker-registration"]],"async_as_completed.py":[[9,"async-as-completed-py"]],"async_gather.py":[[9,"async-gather-py"]],"async_pipeline.py":[[9,"async-pipeline-py"]],"async_queue_workers.py":[[9,"async-queue-workers-py"]],"select_ai documentation":[[0,null]],"select_ai installation":[[12,"select-ai-installation"]],"sync_ordered_results.py":[[9,"sync-ordered-results-py"]],"sync_queue_workers.py":[[9,"sync-queue-workers-py"]],"sync_thread_pool.py":[[9,"sync-thread-pool-py"]]},"docnames":["index","release_notes","user_guide/a2a","user_guide/agent","user_guide/api_reference","user_guide/async_agent","user_guide/async_profile","user_guide/cli","user_guide/compatibility","user_guide/concurrent_prompt_processing","user_guide/connection","user_guide/credential","user_guide/installation","user_guide/introduction","user_guide/profile","user_guide/profile_attributes","user_guide/provider","user_guide/quickstart","user_guide/vector_index","user_guide/web_frameworks"],"envversion":{"sphinx":66,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2,"sphinx.ext.intersphinx":1},"filenames":["index.rst","release_notes.rst","user_guide/a2a.rst","user_guide/agent.rst","user_guide/api_reference.rst","user_guide/async_agent.rst","user_guide/async_profile.rst","user_guide/cli.rst","user_guide/compatibility.rst","user_guide/concurrent_prompt_processing.rst","user_guide/connection.rst","user_guide/credential.rst","user_guide/installation.rst","user_guide/introduction.rst","user_guide/profile.rst","user_guide/profile_attributes.rst","user_guide/provider.rst","user_guide/quickstart.rst","user_guide/vector_index.rst","user_guide/web_frameworks.rst"],"indexentries":{},"objects":{"select_ai":[[16,0,1,"","AWSProvider"],[16,0,1,"","AnthropicProvider"],[6,0,1,"","AsyncProfile"],[18,0,1,"","AsyncVectorIndex"],[16,0,1,"","AzureProvider"],[14,0,1,"","BaseProfile"],[16,0,1,"","CohereProvider"],[16,0,1,"","GoogleProvider"],[16,0,1,"","HuggingFaceProvider"],[16,0,1,"","OCIGenAIProvider"],[16,0,1,"","OpenAIProvider"],[18,0,1,"","OracleVectorIndexAttributes"],[14,0,1,"","Profile"],[15,0,1,"","ProfileAttributes"],[16,0,1,"","Provider"],[18,0,1,"","VectorIndex"],[18,0,1,"","VectorIndexAttributes"],[4,4,0,"-","errors"]],"select_ai.AWSProvider":[[16,1,1,"","aws_apiformat"],[16,2,1,"","create"],[16,2,1,"","dict"],[16,1,1,"","embedding_model"],[16,2,1,"","json"],[16,2,1,"","key_alias"],[16,2,1,"","keys"],[16,1,1,"","model"],[16,2,1,"","profile_dict"],[16,1,1,"","provider_endpoint"],[16,1,1,"","provider_name"],[16,1,1,"","region"],[16,2,1,"","should_include_provider_endpoint"]],"select_ai.AnthropicProvider":[[16,2,1,"","create"],[16,2,1,"","dict"],[16,1,1,"","embedding_model"],[16,2,1,"","json"],[16,2,1,"","key_alias"],[16,2,1,"","keys"],[16,1,1,"","model"],[16,2,1,"","profile_dict"],[16,1,1,"","provider_endpoint"],[16,1,1,"","provider_name"],[16,1,1,"","region"],[16,2,1,"","should_include_provider_endpoint"]],"select_ai.AsyncProfile":[[6,2,1,"","add_negative_feedback"],[6,2,1,"","add_positive_feedback"],[6,2,1,"","chat"],[6,2,1,"","chat_session"],[6,2,1,"","create"],[6,2,1,"","delete"],[6,2,1,"","delete_feedback"],[6,2,1,"","delete_profile"],[6,2,1,"","disable"],[6,2,1,"","enable"],[6,2,1,"","explain_sql"],[6,2,1,"","fetch"],[6,2,1,"","generate"],[6,2,1,"","generate_synthetic_data"],[6,2,1,"","get_attributes"],[6,2,1,"","grant_access"],[6,2,1,"","list"],[6,2,1,"","narrate"],[6,3,1,"","qualified_name"],[6,2,1,"","revoke_access"],[6,2,1,"","run_pipeline"],[6,2,1,"","run_sql"],[6,2,1,"","set_attribute"],[6,2,1,"","set_attributes"],[6,2,1,"","show_prompt"],[6,2,1,"","show_sql"],[6,2,1,"","summarize"],[6,2,1,"","translate"]],"select_ai.AsyncVectorIndex":[[18,2,1,"","create"],[18,2,1,"","delete"],[18,2,1,"","delete_index"],[18,2,1,"","disable"],[18,2,1,"","enable"],[18,2,1,"","fetch"],[18,2,1,"","get_attributes"],[18,2,1,"","get_next_refresh_timestamp"],[18,2,1,"","get_profile"],[18,2,1,"","grant_access"],[18,2,1,"","list"],[18,3,1,"","qualified_name"],[18,2,1,"","revoke_access"],[18,2,1,"","set_attribute"],[18,2,1,"","set_attributes"]],"select_ai.AzureProvider":[[16,1,1,"","azure_deployment_name"],[16,1,1,"","azure_embedding_deployment_name"],[16,1,1,"","azure_resource_name"],[16,2,1,"","create"],[16,2,1,"","dict"],[16,1,1,"","embedding_model"],[16,2,1,"","json"],[16,2,1,"","key_alias"],[16,2,1,"","keys"],[16,1,1,"","model"],[16,2,1,"","profile_dict"],[16,1,1,"","provider_endpoint"],[16,1,1,"","provider_name"],[16,1,1,"","region"],[16,2,1,"","should_include_provider_endpoint"]],"select_ai.BaseProfile":[[14,3,1,"","qualified_name"]],"select_ai.CohereProvider":[[16,2,1,"","create"],[16,2,1,"","dict"],[16,1,1,"","embedding_model"],[16,2,1,"","json"],[16,2,1,"","key_alias"],[16,2,1,"","keys"],[16,1,1,"","model"],[16,2,1,"","profile_dict"],[16,1,1,"","provider_endpoint"],[16,1,1,"","provider_name"],[16,1,1,"","region"],[16,2,1,"","should_include_provider_endpoint"]],"select_ai.GoogleProvider":[[16,2,1,"","create"],[16,2,1,"","dict"],[16,1,1,"","embedding_model"],[16,2,1,"","json"],[16,2,1,"","key_alias"],[16,2,1,"","keys"],[16,1,1,"","model"],[16,2,1,"","profile_dict"],[16,1,1,"","provider_endpoint"],[16,1,1,"","provider_name"],[16,1,1,"","region"],[16,2,1,"","should_include_provider_endpoint"]],"select_ai.HuggingFaceProvider":[[16,2,1,"","create"],[16,2,1,"","dict"],[16,1,1,"","embedding_model"],[16,2,1,"","json"],[16,2,1,"","key_alias"],[16,2,1,"","keys"],[16,1,1,"","model"],[16,2,1,"","profile_dict"],[16,1,1,"","provider_endpoint"],[16,1,1,"","provider_name"],[16,1,1,"","region"],[16,2,1,"","should_include_provider_endpoint"]],"select_ai.OCIGenAIProvider":[[16,2,1,"","create"],[16,2,1,"","dict"],[16,1,1,"","embedding_model"],[16,2,1,"","json"],[16,2,1,"","key_alias"],[16,2,1,"","keys"],[16,1,1,"","model"],[16,1,1,"","oci_apiformat"],[16,1,1,"","oci_compartment_id"],[16,1,1,"","oci_endpoint_id"],[16,1,1,"","oci_runtimetype"],[16,2,1,"","profile_dict"],[16,1,1,"","provider_endpoint"],[16,1,1,"","provider_name"],[16,1,1,"","region"],[16,2,1,"","should_include_provider_endpoint"]],"select_ai.OpenAIProvider":[[16,2,1,"","create"],[16,2,1,"","dict"],[16,1,1,"","embedding_model"],[16,2,1,"","json"],[16,2,1,"","key_alias"],[16,2,1,"","keys"],[16,1,1,"","model"],[16,2,1,"","profile_dict"],[16,1,1,"","provider_endpoint"],[16,1,1,"","provider_name"],[16,1,1,"","region"],[16,2,1,"","should_include_provider_endpoint"]],"select_ai.OracleVectorIndexAttributes":[[18,1,1,"","chunk_overlap"],[18,1,1,"","chunk_size"],[18,2,1,"","create"],[18,2,1,"","dict"],[18,1,1,"","enable_sources"],[18,2,1,"","json"],[18,2,1,"","keys"],[18,1,1,"","location"],[18,1,1,"","match_limit"],[18,1,1,"","object_storage_credential_name"],[18,1,1,"","pipeline_name"],[18,1,1,"","profile_name"],[18,1,1,"","refresh_rate"],[18,1,1,"","similarity_threshold"],[18,1,1,"","vector_db_credential_name"],[18,1,1,"","vector_db_endpoint"],[18,1,1,"","vector_db_provider"],[18,1,1,"","vector_dimension"],[18,1,1,"","vector_distance_metric"],[18,1,1,"","vector_table_name"]],"select_ai.Profile":[[14,2,1,"","add_negative_feedback"],[14,2,1,"","add_positive_feedback"],[14,2,1,"","chat"],[14,2,1,"","chat_session"],[14,2,1,"","create"],[14,2,1,"","delete"],[14,2,1,"","delete_feedback"],[14,2,1,"","delete_profile"],[14,2,1,"","disable"],[14,2,1,"","enable"],[14,2,1,"","explain_sql"],[14,2,1,"","fetch"],[14,2,1,"","generate"],[14,2,1,"","generate_synthetic_data"],[14,2,1,"","get_attributes"],[14,2,1,"","grant_access"],[14,2,1,"","list"],[14,2,1,"","narrate"],[14,3,1,"","qualified_name"],[14,2,1,"","revoke_access"],[14,2,1,"","run_sql"],[14,2,1,"","set_attribute"],[14,2,1,"","set_attributes"],[14,2,1,"","show_prompt"],[14,2,1,"","show_sql"],[14,2,1,"","summarize"],[14,2,1,"","translate"]],"select_ai.ProfileAttributes":[[15,1,1,"","additional_instructions"],[15,1,1,"","annotations"],[15,2,1,"","async_create"],[15,1,1,"","case_sensitive_values"],[15,1,1,"","comments"],[15,1,1,"","constraints"],[15,1,1,"","conversation"],[15,2,1,"","create"],[15,1,1,"","credential_name"],[15,2,1,"","dict"],[15,1,1,"","enable_custom_source_uri"],[15,1,1,"","enable_source_offsets"],[15,1,1,"","enable_sources"],[15,1,1,"","enforce_object_list"],[15,2,1,"","json"],[15,2,1,"","keys"],[15,1,1,"","max_tokens"],[15,1,1,"","object_list"],[15,1,1,"","object_list_mode"],[15,1,1,"","provider"],[15,1,1,"","seed"],[15,2,1,"","set_attribute"],[15,1,1,"","source_language"],[15,1,1,"","stop_tokens"],[15,1,1,"","streaming"],[15,1,1,"","target_language"],[15,1,1,"","temperature"],[15,1,1,"","vector_index_name"]],"select_ai.Provider":[[16,2,1,"","create"],[16,2,1,"","dict"],[16,1,1,"","embedding_model"],[16,2,1,"","json"],[16,2,1,"","key_alias"],[16,2,1,"","keys"],[16,1,1,"","model"],[16,2,1,"","profile_dict"],[16,1,1,"","provider_endpoint"],[16,1,1,"","provider_name"],[16,1,1,"","region"],[16,2,1,"","should_include_provider_endpoint"]],"select_ai.VectorIndex":[[18,2,1,"","create"],[18,2,1,"","delete"],[18,2,1,"","delete_index"],[18,2,1,"","disable"],[18,2,1,"","enable"],[18,2,1,"","fetch"],[18,2,1,"","get_attributes"],[18,2,1,"","get_next_refresh_timestamp"],[18,2,1,"","get_profile"],[18,2,1,"","grant_access"],[18,2,1,"","list"],[18,3,1,"","qualified_name"],[18,2,1,"","revoke_access"],[18,2,1,"","set_attribute"],[18,2,1,"","set_attributes"]],"select_ai.VectorIndexAttributes":[[18,1,1,"","chunk_overlap"],[18,1,1,"","chunk_size"],[18,2,1,"","create"],[18,2,1,"","dict"],[18,1,1,"","enable_sources"],[18,2,1,"","json"],[18,2,1,"","keys"],[18,1,1,"","location"],[18,1,1,"","match_limit"],[18,1,1,"","object_storage_credential_name"],[18,1,1,"","pipeline_name"],[18,1,1,"","profile_name"],[18,1,1,"","refresh_rate"],[18,1,1,"","similarity_threshold"],[18,1,1,"","vector_db_credential_name"],[18,1,1,"","vector_db_endpoint"],[18,1,1,"","vector_db_provider"],[18,1,1,"","vector_dimension"],[18,1,1,"","vector_distance_metric"],[18,1,1,"","vector_table_name"]],"select_ai.action":[[4,0,1,"","Action"]],"select_ai.action.Action":[[4,1,1,"","CHAT"],[4,1,1,"","EXPLAINSQL"],[4,1,1,"","FEEDBACK"],[4,1,1,"","NARRATE"],[4,1,1,"","RUNSQL"],[4,1,1,"","SHOWPROMPT"],[4,1,1,"","SHOWSQL"],[4,1,1,"","SUMMARIZE"],[4,1,1,"","TRANSLATE"],[4,2,1,"","capitalize"],[4,2,1,"","casefold"],[4,2,1,"","center"],[4,2,1,"","count"],[4,2,1,"","encode"],[4,2,1,"","endswith"],[4,2,1,"","expandtabs"],[4,2,1,"","find"],[4,2,1,"","format"],[4,2,1,"","format_map"],[4,2,1,"","index"],[4,2,1,"","isalnum"],[4,2,1,"","isalpha"],[4,2,1,"","isascii"],[4,2,1,"","isdecimal"],[4,2,1,"","isdigit"],[4,2,1,"","isidentifier"],[4,2,1,"","islower"],[4,2,1,"","isnumeric"],[4,2,1,"","isprintable"],[4,2,1,"","isspace"],[4,2,1,"","istitle"],[4,2,1,"","isupper"],[4,2,1,"","join"],[4,2,1,"","ljust"],[4,2,1,"","lower"],[4,2,1,"","lstrip"],[4,2,1,"","maketrans"],[4,2,1,"","partition"],[4,2,1,"","removeprefix"],[4,2,1,"","removesuffix"],[4,2,1,"","replace"],[4,2,1,"","rfind"],[4,2,1,"","rindex"],[4,2,1,"","rjust"],[4,2,1,"","rpartition"],[4,2,1,"","rsplit"],[4,2,1,"","rstrip"],[4,2,1,"","split"],[4,2,1,"","splitlines"],[4,2,1,"","startswith"],[4,2,1,"","strip"],[4,2,1,"","swapcase"],[4,2,1,"","title"],[4,2,1,"","translate"],[4,2,1,"","upper"],[4,2,1,"","zfill"]],"select_ai.agent":[[3,0,1,"","Agent"],[3,0,1,"","AgentAttributes"],[5,0,1,"","AsyncAgent"],[5,0,1,"","AsyncTask"],[5,0,1,"","AsyncTaskHistory"],[5,0,1,"","AsyncTeam"],[5,0,1,"","AsyncTeamHistory"],[5,0,1,"","AsyncTool"],[5,0,1,"","AsyncToolHistory"],[3,0,1,"","Task"],[3,0,1,"","TaskAttributes"],[3,0,1,"","TaskHistory"],[3,0,1,"","Team"],[3,0,1,"","TeamAttributes"],[3,0,1,"","TeamHistory"],[3,0,1,"","Tool"],[3,0,1,"","ToolAttributes"],[3,0,1,"","ToolHistory"],[3,0,1,"","ToolParams"]],"select_ai.agent.Agent":[[3,2,1,"","create"],[3,2,1,"","delete"],[3,2,1,"","delete_agent"],[3,2,1,"","disable"],[3,2,1,"","enable"],[3,2,1,"","fetch"],[3,2,1,"","list"],[3,2,1,"","set_attribute"],[3,2,1,"","set_attributes"]],"select_ai.agent.AgentAttributes":[[3,2,1,"","dict"],[3,1,1,"","enable_human_tool"],[3,2,1,"","json"],[3,2,1,"","keys"],[3,1,1,"","profile_name"],[3,1,1,"","role"],[3,1,1,"","supervisor"]],"select_ai.agent.AsyncAgent":[[5,2,1,"","create"],[5,2,1,"","delete"],[5,2,1,"","delete_agent"],[5,2,1,"","disable"],[5,2,1,"","enable"],[5,2,1,"","fetch"],[5,2,1,"","list"],[5,2,1,"","set_attribute"],[5,2,1,"","set_attributes"]],"select_ai.agent.AsyncTask":[[5,2,1,"","create"],[5,2,1,"","delete"],[5,2,1,"","delete_task"],[5,2,1,"","disable"],[5,2,1,"","enable"],[5,2,1,"","fetch"],[5,2,1,"","list"],[5,2,1,"","set_attribute"],[5,2,1,"","set_attributes"]],"select_ai.agent.AsyncTaskHistory":[[5,2,1,"","list"]],"select_ai.agent.AsyncTeam":[[5,2,1,"","create"],[5,2,1,"","delete"],[5,2,1,"","delete_team"],[5,2,1,"","describe_team"],[5,2,1,"","disable"],[5,2,1,"","enable"],[5,2,1,"","export"],[5,2,1,"","export_team"],[5,2,1,"","fetch"],[5,2,1,"","grant_access"],[5,2,1,"","import_team"],[5,2,1,"","list"],[5,2,1,"","list_tools"],[5,2,1,"","revoke_access"],[5,2,1,"","run"],[5,2,1,"","set_attribute"],[5,2,1,"","set_attributes"]],"select_ai.agent.AsyncTeamHistory":[[5,2,1,"","list"]],"select_ai.agent.AsyncTool":[[5,2,1,"","create"],[5,2,1,"","create_built_in_tool"],[5,2,1,"","create_email_notification_tool"],[5,2,1,"","create_pl_sql_tool"],[5,2,1,"","create_rag_tool"],[5,2,1,"","create_slack_notification_tool"],[5,2,1,"","create_sql_tool"],[5,2,1,"","create_websearch_tool"],[5,2,1,"","delete"],[5,2,1,"","delete_tool"],[5,2,1,"","describe_tool"],[5,2,1,"","disable"],[5,2,1,"","enable"],[5,2,1,"","fetch"],[5,2,1,"","list"],[5,2,1,"","run_tool"],[5,2,1,"","set_attribute"],[5,2,1,"","set_attributes"]],"select_ai.agent.AsyncToolHistory":[[5,2,1,"","list"]],"select_ai.agent.Task":[[3,2,1,"","create"],[3,2,1,"","delete"],[3,2,1,"","delete_task"],[3,2,1,"","disable"],[3,2,1,"","enable"],[3,2,1,"","fetch"],[3,2,1,"","list"],[3,2,1,"","set_attribute"],[3,2,1,"","set_attributes"]],"select_ai.agent.TaskAttributes":[[3,2,1,"","dict"],[3,1,1,"","enable_human_tool"],[3,1,1,"","input"],[3,1,1,"","instruction"],[3,2,1,"","json"],[3,2,1,"","keys"],[3,1,1,"","tools"]],"select_ai.agent.TaskHistory":[[3,2,1,"","list"]],"select_ai.agent.Team":[[3,2,1,"","create"],[3,2,1,"","delete"],[3,2,1,"","delete_team"],[3,2,1,"","describe_team"],[3,2,1,"","disable"],[3,2,1,"","enable"],[3,2,1,"","export"],[3,2,1,"","export_team"],[3,2,1,"","fetch"],[3,2,1,"","grant_access"],[3,2,1,"","import_team"],[3,2,1,"","list"],[3,2,1,"","list_tools"],[3,2,1,"","revoke_access"],[3,2,1,"","run"],[3,2,1,"","set_attribute"],[3,2,1,"","set_attributes"]],"select_ai.agent.TeamAttributes":[[3,1,1,"","agents"],[3,2,1,"","dict"],[3,2,1,"","json"],[3,2,1,"","keys"],[3,1,1,"","process"],[3,1,1,"","supervisor_agent"],[3,1,1,"","supervisor_task"]],"select_ai.agent.TeamHistory":[[3,2,1,"","list"]],"select_ai.agent.Tool":[[3,2,1,"","create"],[3,2,1,"","create_built_in_tool"],[3,2,1,"","create_email_notification_tool"],[3,2,1,"","create_pl_sql_tool"],[3,2,1,"","create_rag_tool"],[3,2,1,"","create_slack_notification_tool"],[3,2,1,"","create_sql_tool"],[3,2,1,"","create_websearch_tool"],[3,2,1,"","delete"],[3,2,1,"","delete_tool"],[3,2,1,"","describe_tool"],[3,2,1,"","disable"],[3,2,1,"","enable"],[3,2,1,"","fetch"],[3,2,1,"","list"],[3,2,1,"","run_tool"],[3,2,1,"","set_attribute"],[3,2,1,"","set_attributes"]],"select_ai.agent.ToolAttributes":[[3,2,1,"","create"],[3,2,1,"","dict"],[3,1,1,"","function"],[3,1,1,"","instruction"],[3,2,1,"","json"],[3,2,1,"","keys"],[3,1,1,"","tool_inputs"],[3,1,1,"","tool_params"],[3,1,1,"","tool_type"]],"select_ai.agent.ToolHistory":[[3,2,1,"","list"]],"select_ai.agent.ToolParams":[[3,1,1,"","channel"],[3,2,1,"","create"],[3,1,1,"","credential_name"],[3,2,1,"","dict"],[3,1,1,"","endpoint"],[3,2,1,"","json"],[3,2,1,"","keys"],[3,1,1,"","notification_type"],[3,1,1,"","profile_name"],[3,1,1,"","recipient"],[3,1,1,"","sender"],[3,1,1,"","smtp_host"],[3,1,1,"","subject"]],"select_ai.agent.history":[[4,0,1,"","TaskHistoryEvent"],[4,0,1,"","TeamHistoryEvent"],[4,0,1,"","ToolHistoryEvent"]],"select_ai.agent.history.TaskHistoryEvent":[[4,1,1,"","agent_name"],[4,1,1,"","conversation_params"],[4,2,1,"","dict"],[4,1,1,"","end_date"],[4,1,1,"","input"],[4,2,1,"","json"],[4,2,1,"","keys"],[4,1,1,"","result"],[4,1,1,"","start_date"],[4,1,1,"","state"],[4,1,1,"","task_name"],[4,1,1,"","task_order"],[4,1,1,"","team_exec_id"],[4,1,1,"","team_name"]],"select_ai.agent.history.TeamHistoryEvent":[[4,1,1,"","conversation_id"],[4,2,1,"","dict"],[4,1,1,"","end_date"],[4,2,1,"","json"],[4,2,1,"","keys"],[4,1,1,"","params"],[4,1,1,"","start_date"],[4,1,1,"","state"],[4,1,1,"","team_exec_id"],[4,1,1,"","team_name"]],"select_ai.agent.history.ToolHistoryEvent":[[4,1,1,"","agent_name"],[4,2,1,"","dict"],[4,1,1,"","end_date"],[4,1,1,"","input"],[4,1,1,"","invocation_id"],[4,2,1,"","json"],[4,2,1,"","keys"],[4,1,1,"","output"],[4,1,1,"","start_date"],[4,1,1,"","task_name"],[4,1,1,"","task_order"],[4,1,1,"","team_exec_id"],[4,1,1,"","tool_name"],[4,1,1,"","tool_output"]],"select_ai.agent.tool":[[4,0,1,"","NotificationType"],[4,0,1,"","ToolType"]],"select_ai.agent.tool.NotificationType":[[4,1,1,"","EMAIL"],[4,1,1,"","SLACK"],[4,2,1,"","capitalize"],[4,2,1,"","casefold"],[4,2,1,"","center"],[4,2,1,"","count"],[4,2,1,"","encode"],[4,2,1,"","endswith"],[4,2,1,"","expandtabs"],[4,2,1,"","find"],[4,2,1,"","format"],[4,2,1,"","format_map"],[4,2,1,"","index"],[4,2,1,"","isalnum"],[4,2,1,"","isalpha"],[4,2,1,"","isascii"],[4,2,1,"","isdecimal"],[4,2,1,"","isdigit"],[4,2,1,"","isidentifier"],[4,2,1,"","islower"],[4,2,1,"","isnumeric"],[4,2,1,"","isprintable"],[4,2,1,"","isspace"],[4,2,1,"","istitle"],[4,2,1,"","isupper"],[4,2,1,"","join"],[4,2,1,"","ljust"],[4,2,1,"","lower"],[4,2,1,"","lstrip"],[4,2,1,"","maketrans"],[4,2,1,"","partition"],[4,2,1,"","removeprefix"],[4,2,1,"","removesuffix"],[4,2,1,"","replace"],[4,2,1,"","rfind"],[4,2,1,"","rindex"],[4,2,1,"","rjust"],[4,2,1,"","rpartition"],[4,2,1,"","rsplit"],[4,2,1,"","rstrip"],[4,2,1,"","split"],[4,2,1,"","splitlines"],[4,2,1,"","startswith"],[4,2,1,"","strip"],[4,2,1,"","swapcase"],[4,2,1,"","title"],[4,2,1,"","translate"],[4,2,1,"","upper"],[4,2,1,"","zfill"]],"select_ai.agent.tool.ToolType":[[4,1,1,"","HTTP"],[4,1,1,"","HUMAN"],[4,1,1,"","NOTIFICATION"],[4,1,1,"","RAG"],[4,1,1,"","SQL"],[4,1,1,"","WEBSEARCH"],[4,2,1,"","capitalize"],[4,2,1,"","casefold"],[4,2,1,"","center"],[4,2,1,"","count"],[4,2,1,"","encode"],[4,2,1,"","endswith"],[4,2,1,"","expandtabs"],[4,2,1,"","find"],[4,2,1,"","format"],[4,2,1,"","format_map"],[4,2,1,"","index"],[4,2,1,"","isalnum"],[4,2,1,"","isalpha"],[4,2,1,"","isascii"],[4,2,1,"","isdecimal"],[4,2,1,"","isdigit"],[4,2,1,"","isidentifier"],[4,2,1,"","islower"],[4,2,1,"","isnumeric"],[4,2,1,"","isprintable"],[4,2,1,"","isspace"],[4,2,1,"","istitle"],[4,2,1,"","isupper"],[4,2,1,"","join"],[4,2,1,"","ljust"],[4,2,1,"","lower"],[4,2,1,"","lstrip"],[4,2,1,"","maketrans"],[4,2,1,"","partition"],[4,2,1,"","removeprefix"],[4,2,1,"","removesuffix"],[4,2,1,"","replace"],[4,2,1,"","rfind"],[4,2,1,"","rindex"],[4,2,1,"","rjust"],[4,2,1,"","rpartition"],[4,2,1,"","rsplit"],[4,2,1,"","rstrip"],[4,2,1,"","split"],[4,2,1,"","splitlines"],[4,2,1,"","startswith"],[4,2,1,"","strip"],[4,2,1,"","swapcase"],[4,2,1,"","title"],[4,2,1,"","translate"],[4,2,1,"","upper"],[4,2,1,"","zfill"]],"select_ai.async_profile":[[4,0,1,"","AsyncSession"]],"select_ai.async_profile.AsyncSession":[[4,2,1,"","chat"],[4,2,1,"","explain_sql"],[4,2,1,"","narrate"],[4,2,1,"","run_sql"],[4,2,1,"","show_prompt"],[4,2,1,"","show_sql"]],"select_ai.errors":[[4,5,1,"","AgentAttributesEmptyError"],[4,5,1,"","AgentNotFoundError"],[4,5,1,"","AgentTaskAttributesEmptyError"],[4,5,1,"","AgentTaskNotFoundError"],[4,5,1,"","AgentTeamAttributesEmptyError"],[4,5,1,"","AgentTeamNotFoundError"],[4,5,1,"","AgentToolAttributesEmptyError"],[4,5,1,"","AgentToolNotFoundError"],[4,5,1,"","ConversationNotFoundError"],[4,5,1,"","DatabaseNotConnectedError"],[4,5,1,"","InvalidSQLError"],[4,5,1,"","ProfileAttributesEmptyError"],[4,5,1,"","ProfileExistsError"],[4,5,1,"","ProfileNotFoundError"],[4,5,1,"","SelectAIError"],[4,5,1,"","VectorIndexNotFoundError"]],"select_ai.errors.AgentAttributesEmptyError":[[4,2,1,"","add_note"],[4,1,1,"","args"],[4,2,1,"","with_traceback"]],"select_ai.errors.AgentNotFoundError":[[4,2,1,"","add_note"],[4,1,1,"","args"],[4,2,1,"","with_traceback"]],"select_ai.errors.AgentTaskAttributesEmptyError":[[4,2,1,"","add_note"],[4,1,1,"","args"],[4,2,1,"","with_traceback"]],"select_ai.errors.AgentTaskNotFoundError":[[4,2,1,"","add_note"],[4,1,1,"","args"],[4,2,1,"","with_traceback"]],"select_ai.errors.AgentTeamAttributesEmptyError":[[4,2,1,"","add_note"],[4,1,1,"","args"],[4,2,1,"","with_traceback"]],"select_ai.errors.AgentTeamNotFoundError":[[4,2,1,"","add_note"],[4,1,1,"","args"],[4,2,1,"","with_traceback"]],"select_ai.errors.AgentToolAttributesEmptyError":[[4,2,1,"","add_note"],[4,1,1,"","args"],[4,2,1,"","with_traceback"]],"select_ai.errors.AgentToolNotFoundError":[[4,2,1,"","add_note"],[4,1,1,"","args"],[4,2,1,"","with_traceback"]],"select_ai.errors.ConversationNotFoundError":[[4,2,1,"","add_note"],[4,1,1,"","args"],[4,2,1,"","with_traceback"]],"select_ai.errors.DatabaseNotConnectedError":[[4,2,1,"","add_note"],[4,1,1,"","args"],[4,2,1,"","with_traceback"]],"select_ai.errors.InvalidSQLError":[[4,2,1,"","add_note"],[4,1,1,"","args"],[4,2,1,"","with_traceback"]],"select_ai.errors.ProfileAttributesEmptyError":[[4,2,1,"","add_note"],[4,1,1,"","args"],[4,2,1,"","with_traceback"]],"select_ai.errors.ProfileExistsError":[[4,2,1,"","add_note"],[4,1,1,"","args"],[4,2,1,"","with_traceback"]],"select_ai.errors.ProfileNotFoundError":[[4,2,1,"","add_note"],[4,1,1,"","args"],[4,2,1,"","with_traceback"]],"select_ai.errors.SelectAIError":[[4,2,1,"","add_note"],[4,1,1,"","args"],[4,2,1,"","with_traceback"]],"select_ai.errors.VectorIndexNotFoundError":[[4,2,1,"","add_note"],[4,1,1,"","args"],[4,2,1,"","with_traceback"]],"select_ai.profile":[[4,0,1,"","Session"]],"select_ai.profile.Session":[[4,2,1,"","chat"],[4,2,1,"","explain_sql"],[4,2,1,"","narrate"],[4,2,1,"","run_sql"],[4,2,1,"","show_prompt"],[4,2,1,"","show_sql"]],"select_ai.vector_index":[[4,0,1,"","VectorDBProvider"],[4,0,1,"","VectorDistanceMetric"]],"select_ai.vector_index.VectorDBProvider":[[4,1,1,"","ORACLE"],[4,2,1,"","capitalize"],[4,2,1,"","casefold"],[4,2,1,"","center"],[4,2,1,"","count"],[4,2,1,"","encode"],[4,2,1,"","endswith"],[4,2,1,"","expandtabs"],[4,2,1,"","find"],[4,2,1,"","format"],[4,2,1,"","format_map"],[4,2,1,"","index"],[4,2,1,"","isalnum"],[4,2,1,"","isalpha"],[4,2,1,"","isascii"],[4,2,1,"","isdecimal"],[4,2,1,"","isdigit"],[4,2,1,"","isidentifier"],[4,2,1,"","islower"],[4,2,1,"","isnumeric"],[4,2,1,"","isprintable"],[4,2,1,"","isspace"],[4,2,1,"","istitle"],[4,2,1,"","isupper"],[4,2,1,"","join"],[4,2,1,"","ljust"],[4,2,1,"","lower"],[4,2,1,"","lstrip"],[4,2,1,"","maketrans"],[4,2,1,"","partition"],[4,2,1,"","removeprefix"],[4,2,1,"","removesuffix"],[4,2,1,"","replace"],[4,2,1,"","rfind"],[4,2,1,"","rindex"],[4,2,1,"","rjust"],[4,2,1,"","rpartition"],[4,2,1,"","rsplit"],[4,2,1,"","rstrip"],[4,2,1,"","split"],[4,2,1,"","splitlines"],[4,2,1,"","startswith"],[4,2,1,"","strip"],[4,2,1,"","swapcase"],[4,2,1,"","title"],[4,2,1,"","translate"],[4,2,1,"","upper"],[4,2,1,"","zfill"]],"select_ai.vector_index.VectorDistanceMetric":[[4,1,1,"","COSINE"],[4,1,1,"","DOT"],[4,1,1,"","EUCLIDEAN"],[4,1,1,"","HAMMING"],[4,1,1,"","L2_SQUARED"],[4,1,1,"","MANHATTAN"],[4,2,1,"","capitalize"],[4,2,1,"","casefold"],[4,2,1,"","center"],[4,2,1,"","count"],[4,2,1,"","encode"],[4,2,1,"","endswith"],[4,2,1,"","expandtabs"],[4,2,1,"","find"],[4,2,1,"","format"],[4,2,1,"","format_map"],[4,2,1,"","index"],[4,2,1,"","isalnum"],[4,2,1,"","isalpha"],[4,2,1,"","isascii"],[4,2,1,"","isdecimal"],[4,2,1,"","isdigit"],[4,2,1,"","isidentifier"],[4,2,1,"","islower"],[4,2,1,"","isnumeric"],[4,2,1,"","isprintable"],[4,2,1,"","isspace"],[4,2,1,"","istitle"],[4,2,1,"","isupper"],[4,2,1,"","join"],[4,2,1,"","ljust"],[4,2,1,"","lower"],[4,2,1,"","lstrip"],[4,2,1,"","maketrans"],[4,2,1,"","partition"],[4,2,1,"","removeprefix"],[4,2,1,"","removesuffix"],[4,2,1,"","replace"],[4,2,1,"","rfind"],[4,2,1,"","rindex"],[4,2,1,"","rjust"],[4,2,1,"","rpartition"],[4,2,1,"","rsplit"],[4,2,1,"","rstrip"],[4,2,1,"","split"],[4,2,1,"","splitlines"],[4,2,1,"","startswith"],[4,2,1,"","strip"],[4,2,1,"","swapcase"],[4,2,1,"","title"],[4,2,1,"","translate"],[4,2,1,"","upper"],[4,2,1,"","zfill"]]},"objnames":{"0":["py","class","Python class"],"1":["py","attribute","Python attribute"],"2":["py","method","Python method"],"3":["py","property","Python property"],"4":["py","module","Python module"],"5":["py","exception","Python exception"]},"objtypes":{"0":"py:class","1":"py:attribute","2":"py:method","3":"py:property","4":"py:module","5":"py:exception"},"terms":{"007f":4,"07b9":[3,5],"10_000":19,"120_000":17,"128e":[3,5],"15c":2,"17b":[3,5],"20is":19,"20oci":19,"25e23a25":[3,5],"26ai":[6,8],"3f":19,"40e7":[3,5],"42b":2,"463a":[3,5],"4ed7":[3,5],"4o":16,"70b":16,"7e1":2,"91a":2,"987c":[3,5],"A":[2,3,4,5,6,7,11,12,14,15,16,17,18,19],"AS":[6,14],"After":[2,6,10,12,14,18],"All":[2,3,5,14],"An":[3,6,14,16,18],"At":2,"Both":[2,18],"By":[2,7,18],"Could":[3,5],"Do":[2,3,4,10,16,17,19],"Each":[2,3,6,14,16],"FROM":[6,14,17],"For":[2,3,4,6,7,9,10,12,13,14,15,16,17,18,19],"From":18,"Have":17,"Here":[3,5,6],"How":[3,5,6,9,14,16,17,18],"I":[2,7,16],"INTO":17,"IS":[3,5],"If":[1,2,3,4,5,6,7,11,12,14,15,17,18,19],"In":[2,3,4,5,7,9,10,11,16,17,18,19],"It":[0,2,6,7,9,13,14,17,18],"MY":[3,5,6,14],"More":4,"Most":[10,13,15,16],"My":[3,5],"No":[3,5],"OR":[2,3,5],"On":[2,3,5,6,7,9,12,17],"Other":[3,4,5,17],"Same":8,"Some":8,"That":2,"The":[1,2,3,4,5,6,7,8,9,10,11,13,14,15,16,17,18,19],"Their":[2,5],"Then":[10,12,16],"There":[2,14],"These":[3,5,6,7,12,14,16],"They":[2,3],"This":[2,3,4,5,6,7,8,9,11,12,14,15,16,17,18,19],"To":[2,6,10,12,16,17,18],"We":6,"What":[0,2,3,5,6,7,14,19],"When":[2,4,5,6,7,9,14,18,19],"Where":2,"While":[6,14,18],"With":[2,4,6,14],"You":[3,5,10,18],"Your":[2,3,5,6,14],"_":9,"__doc__":17,"__getitem__":4,"__name__":19,"__traceback__":4,"_common":2,"_required_field":[3,5],"a2a":[1,8,12,13],"a2ui":[0,1,7,8],"a489":[3,5],"ab":4,"abc":[3,5],"abl":2,"abov":[2,11,16,17],"absent":2,"abstract":14,"accept":[1,2,3,5,6,7,11,14],"access":[0,2,3,5,6,7,8,12,14,15,16,17,18],"accomplish":[3,5],"account":[0,2,8,16,18],"acl":[13,16,17,18],"aclos":6,"acquir":[6,10,14,19],"acquisit":19,"across":[2,4,18],"act":[3,5],"action":[1,2,3,4,5,9,13,15,17,18,19],"activ":[2,10,12,17,18],"actor":[3,5],"actual":[2,6,14],"adapt":[2,7],"adbst":16,"add":[1,2,3,4,5,6,7,13,16,17,18,19],"add_argu":17,"add_negative_feedback":[6,14],"add_not":4,"add_positive_feedback":[6,14],"add_tag":0,"addit":[0,2,3,5,6,9,10,14,17],"additional_instruct":[1,6,14,15],"address":[2,7,13],"adjac":18,"adjust":16,"admin_password":16,"admin_us":16,"administr":[2,6,10,16,17],"advanc":[3,5],"adventur":[3,5],"advertis":[2,7],"after_serv":19,"after_server_stop":19,"afterward":[6,14],"age":[3,5],"agent":[1,8,10,11,13,16],"agent2ag":[0,13],"agent_attribut":[3,5],"agent_nam":[3,4,5],"agent_name_pattern":[3,5],"agentattribut":[0,1,3,5],"agentattributesemptyerror":[0,4],"agentnotfounderror":[0,3,4,5],"agenttaskattributesemptyerror":[0,4],"agenttasknotfounderror":[0,3,4,5],"agentteamattributesemptyerror":[0,4],"agentteamnotfounderror":[0,3,4,5],"agenttoolattributesemptyerror":[0,4],"agenttoolnotfounderror":[0,3,4,5],"aggreg":[3,5],"ai":[2,4,6,7,8,9,10,12,14,15,17,18,19],"ai_ag":2,"alcohol":[6,14],"algorithm":[3,5],"alia":[2,6],"allow":[3,5,6,16,17],"allowlist":19,"alon":[1,2,7,17],"along":[10,18],"alpha":4,"alphabet":4,"alreadi":[2,3,4,5,6,7,11,12,14,15,18,19],"also":[2,3,5,6,7,8,9,11,14,16,17,18,19],"alway":[2,6,14],"amazon":[14,16],"amazonaw":16,"amount":[6,14,18],"analyst":[3,5],"analyt":[3,5,6,14],"analyz":[3,5,6],"analyze_movie_task":[3,5],"ancient":[6,14],"ani":[0,3,4,5,7,9,12,16],"anim":[3,5],"annot":[6,14,15],"anomali":[3,5],"anonym":2,"anoth":[2,3,5,6,11,13,14,18],"answer":[2,3,4,5,6,7,9,13,14,18],"ant":11,"anthrop":[11,16],"anthropic_cr":11,"anthropicprovid":0,"api":[2,3,4,7,8,9,10,15,17,19],"api_key":17,"app":[13,19],"app_own":[6,14,18],"app_password":16,"app_profil":4,"app_us":[2,3,5,6,11,14,16,18],"appconfig":19,"append":[2,3,5,16],"append_host_ac":16,"appli":[1,2,6,7,9,14,15,19],"applic":[1,2,3,4,5,6,7,9,10,13,16,17,18,19],"approach":19,"appropri":17,"approv":17,"approxim":[9,19],"arbitrari":[2,19],"archiv":2,"area":[6,14],"arg":[4,6,9,14,17,19],"argpars":17,"argument":[3,4,5,6,10,14,17],"argumentpars":17,"array":[2,3,15],"arriv":[9,14],"articl":7,"artifact":2,"artifici":[3,5,6],"as_complet":[6,9],"ascii":4,"asgi":19,"ashburn":18,"ask":[2,3,7],"assert":2,"assign":[2,3,5,15,17],"assist":13,"associ":[2,3,5,6,18],"assum":[4,6,14],"astrobiolog":[6,14],"astronom":[6,14],"astronomi":[6,14],"async":[1,2,3,4,8,9,10,13,15,19],"async_as_complet":0,"async_connect":[2,5,6,10,11,16,18],"async_cr":15,"async_create_credenti":[5,11],"async_cursor":5,"async_delete_credenti":11,"async_disconnect":[6,9,10,18,19],"async_gath":0,"async_get_definit":[3,5],"async_grant_credential_access":11,"async_grant_http_access":16,"async_is_connect":10,"async_oci_ai_profil":[6,9,19],"async_oci_vector_ai_profil":18,"async_pipelin":0,"async_profil":[4,6,18],"async_queue_work":0,"async_revoke_credential_access":11,"async_revoke_http_access":16,"async_vector_index":18,"asyncag":0,"asyncconnect":[6,8],"asynccontextmanag":19,"asyncconvers":[0,2,5,6],"asyncgener":[4,5,6,18],"asynchron":[0,2,3,5,9,11,18],"asyncio":[5,6,9,11,13,16,18],"asyncprofil":[0,4,5,9,14,15,18,19],"asyncsess":[0,4,6],"asynctask":0,"asynctaskhistori":[0,5],"asyncteam":[0,2],"asyncteamhistori":[0,5],"asynctool":0,"asynctoolhistori":[0,5],"asyncvectorindex":0,"atexit":19,"attach":3,"attempt":17,"attribut":[1,3,4,5,9,16,17,19],"attribute_nam":[3,5,6,14,18],"attribute_valu":[3,5,6,14,18],"audienc":[2,3,5],"augment":[4,6,13,14,15,18],"authent":[1,2,6,8,11,14,15,16],"author":[2,4,7,17,18],"autom":[2,3,5,6,15],"automat":[2,3,12],"automl":[3,5],"autonom":[2,3,6],"autopilot":2,"autoreload":19,"avail":[2,3,4,5,6,7,8,9,10,12,14,16,17,18],"avoid":[4,10,19],"aw":[0,11,14],"await":[5,6,9,10,11,16,18,19],"awar":[4,6,7,13,14,15],"aws_access_key_id":[11,16],"aws_apiformat":16,"aws_bedrock_cr":11,"aws_bedrock_meta_prf":16,"aws_cr":16,"aws_host":16,"aws_secret_access_key":[11,16],"awsprovid":0,"azur":[0,11,14,15],"azure_api_key":16,"azure_deploy":16,"azure_deployment_nam":16,"azure_embedding_deploy":16,"azure_embedding_deployment_nam":16,"azure_host":16,"azure_openai_api_key":11,"azure_openai_cr":11,"azure_resourc":16,"azure_resource_nam":16,"azureai_cr":16,"azureai_prf":16,"azureprovid":0,"b":[2,7,18],"b2":[6,14],"back":[0,1,5,7,14],"backend":2,"background":[9,19],"balanc":2,"base":[0,3,4,5,6,7,10,13,16,18,19],"baseprofil":[0,14,18],"basi":7,"batch":[9,19],"be11":[3,5],"bear":2,"bearer":[1,2,7],"becaus":[2,3,5,6],"becom":[2,6,14],"bedrock":[0,11],"beer":[6,14],"befor":[0,2,4,6,8,9,10,11,14,16,17,18,19],"before_serv":19,"before_server_start":19,"begin":[3,5,15],"behav":15,"behavior":[0,2,3,7,15,16,18],"behind":12,"benefit":[3,5,10],"better":19,"bill":17,"bin":[12,17],"binari":2,"bind":[2,7],"biograph":[3,5],"biographi":[3,5],"birth":[3,5],"bit":15,"block":[2,3,5,9,13,14,18,19],"blocking_task":2,"blog":[10,19],"bodi":2,"bool":[3,4,5,6,14,15,16,18],"bound":[2,19],"boundari":[3,4,18],"brace":4,"branch":[6,14],"break":[4,14],"breakdown":6,"brew":[6,14],"bridg":[0,13],"bucket":[7,18],"budget":19,"build":[0,2,3,5,6,12,16,18],"built":[3,4,5,14],"bundl":[2,7],"burn":[6,14],"busi":[3,5,6,15,19],"bypass":[2,7],"byte":[6,14],"c":[2,3,4,5,6,19],"c2e3ff20":[3,5],"ca":[0,7],"calcul":[3,5,6,18],"calendar":18,"call":[0,1,3,4,5,6,7,9,10,11,13,14,15,16,17,18,19],"call_timeout":17,"callabl":3,"caller":[2,6,14,17],"can":[0,1,2,3,4,5,6,7,9,10,12,14,15,16,17,18,19],"cancel":[0,6],"canceltask":2,"cancri":[6,14],"canon":[2,3,5],"capabl":[0,2,3,5,8,13],"capac":[8,9,10,19],"capit":[3,4,5],"card":0,"care":[3,5,19],"cas":2,"case":[3,4,5,6,15,16],"case_sensitive_valu":[6,14,15],"casefold":4,"caseless":4,"catalog":2,"catch":4,"categori":2,"caus":4,"cc72740c75a5":[3,5],"celesti":[6,14],"center":4,"central1":2,"cert":[2,7],"certain":3,"certif":[0,7],"chain":3,"chang":[1,2,6,14,15,18,19],"channel":[3,5],"char":4,"charact":[4,6,7,14,18],"chat":[0,4,8,12,13,15,16,19],"chat_sess":[4,6,7,14],"chat_task":6,"chatgpt":[3,5,17],"check":[1,2,6,7,10,17,18,19],"checkout":[2,17],"chemistri":[6,14],"chicago":[3,5,6,14,15,16,18],"child":[2,7],"choic":2,"choos":[0,1,3,4,8,9,13,17,18,19],"chosen":17,"chunk":[4,6,7,14,18],"chunk_overlap":[0,18],"chunk_siz":[0,4,6,14,18],"chunkprocessingmethod":0,"churn":19,"civil":[6,14],"claim":[1,2,7],"clarif":3,"class":[3,4,5,6,14,15,18,19],"classif":[3,5],"classmethod":[3,4,5,6,14,15,16,18],"claud":16,"clean":3,"cleanup":[0,3,5,6,11,14,17,19],"clear":[3,5,7],"cli":[0,1,8,12],"client":[2,7,8,9,13,19],"clob":[3,6,7,9,14],"close":[4,6,9,10,13,14,17,18,19],"close_select_ai_pool":19,"cloud":[0,3,4,5,6,7,14],"cloudbuild":2,"clue":[6,14],"cluster":[0,1,3,5,8,13,16],"co":16,"code":[4,7,9,10,15,16,19],"codec":4,"codex":17,"coher":[11,16,18],"cohere_api_key":11,"cohere_cr":11,"cohereprovid":0,"collect":[2,3,5,7,18],"column":[2,3,6,14,15,17],"com":[2,3,5,7,12,16,17,18],"combin":[6,13,14],"come":18,"comedi":[3,5],"comfort":7,"command":[1,2,8,9,12,13,16,17,18,19],"comment":[3,6,11,14,15],"commit":[2,16,17],"common":[0,3,5,16,18],"compar":18,"comparison":4,"compart":16,"compat":[0,1,7,9,13,16,17],"compet":14,"complet":[0,1,2,3,5,6,7,9,13,14,16,18],"complianc":[3,5],"compliant":[3,5],"compon":[2,3,5,6],"component_typ":[3,5],"compos":[3,13],"composit":[3,5,6,14],"comprehens":14,"comput":[6,14],"concaten":4,"concept":[0,16],"conceptu":18,"concis":[3,5,6,15],"concret":[16,19],"concurr":[8,10,13,18,19],"config":[2,11],"config_dir":10,"configur":[0,1,3,4,5,6,7,8,10,11,14,15,16,18,19],"conflict":[3,5],"connect":[0,1,3,4,5,8,9,11,13,15,16,17,18,19],"connection_url":2,"consid":[4,18],"consider":0,"consist":2,"constant":[6,14],"constrain":15,"constraint":[6,14,15],"construct":[3,16,18,19],"constructor":[3,5],"consul":[0,7,8],"consul_http_url":[2,7],"consult":[2,8],"consum":[1,4,6,9,14],"consumpt":14,"contain":[2,3,4,5,6,14,16,17,18],"content":[0,2,3,5,6,7,13,14,18],"context":[1,2,3,4,6,7,13,14,15,18,19],"context_id":2,"contextlib":[6,14,19],"continu":[2,3,5,6],"continue_on_error":[6,9],"contract":7,"control":[0,2,6,7,9,14,17,18,19],"conveni":2,"convers":[2,3,4,5,6,7,13,14,15,16],"conversation_cr":[3,5],"conversation_id":[3,4,5,6,14],"conversation_length":0,"conversation_param":4,"conversation_retention_day":7,"conversationattribut":[0,3,5],"conversationnotfounderror":[0,4],"conversationprompt":0,"convert":[4,6,16,18],"cool":[6,14],"coordin":[3,5],"copi":[2,4,7],"core":0,"corpor":[6,14],"correct":[4,6,7,14,18],"correspond":[6,7,8,11,14,18],"cosin":[4,18],"count":[2,4,6,7,9,14,17],"countri":16,"cover":[1,15],"creat":[0,2,4,6,9,10,12,13,15,16,17,19],"create_app":19,"create_built_in_tool":[0,3,5],"create_credenti":[3,11,16,17],"create_email_notification_tool":[0,3,5],"create_pipelin":[6,8],"create_pl_sql_tool":[0,3,5],"create_pool":[9,10,19],"create_pool_async":[5,6,9,10,19],"create_rag_tool":[0,3,5],"create_slack_notification_tool":[0,3,5],"create_sql_tool":[0,3,5],"create_task":[3,5,6,9],"create_tool":3,"create_websearch_tool":[0,3,5],"created_object":[3,5],"creation":[0,1,3,5,10,16,18],"credenti":[1,2,3,4,5,6,7,8,10,12,13,14,15,16,17,18,19],"credential_cr":17,"credential_nam":[3,5,6,11,14,15,16,17,18],"crt":2,"ctrl":[2,19],"curat":7,"curl":[2,19],"current":[2,3,4,5,6,7,10,14,18,19],"current_schema":17,"cursor":[3,5,6,14,17],"cust_email":6,"cust_first_nam":6,"cust_id":6,"cust_last_nam":6,"custom":[2,3,5,6,7,9,15,16,18],"customer_count":6,"d4328a7b2210":[3,5],"d917b055":[3,5],"dank":[6,14],"data":[2,3,4,5,6,12,13,14,16,18],"databas":[0,1,3,4,5,6,7,8,9,11,12,13,14,15,16,18,19],"databaseerror":[4,6,14,18],"databasenotconnectederror":[0,4],"datafram":[1,4,6,13,14,17],"date":[3,5],"datetim":[4,18],"day":[2,7,18],"db":2,"db_connect_str":[9,10,12,19],"db_password":[2,7,10],"db_servic":[2,7],"db_user":10,"db_usernam":[2,7],"dba":6,"dbhost":[2,7],"dbms_ai_a2a_context":2,"dbms_ai_a2a_task":2,"dbms_cloud":[8,11],"dbms_cloud_ai":[0,4,8,9,13,16,17],"dbms_cloud_ai_ag":[3,5,8],"dbms_cloud_pipelin":8,"dbms_network_acl_admin":16,"de":[6,14],"dead":2,"deadlin":[2,7],"decid":[3,4,18],"decim":4,"declar":2,"decod":[3,5],"dedic":[3,16],"def":[2,4,5,6,9,11,16,18,19],"default":[1,2,3,4,5,6,7,11,14,15,16,17,18,19],"default_config":11,"defaultinputmod":2,"defaultoutputmod":2,"defin":[2,3,6,13,14,15,18],"definit":[0,18],"degre":15,"deleg":[6,14],"delet":[0,1,2,3,4,5,7],"delete_ag":[0,3,5],"delete_credenti":[11,17],"delete_feedback":[6,14],"delete_index":[0,18],"delete_profil":[6,14,17],"delete_prompt":0,"delete_task":[0,3,5],"delete_team":[0,3,5],"delete_tool":[0,3,5],"deliber":[17,19],"delimit":4,"deliveri":6,"demonstr":[3,5],"denot":15,"depend":[0,2,3,4,5,6,7,8,11,14,15,16,18],"deploy":[0,1,3,5,6,8,9,10,13,15,16,19],"deregist":2,"deriv":[1,2,7],"describ":[1,2,3,5,6,13,14,15,16,17],"describe_team":[0,3,5],"describe_tool":[0,3,5],"descript":[0,2,3,5,6,7,14,17,18,19],"descriptor":2,"design":[3,6],"desir":2,"despit":[6,14],"detail":[2,3,5,6,7,11,14,16,19],"detect":[3,5,6,14,15],"determin":17,"dev":[2,7],"develop":[2,3,5,6,7,14,17,18,19],"df":[6,14],"dict":[0,3,4,5,6,14,15,16,18],"dictionari":[3,4,11],"differ":[2,3,5,7,17],"digit":4,"direct":[2,3,5,6,7,14,16,19],"director":[3,5],"directori":[2,7],"disabl":[0,1,2,3,5,18],"discard":[2,4],"disconnect":[6,9,10,14,16,17,18,19],"discov":[2,6,7,14],"discover":13,"discoveri":[0,6,14],"display":2,"distanc":18,"django":[0,13],"dns":2,"doc":[12,18],"docker":2,"dockerfil":2,"document":[2,6,13,17,18],"documentari":[3,5],"doe":[1,2,3,5,6,7,8,11,14,15,16,17,18],"domain":[2,3],"done":[4,6],"dot":[4,18],"doubl":6,"drama":[3,5],"drive":6,"driver":[4,6,8,9,10,17],"drop":[3,5,11,17,18],"dsn":[2,3,5,6,7,9,10,11,12,14,16,17,18,19],"dtype":14,"dual":17,"due":[6,14],"dump":[2,3,5],"durabl":2,"durat":[2,4],"dure":[3,5,6,7,11,13,14,18,19],"dynam":[0,1,6,8,13,14],"e":[3,5,6,10,14,18],"e8a1":[3,5],"earli":[6,14],"earlier":[17,18],"earliest":15,"easi":7,"easier":[6,14],"east":16,"ecosystem":[0,13],"edit":[11,18],"effect":19,"egress":[2,17],"either":[2,3,5,14,15,18,19],"element":[6,14,18],"elig":15,"elimin":[3,5,6],"els":[6,14],"email":[3,4,5,6],"emb":[16,18],"embed":[8,16,18],"embedding_model":[6,14,16,18],"empti":[4,6,14],"en":[6,14],"enabl":[0,1,2,3,5,7,8,15,18],"enable_custom_source_uri":15,"enable_human_tool":[0,3,5],"enable_sourc":[0,6,14,15,18],"enable_source_offset":[6,14,15],"enclos":6,"encod":[2,4],"end":[0,3,4,5,6,7,14,18],"end_dat":4,"endpoint":[0,2,3,5,6,7,11,12,13,14,15,16,18],"endswith":4,"enforc":17,"enforce_object_list":[6,14,15],"engin":2,"english":7,"enhanc":[3,5],"enough":[3,6,14],"ensur":[2,3,5,6],"enterpris":[2,7],"entri":[3,5,6,16],"enumer":[0,6,9],"environ":[0,1,2,3,5,6,7,8,10,12,14,16,17,18,19],"ephemer":2,"epic":[3,5],"equal":4,"equival":[1,3,7,11,16],"error":[0,2,3,5,6,11,14,17,18,19],"error_messag":4,"especi":19,"establish":[2,4,17],"euclidean":[4,18],"eval":7,"evalu":[7,18],"even":[2,3,5,6,11,14,17],"event":[2,3,4,5,6,14],"everi":[1,2,6,9,10,11,15,19],"evolut":[6,14],"evolv":[6,14],"exact":[2,3,5,6,7,14,17,18],"exampl":[0,1,2,4,6,8,10,11,12,13,14,18],"exc":4,"except":[4,6,17],"exclude_nul":[3,4,6,14,15,16,18],"execut":[1,2,3,5,6,7,8,9,10,14,15,17,18,19],"executemani":17,"executor":9,"exhaust":19,"exist":[2,3,4,5,6,7,8,11,13,14,17,18],"exit":[7,14],"exoplanet":[6,14],"expand":[0,2,4],"expandtab":4,"expans":[6,14],"expect":[2,3,4,5,6,14,16,19],"expir":2,"expiri":2,"explain":[0,2,4,7,9,13],"explain_sql":[4,6,14],"explainsql":[4,6,9],"explan":[6,14],"explicit":[1,2,6,7,17],"explor":7,"export":[0,2,9,10,12,17,18,19],"export_import_movie_analyst":[3,5],"export_import_movie_task":[3,5],"export_import_movie_team":[3,5],"export_team":[0,3,5],"exported_spec":[3,5],"expos":[1,2,4,7,13,18],"express":[3,4,5,6,7,14,18],"extend":17,"extern":[2,3,5,11,19],"extra":[7,12],"extractivenesslevel":0,"extraterrestri":[6,14],"f":[2,3,4,5,6,9,17],"f56d":[3,5],"f7e5e445d286":[3,5],"fail":[2,3,4,5,6,17,19],"failur":[4,6,17],"fallback":[2,7],"fals":[2,3,4,5,6,14,16,17,18],"famili":[3,5],"fantasi":[3,5],"fast":[7,16,19],"fastapi":[0,10,13],"featur":[1,3,5,13,17],"februari":[3,5],"feedback":[0,4],"feedback_cont":[6,14],"fetch":[0,1,2,3,4,5,6,14,16],"fetchon":17,"fewer":19,"fi":[3,5],"field":[2,3,4,6,11,14],"file":[6,7,11,12,14,16,17,18,19],"filenam":18,"fill":4,"fillchar":4,"film":[3,5],"filter":[0,2,5,7],"final":[2,3,5,6,9,14,16,17,18],"find":[0,2,4,7],"fine":19,"fingerprint":11,"finish":[2,6,9,17,18],"first":[0,1,2,3,4,5,6,7,12,13,14,16],"fix":[0,1,8,13],"flag":[1,2,17],"flash":16,"flask":[0,13],"flexibl":[3,5],"float":[6,14,15,18],"flow":[3,5],"flush":17,"focus":[3,5,6,14],"follow":[2,3,4,5,6,7,9,11,12,13,14,16,17,18],"forc":[3,5,6,11,14,17,18],"foreign":15,"form":[0,1,6,14],"format":[3,4,5,6,14,15,16,17,18],"format_map":4,"forti":19,"forward":[2,6,9,14],"found":[3,4,5,6,14],"four":[2,7,19],"fp":11,"fp8":[3,5],"framework":[3,5,13],"franc":[3,5],"fresh":[2,7,17],"from_fil":11,"front":[2,4,9],"fulfil":3,"full":[6,14],"fulli":7,"function":[2,3,5,6,9,14,18],"function_nam":[3,5],"futur":9,"g":[6,14],"galaxi":[6,14],"gamma":[6,14],"gap":0,"gas":[6,14],"gateway":[0,8,19],"gather":[6,9],"gcloud":2,"gcp":[0,14],"gcp_host":16,"gemini":[0,2,7],"gen":[6,11,14,16],"general":[3,5,6,14,18,19],"generat":[0,1,2,3,4,5,8,13,16,17,18,19],"generate_synthetic_data":[6,14],"generativelanguag":16,"generic":[3,5,6,14,15,16,18],"genr":[3,5,7],"german":7,"get":[2,6,9,14,18,19],"get_attribut":[0,6,14,16,18],"get_definit":3,"get_next_refresh_timestamp":[0,18],"get_profil":[0,18],"getenv":[3,5,6,9,10,11,12,14,16,18,19],"getmod":19,"gettask":2,"give":[0,2,7,13],"given":[3,4,5,7],"gke":2,"go":2,"good":19,"googl":[0,7,11,14],"google_api_key":[11,16],"google_cr":[11,16],"google_gemini_3_6_flash":16,"googleapi":16,"googleprovid":0,"got":2,"govern":[3,5],"gpt":[0,14,16,17],"grant":[0,2,3,5,6,8,11,13,14,15,16,17,18],"grant_access":[0,3,5,6,14,18],"grant_credential_access":11,"grant_http_access":16,"grant_network_access":[16,17],"grant_privileg":[2,17],"gravit":[6,14],"grok":16,"ground":[13,18],"group":[0,3,7],"grow":10,"guarante":[6,14],"guid":[2,3,6,7,13,14,18],"guidanc":[5,15],"gunicorn":19,"halo":[6,14],"ham":[4,18],"handl":[0,2,9,10,17],"handler":[2,19],"handshak":2,"happen":19,"hard":19,"hash":2,"hashicorp":2,"header":2,"headless":2,"health":[0,2],"healthi":[2,7],"heartbeat":[2,7],"heavi":18,"held":[16,18],"help":[3,5,7,13,15,17,18],"helper":[0,2,6,9,14,16,17,18],"hex":[3,5,17],"hf":11,"hf_xxx":11,"high":[3,5,6,14],"higher":[6,13],"highest":4,"highlight":[3,5,16],"histor":[3,5],"histori":[0,1,2,6,8,13,14,15],"hold":[2,4],"hook":[4,19],"horror":[3,5],"host":[2,3,5,7,16,17,19],"hostnam":16,"hot":[6,14],"http":[0,3,4,7,12,16,17,19],"https":[0,3,5,6,7,14,16,18],"https_proxi":10,"https_proxy_port":10,"huggingfac":[11,16],"huggingface_cr":11,"huggingfaceprovid":0,"human":[2,3,4,5],"hyperparamet":[3,5],"iam":2,"ibm":14,"ice":[6,14],"id":[2,3,5,6,7,17],"ident":[1,2],"identifi":[2,3,4,5,6,7,8,14,16,17,18],"ignor":[4,6,14,18],"imag":2,"immedi":2,"immut":2,"implement":[2,4,16],"import":[0,2,4,6,7,9,10,11,12,14,16,17,18,19],"import_team":[0,3,5],"imported_analyze_movie_task":[3,5],"imported_movie_agent_team":[3,5],"imported_movie_analyst":[3,5],"improv":[3,5],"includ":[1,2,3,4,5,6,7,11,14,15,16,17,18],"include_data":18,"include_task":2,"incorpor":15,"incorrect":[6,14],"increas":[17,19],"increment":[6,9,10,19],"indent":[2,3,5],"independ":[2,3,8,9],"index":[1,3,4,5,7,8,9,10,11,12,13,14,15,16],"index_nam":[4,18],"index_name_pattern":18,"indic":[15,16,17,18],"individu":[6,8,9],"infer":16,"info":2,"inform":[2,3,5],"infrar":[6,14],"infrastructur":[2,3,5,6,14],"inherit":[3,5],"initi":[2,6,7,18,19],"inlin":[0,3,5,6,7,14],"input":[0,2,3,4,5,6,9,18,19],"insert":[3,4,5,17],"insid":[2,3,4,5,6,7,18,19],"insight":[6,14],"inspect":[0,1,4,6,7,14,18],"inspir":[6,14],"instal":[0,1,6,7,8,11],"instanc":[2,4,6,16],"instanti":[6,14,16],"instead":[2,3,4,5,7,15,16,19],"instruct":[0,2,3,5,6,14,16],"int":[3,4,5,6,9,14,15,18,19],"integ":[1,4,6,14,15],"integr":[1,3,5,6,7,8,12,15,17],"intellig":[3,5,6],"intend":[2,3,5,7],"intent":[2,4,9,18],"interact":[0,6,14],"interfac":[2,5,6,12,13,14,19],"internal":[2,7],"interpret":[4,16,19],"interval":18,"introduc":8,"introduct":0,"invalid":[4,6],"invalidsqlerror":[0,4],"invoc":[2,3,5],"invocation_id":[3,4,5],"invok":[2,3,5,7,13,16],"involv":19,"ip":[2,7],"is_connect":[10,19],"isalnum":4,"isalpha":4,"isascii":4,"isdecim":4,"isdigit":4,"isidentifi":4,"isinst":[3,5,6],"iskeyword":4,"islow":4,"isnumer":4,"isol":[0,12],"isprint":4,"iss":2,"isspac":4,"issuer":2,"istitl":4,"isupp":4,"item":[3,5,9,18],"item_count":17,"iter":[3,4,5,6,14,18],"januari":[3,5],"japan":16,"job":[18,19],"join":[4,9],"jq":2,"json":[0,1,2,3,4,5,6,7,14,15,16,18,19],"jsonifi":19,"jsonrespons":19,"jsonrpc":2,"justifi":4,"jwt":2,"k":16,"keep":[2,3,5,6,7,9,10,11,13,14,16,17,18,19],"keepend":4,"key":[0,2,3,4,5,7,8,11,15,16,17,18],"key_alia":16,"key_cont":11,"key_fil":11,"keyboard":17,"keyword":[4,5,6,10,14,18],"kind":2,"know":[6,14],"known":[2,6,9,14,16,18],"kubernet":2,"kv":2,"kwarg":[3,4,6,14,15,16,18],"l2_squar":[4,18],"label":17,"lack":6,"languag":[1,3,4,5,6,7,13,14,15,16,17,18],"language_default":[6,14],"larg":[6,14,16],"larger":[6,18],"last":[2,6],"latenc":19,"later":[2,6,7,8,11,14,17,18],"latest":[3,5],"latter":17,"launcher":2,"layer":[3,5],"lead":4,"learn":[3,5,6],"least":[2,4,7,8],"leav":[2,6,9,14,16,18,19],"led":[6,14],"left":4,"len":[4,9],"length":[4,7],"less":15,"let":[2,3,4,5,6,14,18,19],"level":[0,1,2,9,13,18],"leverag":[3,5,6],"librari":[2,10,11,16],"life":[6,14],"lifecycl":[0,13,18,19],"lifespan":19,"lifetim":2,"lightweight":2,"like":[3,5,10],"limit":[2,3,4,5,7,17,19],"line":[2,4,9,12,13,18,19],"link":[3,5,18],"list":[0,1,2,4,7,9,17],"list_prompt":0,"list_tool":[0,3,5],"listen":[2,7],"listtask":2,"live":[2,19],"ljust":4,"llama":[3,5,16],"llama3":16,"llama_4_maverick":[3,5],"llm":[3,4,5,6,14,15,16],"llm_chat_tool":[3,5],"llm_config":[3,5],"llms":14,"load":[2,3,5,8,10,11,16,18,19],"local":[2,6,7,10,12,14,17,19],"localhost":19,"locat":[0,2,3,5,7,18],"location_uri":[6,14],"log":[2,18],"logic":5,"long":[6,9,10,13,14,19],"longer":[1,2,6,11,14,18,19],"lookup":4,"lookuperror":4,"loop":[2,7],"loopback":2,"lose":14,"lost":2,"low":19,"lower":[4,15],"lowercas":4,"lowest":4,"lstrip":4,"m":[1,2,6,10,11,12,17,19],"machin":[2,3,5,6],"made":[9,15],"main":[3,4,5,6,9,11,16,18],"maintain":[2,3,5],"major":14,"make":[2,4,6,7,11,14,15,17,18],"makefil":12,"maketran":4,"manag":[2,6,15,17,18,19],"manhattan":[4,18],"mani":[3,5,6,7,9,13,14,16,17,18],"manifest":[2,7],"manipul":6,"manner":6,"manual":[2,6,7,18],"map":[1,2,3,4,5,6,7,9,14,15],"mapped_sql":[6,14],"marker":[6,14],"match":[2,3,5,6,9,14,18,19],"match_limit":[0,18],"materi":[2,13],"mathemat":[6,14],"matrix":8,"matter":2,"maverick":[3,5],"max":[2,7],"max_siz":[6,9,10,19],"max_token":[6,14,15,16],"max_work":9,"maximum":[2,4,7,9,18],"maxsplit":4,"may":[3,4,5,9,17,19],"md":2,"mean":[2,4,6,14,15,18],"meant":3,"meanwhil":[6,14],"measur":19,"mechan":2,"memori":[2,6,14],"merg":[6,14],"messag":2,"messageid":2,"meta":[3,5,16],"metadata":[2,3,5,14,15,17,18,19],"method":[0,2,3,4,5,6,10,14,15,16,18,19],"metric":18,"microsoft":14,"migrat":2,"milki":[6,14],"millisecond":[17,19],"mime":2,"min_siz":[6,9,10,19],"minimum":[1,8,18],"minut":[2,18],"mirror":[5,18],"miss":[2,4,7,17,18],"mission":[3,5],"mix":19,"ml":[3,5,6],"mode":[0,2,6,7,8],"model":[0,2,6,8,13,15,16,17,19],"modul":[0,2,4,9,13],"monitor":17,"month":2,"months_between":[3,5],"mount":2,"move":[2,3,5],"movement":[3,5],"movi":[3,5,7],"movie_agent_team":[3,5],"movie_analyst":[3,5],"movie_sql_tool":[3,5],"movie_supervisor":[3,5],"mtls":[2,7,19],"much":[6,14,15],"multi":[0,3,9,10,13,19],"multipl":[2,3,5,6,9,10,14,18],"music":[3,5],"must":[1,2,3,4,5,6,7,9,11,14,15,17,18],"mutat":19,"my_oci_ai_profile_key":[3,5,6,11,14,15,18],"my_provider_credenti":11,"myapp":19,"myappconfig":19,"mysteri":[3,5,6,14],"n":[4,7,18],"nad":6,"name":[0,2,3,4,5,7,9,11,15,16,17,18,19],"namespac":[2,7,18],"narrat":[0,4,6,7,13,18],"nativ":16,"natur":[3,4,5,6,7,13,14,15,16,17,18],"need":[2,3,5,6,7,8,9,11,12,13,14,15,16,18],"negat":[6,14,15],"neither":[2,7],"network":[0,2,3,5,6,7,8,11,12,13,14,16,17,18],"neutral":7,"never":4,"new":[0,2,3,4,5,6,7,10,13,14,18],"newest":[3,5],"next":[2,7,9,18],"night":[3,5,6,14],"node":2,"non":[2,4,6,7,14,15,16,18],"none":[2,3,4,5,6,9,14,15,16,18],"nonempti":2,"noqa":2,"normal":19,"notat":4,"note":[0,4,6,7],"notebook":[10,13,17,18],"notif":[2,3,4,5],"notification_typ":[3,5],"notificationtyp":[0,3,4],"now":[1,2],"nprompt":9,"null":[6,14],"number":[2,3,4,5,6,7,14,15,17,18],"number_of_promot":6,"numer":[1,4,6,14],"o":[7,18],"oauth":[0,1,7],"obj":[3,5],"object":[0,1,2,6,7,8,10,11,13,16,17,19],"object_list":[3,5,6,14,15,16,17],"object_list_mod":[6,14,15],"object_nam":[3,5],"object_storage_credential_nam":[0,3,5,18],"object_store_cr":[7,18],"object_store_credenti":18,"object_typ":[3,5],"objectstorag":[7,18],"obscur":2,"observ":[6,14],"obsolet":[6,14],"obtain":4,"occur":[7,18],"occurr":4,"oci":[3,5,6,7,8,11,14,15,16,17,18],"oci_":[6,14],"oci_ai_profil":[3,5,6,7,9,14,19],"oci_apiformat":[3,5,6,14,15,16,18],"oci_compartment_id":[6,14,16],"oci_endpoint_id":[6,14,16],"oci_genai_cr":11,"oci_runtimetyp":[6,14,16],"oci_vector_ai_profil":[6,14,18],"ocid":16,"ocigenaiprovid":[0,3,5,6,14,15,18],"offer":[3,5,6,14,16],"old":4,"older":[6,8,9,14],"oldest":[3,5],"omf":6,"omit":[1,2,3,5,6,7,14,15,16,17,18],"oml":6,"oml4pi":6,"onc":[6,12,13,14,17,19],"one":[2,3,4,5,6,7,8,9,10,14,15,16,18,19],"onli":[1,2,3,4,5,6,7,11,14,15,16,17,18,19],"opaqu":2,"open":[2,3,4,5,6,7,10,11,14,19],"open_ai_api_key":[3,5],"open_ai_credential_nam":[3,5],"open_ai_profile_nam":[3,5],"openai":[0,11,14,17],"openai_api_key":17,"openai_cr":[3,5,11],"openai_profil":[3,5],"openaiprovid":[0,3,5,15,17],"oper":[1,3,4,5,6,7,8,9,10,14,15,18,19],"operation":[3,5],"optic":[6,14],"option":[0,1,3,4,5,6,8,9,12,13,14,15,16,18,19],"ora":6,"oracl":[0,1,3,4,5,6,7,8,11,12,13,14,16,17,18,19],"oracle_ai_database_ag":[2,3,5,7],"oraclecloud":18,"oracledb":[4,6,8,10,12,14,18,19],"oraclevectorindexattribut":0,"orchestr":3,"order":[3,5,6,9,14],"ordereddict":[3,5],"ordin":4,"ordinari":[2,19],"org":[6,14],"organiz":[3,5],"origin":[4,6,14,18],"os":[3,5,6,9,10,11,12,14,16,17,18,19],"ot":5,"otherwis":[3,4,5,6,7,14],"outbound":[8,17],"outlin":3,"output":[2,3,4,5,6,7,11,14,15,16,17,18],"outsid":[2,3,5,11],"overal":[6,14],"overlap":[4,18],"overrid":[0,1,7,9,15,16],"overridden":2,"overwhelm":19,"owned":[13,18],"owner":[1,2,3,5,6,7,14,15,16,17,18],"ownership":[6,7,13,14,18],"owns":[2,14],"p":[6,14,16],"p_birth_dat":[3,5],"packag":[0,2,3,7,8,11,12,13,17,19],"pad":4,"page":[3,5,7],"pagin":2,"pair":[3,6,9],"panda":[1,4,6,8,12,13,14],"parallel":9,"param":[2,3,4,5,6,14],"paramet":[0,2,3,4,5,6,10,14,15,16,17,18,19],"parse_arg":17,"parser":17,"part":[2,3,4,5,9],"parti":13,"partial":[6,14],"particular":[1,2,16],"partit":4,"pass":[1,2,3,5,6,7,9,10,11,14,15,17,18,19],"password":[2,3,5,6,7,9,10,11,12,14,16,17,18,19],"path":[2,6,7,10,11,14,16],"pattern":[0,2,3,5,6,7,9,13,14,18],"paus":[3,5,18],"payload":[2,3,5,16],"peer":2,"pem":[2,7],"per":[1,2,4,6,7,10,14,15,19],"perform":[2,3,5,6,14,17,18],"permiss":[8,18],"permit":16,"persist":[0,3,4,5,7,15],"pformat":[3,5,6,14,16],"phenomena":[6,14],"physic":[6,14],"pile":19,"pin":[1,2],"pint":[6,14],"pip":[1,2,6,7,11,12,17,19],"pipelin":[0,1,8,9,13,18],"pipeline_nam":[0,18],"pki":2,"pl":[0,3,4,5,8,10],"place":[2,6,14,17,19],"placehold":[3,5,7],"plain":[2,13],"plan":[3,12],"plane":2,"platform":[2,14,19],"plus":[2,16],"pod":[2,7],"point":[2,4,5,7,11],"polici":[2,18],"poll":[0,1,7,8],"pool":[0,2,4,6,7,8,13,14,18],"pool_getmode_timedwait":19,"pool_getmode_wait":19,"pool_incr":[9,19],"pool_max":[9,19],"pool_min":[9,19],"pop":19,"popul":18,"port":[2,7,16,19],"portabl":[2,3,5,7],"posit":[4,6,14],"possibl":[4,9,19],"post":2,"power":6,"powershel":12,"pprint":[3,5,6,14,16],"pq":4,"prebuilt":[3,5],"preced":[2,6,7,14],"precompil":2,"preconfigur":7,"predict":[3,5,15],"prefer":[2,18,19],"prefil":2,"prefix":4,"prepar":[0,1],"prerequisit":[0,1],"present":[2,4],"preserv":[6,18],"press":[2,19],"pressur":[6,14],"prevent":[2,6,7,14,19],"previous":[3,5,18],"primari":[15,17],"primarili":[3,5],"print":[2,3,4,5,6,7,9,11,12,14,16,17,18],"print_task_summari":2,"printabl":4,"prior":3,"privaci":[3,5],"privat":[0,7,11],"private_key":11,"private_key_cont":11,"privileg":[1,2,3,6,8,11,12,13,14,15,16,17,18],"procedur":[3,5,18],"process":[2,3,5,7,8,10,13,16,17,18,19],"processor":9,"produc":[6,9,14],"product":[2,3,5,6,9,15,16,18,19],"product_doc":18,"profil":[1,2,3,4,5,8,9,10,11,12,13,17,18,19],"profile_attribut":[3,5,6,14],"profile_cr":17,"profile_create_aw":16,"profile_create_azur":16,"profile_create_gcp":16,"profile_dict":16,"profile_nam":[0,3,4,5,6,9,14,16,17,18,19],"profile_name_pattern":[6,14],"profileattribut":[0,1,3,5,6,14,16,17,18],"profileattributesemptyerror":[0,4],"profileemptyattributeserror":14,"profileexistserror":[0,4,14],"profilenotfounderror":[0,4,6,14,18],"program":[6,13],"progress":[6,14,17],"project":[2,12,17],"project_id":2,"promo_id":[6,14],"promot":[3,5,6,9,14],"promotion_count":6,"prompt":[2,3,4,5,7,10,13,15,16,17,18,19],"prompt_spec":[6,14],"prompt_specif":[6,9],"properti":[2,3,6,14,18],"protect":2,"protocol":[0,7],"protocolvers":2,"prove":17,"provid":[2,3,4,5,6,7,8,9,10,12,13,14,15,17,18,19],"provider_credenti":18,"provider_endpoint":[6,14,16],"provider_nam":[6,14,16],"provis":2,"proxi":[2,3,5,6,12,14,18],"ps1":12,"public":[0,3,4,5,7,11,16,17,18,19],"public_synonym":11,"public_url":[2,7],"publish":2,"punctuat":4,"purchas":18,"purg":17,"purpos":[2,3,7,13,15],"push":2,"put":[2,6,9],"py":[0,2,3,5,12,16,17,18,19],"pypi":12,"pysai_request_attributes_sampl":[6,14],"python":[0,1,2,3,4,5,6,7,8,10,11,12,14,15,16,18],"python3":[12,17],"qualifi":[6,14,18],"qualified_nam":[0,1,6,14,18],"quart":19,"queri":[0,1,3,5,6,12,13,15,18],"question":[2,3,5,6,14],"queu":9,"queue":[9,13],"quick":[7,10],"quickstart":17,"quit":[2,7],"quot":6,"quota":[2,8,17],"r":[3,4,5,12,16],"radio":[6,14],"rag":[0,3,4,5,7,8,13,19],"rais":[2,3,4,5,6,11,14,17,18],"raise_error_if_exist":14,"raise_error_on_empty_attribut":14,"random":15,"rang":[3,4,5,6,9,14],"rate":[17,18],"rather":[1,2,6,8,14,17,18],"raw":4,"ray":[6,14],"re":[6,14],"reach":[2,6,14],"reachabl":[2,7],"read":[2,3,4,5,6,7,9,10,11,14,17,18,19],"readabl":11,"readi":[2,9,19],"readm":2,"reaper":2,"reason":[3,5,16],"receiv":[2,7,9,14,16],"recip":0,"recipi":[3,5,6,8,14,18],"recommend":12,"reconnect":2,"record":[2,3,5,6,7,14,18],"recov":4,"recreat":[2,3,5,6,11,14,18],"reduc":[6,9,14,19],"reduct":6,"refer":[5,6,11,16],"referenc":[3,5,6,14],"referenti":15,"reflect":18,"refresh":[2,18,19],"refresh_r":[0,18],"refund":18,"regard":14,"regexp_lik":[3,5,6,14,18],"region":[2,3,5,6,8,11,13,14,15,16,18],"regist":[2,3,4,5,7,11,19],"register_error":4,"registr":2,"registri":2,"regress":[3,5],"regular":[2,3,4,5,6,7,14,18],"reject":2,"relat":[1,3,5,18],"releas":[3,5,10,19],"relev":[2,8,15,18],"reli":1,"remain":[2,3,4,5,6,7,14,16,17],"remov":[0,2,3,4,5,6,7,11,14,16,17,18],"remove_tag":0,"removeprefix":4,"removesuffix":4,"render":2,"repair":6,"repeat":2,"replac":[1,2,3,4,5,6,7,8,11,14,16,17,18],"repli":16,"replica":2,"report":[2,7,15,18],"repositori":[2,3,5,11,12,16,17,18],"repr":4,"repres":[3,5,14,17,18],"represent":2,"reproduc":15,"request":[0,1,2,3,4,5,7,9,10,15,16,17,18],"request_attribut":[6,14],"requests_ca_bundl":2,"requir":[0,1,2,3,5,6,7,9,11,13,14,17,18,19],"rerun":17,"research":[3,5,6,14],"reserv":4,"resid":[3,5,18],"resolv":[2,7,17,18],"resourc":16,"respect":3,"respond":[3,5],"respons":[2,3,4,5,6,7,14,15,16,17,18,19],"rest":[4,7,19],"restart":2,"restrict":[2,15],"result":[0,2,3,5,7,8,9,13,14,16,17,18],"resum":[3,5,18],"retain":[2,6,7,16],"retent":[2,7],"retention_day":0,"retri":[4,17],"retriev":[2,3,4,5,6,13,14,15,18],"return":[2,3,4,5,6,7,9,13,14,15,16,17,18,19],"return_except":6,"returnimmedi":2,"reus":[0,2,3,5,19],"reusabl":13,"revers":[3,5],"review":[6,13,14,17],"revok":[0,3,5,6,8,11,14,17,18],"revoke_access":[0,3,5,6,14,18],"revoke_credential_access":11,"revoke_http_access":16,"rfind":4,"rich":0,"right":[3,4],"rindex":4,"rjust":4,"robin":2,"robust":[3,5],"role":[0,1,2,3,5,6,11,13,14,18],"romanc":[3,5],"root":[2,3,5,16,18],"rotat":2,"round":[2,6,9,17],"rout":[0,7,19],"row":[4,6,17],"rpartit":4,"rpc":[1,2],"rs":4,"rsplit":4,"rstrip":4,"rule":15,"run":[0,1,2,4,7,9,10,11,12,13,16,18,19],"run_pipelin":[6,8,9],"run_sql":[4,6,14,17],"run_tool":[0,3,5],"runnabl":[0,2],"runsql":[4,6,9,14],"runtim":[2,7,8,16,17],"runtimeerror":[2,3,5],"s":[0,2,4,6,7,8,11,12,13,14,15,16,17,18,19],"s310":2,"sagittarius":[6,14],"sale":[2,9,15,16],"sampl":[0,2,3,5,6,8,9,10,11,12,14,17,18],"sample_age_tool_":[3,5],"sample_calculate_age_":[3,5],"sample_definition_task_":[3,5],"sample_supervisor_":[3,5],"sample_task_":[3,5],"sample_team_":[3,5],"sample_worker_":[3,5],"sanic":19,"save":[0,1,6,7,14,15,16,17,18,19],"say":18,"scalabl":[3,5,6],"scale":[3,5],"schedul":18,"schema":[3,5,6,11,13,14,15,17,18],"scheme":4,"sci":[3,5],"scienc":[6,14],"scientist":[3,5],"scope":[1,2,3,5,6,7,13,14,15],"score":18,"script":[0,2,3,5,6,9,10,12,13,14,17,18,19],"sdk":[1,2,4,6,7,8,11,14,16],"sdk_cred_":17,"sdk_demo_":17,"sdk_profile_":17,"seamless":[3,5],"search":[0,4,6,14,18],"second":[2,6,7,14,19],"secret":[2,7,11,13,16,17,19],"secur":[0,3,5,6,7,11,13],"see":[1,2,3,5,6,7,8,9,11,12,13,14,16,17,18,19],"seed":[1,6,14,15,16],"seek":[6,14],"select":[0,2,4,6,7,8,10,11,12,14,15,16,17,18,19],"select_ai":[1,2,3,4,5,6,7,8,9,10,11,13,14,15,16,17,18],"select_ai_a2a_team":[2,7],"select_ai_admin_password":[16,17],"select_ai_admin_us":[16,17],"select_ai_agent_nam":[3,5],"select_ai_connection_test":12,"select_ai_database_tls_ca_fil":7,"select_ai_db_connect_str":[2,3,5,6,7,9,10,11,12,14,16,17,18,19],"select_ai_db_password":[9,12,19],"select_ai_db_us":[9,12,16,19],"select_ai_document_uri":18,"select_ai_embedding_model":18,"select_ai_max_work":9,"select_ai_model":17,"select_ai_object_storage_credenti":18,"select_ai_oci_region":18,"select_ai_password":[2,3,5,6,7,9,10,11,12,14,16,17,18,19],"select_ai_pool_incr":[9,19],"select_ai_pool_max":[9,19],"select_ai_pool_min":[9,19],"select_ai_profile_nam":[3,5,6,9,14],"select_ai_provider_credenti":18,"select_ai_task_nam":[3,5],"select_ai_team_nam":[3,5],"select_ai_tool_nam":[3,5],"select_ai_us":[2,3,5,6,7,9,10,11,12,14,16,17,18,19],"select_ai_wallet_loc":[2,7],"select_ai_wallet_password":7,"select_ai_worker_count":9,"selectaierror":[0,4],"self":[2,4,6,19],"semant":18,"send":[2,3,6,7,9,14,15,16],"send_prompt":2,"sender":[3,5],"sendmessag":2,"sendstreamingmessag":2,"sensit":[6,15],"sent":[3,4,5,6,14,15,16],"sentenc":[3,5,6,14],"sep":4,"separ":[2,4,6,7,9,12,14,16,17,18],"sequenc":[6,14,15],"sequenti":[3,5,6,9],"seri":[3,5],"serial":2,"serializ":[3,5,6,14],"serv":[2,3,5,7],"server":[0,1,3,5,7,12,13,19],"serverless":2,"servic":[0,1,2,3,4,5,6,7,8,9,10,11,13,14,19],"session":[0,1,3,5,6,13,14,17],"session_ttl_second":[2,7],"set":[1,2,3,4,5,6,7,9,12,14,15,16,17,18,19],"set_attribut":[0,3,5,6,14,15,18],"setup":[2,3,7,13,16,17,18],"sever":[6,13,14,18],"sh":[2,6,14,15,16],"shape":2,"share":[1,2,7,8,10,13,19],"shell":[7,17],"short":[2,10],"shot":7,"should_include_provider_endpoint":16,"show":[0,3,4,5,7,10,11,13],"show_prompt":[4,6,14],"show_sql":[4,6,9,14,16,17,19],"shown":[2,10,11,16],"showprompt":4,"showsql":[4,6,9,14],"shut":19,"shutdown":[2,4,13,19],"side":2,"sign":[2,8,11,15],"signatur":[1,2,7],"silent":2,"similar":[2,3,18],"similarity_threshold":[0,18],"simpl":[7,13,19],"simplifi":2,"simul":6,"sinc":[6,14],"singl":[0,2,3,5,6,7,14,18,19],"sit":2,"site":[6,14],"size":[0,2,4,7,10,18],"sk":11,"skill":[2,3,5],"skip":2,"sky":[6,14],"slack":[3,4,5],"sleep":2,"slice":4,"slow":19,"slower":17,"small":19,"smoke":0,"smtp":[3,5],"smtp_host":[3,5],"snippet":6,"solid":[6,14],"solver":[3,5],"sonnet":16,"soon":9,"sourc":[2,3,5,6,7,8,11,12,13,14,15,16,17,18,19],"source_languag":[6,14,15],"source_team":[3,5],"source_uri":18,"space":4,"spare":19,"special":[3,5],"specif":[2,3,4,5,7,11,13,14,15,16,17,18],"specifi":[3,4,5,6,7,14,15,16,18],"speed":[3,5],"sphinx":12,"split":[4,6,14,18],"splitlin":4,"sport":[3,5],"sql":[0,1,3,4,5,8,9,10,12,13,15,16,19],"sql_id":[6,14],"sql_tool":[3,5],"sqls":6,"sqltoolparam":[3,5],"ss":2,"stabl":2,"stand":[6,14],"standalon":[0,1,8,10,13,19],"star":[6,14],"starlett":19,"start":[2,3,4,5,6,7,9,13,14,18,19],"start_dat":4,"startswith":[3,4,5],"startup":[2,7,13,19],"state":[0,3,4,5,7],"statefulset":2,"statement":[3,4,5,14,17],"static":4,"status":[2,6,14],"step":[2,3,5,6,11,13,14,17,18],"still":[2,8,18],"stop":[2,4,6,14,15,19],"stop_token":[6,14,15],"storag":[0,1,2,3,5,6,7,8,14,18,19],"storage_credenti":18,"store":[0,2,3,5,6,11,13,14,16,17,18,19],"store_tru":17,"stori":[3,5],"str":[2,3,4,5,6,9,14,15,16,18,19],"strategi":3,"stream":[0,4,7,8,15,19],"strict":4,"string":[1,2,3,4,5,6,7,15,17],"strip":4,"structur":[3,5],"studi":[6,14],"style":[0,6,10,13,19],"sub":[2,4],"subcommand":7,"subject":[3,5],"submiss":2,"submit":[2,6,7,9],"submit_database_connect":2,"subnet":2,"subprogram":10,"subscript":17,"subsequ":[2,17],"substitut":[2,4],"substr":4,"succeed":[2,3,5,6,11,12,14,16,18],"success":[2,6,17],"suffici":[2,17],"suffix":[2,3,4,5,17],"suitabl":[4,16],"summar":[0,4,7,13],"summari":[3,5,6,14],"summarize_movie_task":3,"summaryparam":[0,6,14],"summarystyl":0,"sun":[6,14],"supervis":[0,1],"supervisor":[0,1,3,5],"supervisor_ag":[0,1,3,5],"supervisor_task":[0,1,3,5],"suppli":[1,2,3,5,6,7,14,15,17],"support":[0,1,4,5,6,7,8,9,14,15,16,17,18],"suppos":2,"sure":[6,11,14,18],"surfac":[2,4],"surfaceid":2,"swapcas":4,"switch":[2,10,18],"sync":[0,3,9,10],"sync_ordered_result":0,"sync_queue_work":0,"sync_thread_pool":0,"synchron":[0,5,6,9,11,14,18],"synonym":[11,17],"syntax":2,"synthet":[6,13,14],"synthetic_data_attribut":[6,14],"syntheticdataattribut":[0,6,14],"syntheticdataparam":[0,1],"sys_context":17,"sysdat":[3,5],"system":[2,7,11,12,18],"t":4,"tab":4,"tabl":[0,2,3,4,6,7,11,13,14,15,17,18],"table_cr":17,"table_nam":17,"tablespac":[2,17],"tabsiz":4,"tag":0,"tail":19,"take":[2,6,7,14,19],"taken":18,"target":[3,5,6,7,9,11,12,14,15],"target_languag":[6,14,15],"task":[0,1,4,6,7,9,10,13],"task_attribut":[3,5],"task_don":9,"task_id":2,"task_nam":[3,4,5],"task_name_pattern":[3,5],"task_ord":4,"task_pol":2,"task_run":[3,5],"taskattribut":[0,3,5],"taskhistori":[0,1,3],"taskhistoryev":[0,3,4,5],"tb":4,"tcp_connect_timeout":17,"team":[0,1,2,4,7,8,13],"team_exec_id":[3,4,5],"team_nam":[3,4,5],"team_name_pattern":[3,5],"team_run":[3,5],"team_supervisor_inspect":[3,5],"teamattribut":[0,1,3,5],"teamhistori":[0,1,3],"teamhistoryev":[0,3,4,5],"teardown_appcontext":19,"technolog":[3,5,6,14],"tell":[6,14],"temperatur":[6,14,15,16],"templat":2,"temporari":[2,3,5,7,17],"ten":19,"tenanc":11,"tenancy_ocid":11,"termin":[2,7,13,15,19],"terminal_st":2,"test":[0,2,3,4,7,13,16,18],"test_vector_index":18,"text":[2,3,4,5,6,7,13,14,15,16,18,19],"textfield":2,"thank":[6,7,14],"theoret":[6,14],"therefor":2,"thin":[3,6,8,10],"thing":[2,6],"third":[4,13],"though":2,"thread":[9,10,13,19],"threadpoolexecutor":9,"three":[2,4,17],"threshold":18,"thriller":[3,5],"tie":2,"time":[2,3,5,7,9,19],"timeout":[0,19],"timestamp":[2,18],"titan":16,"titl":[0,3,4,5],"titlecas":4,"tls":[0,10],"tmp":2,"tns":2,"tns_admin":[2,19],"togeth":[2,3,5,7,18],"token":[1,2,6,7,14,15],"tool":[0,4,7,9,11,13,16,18,19],"tool_input":[3,5],"tool_nam":[3,4,5],"tool_name_pattern":[3,5],"tool_output":4,"tool_param":[3,5],"tool_run":[3,5],"tool_typ":[3,5],"toolattribut":[0,3,5],"toolhistori":[0,1,3],"toolhistoryev":[0,3,4,5],"toolparam":[0,3,5],"tooltyp":[0,3,4,5],"top":[2,9],"topic":[3,5],"topolog":[0,7],"total":[6,9,10,17,19],"touch":[6,14],"traffic":2,"trail":4,"train":[3,5],"transform":16,"translat":[0,1,2,3,4,5,7,13,15],"tree":2,"tri":[3,4,5,6,7,9,14,16,17,18],"trillion":[6,14],"trip":[6,9,17],"troubleshoot":0,"true":[2,3,4,5,6,8,9,11,14,15,16,17,18],"trunc":[3,5],"truncat":4,"trust":[1,2,7,13],"ttl":[2,7],"tune":[3,5,6,14,15],"tupl":[4,6,14],"twenti":19,"two":[2,4,6,14,18],"txt":[7,12,18],"type":[1,2,3,4,5,6,7,16,18],"typic":[6,18],"u":4,"ultraviolet":[6,14],"unavail":[6,8,14],"uncas":4,"unchang":6,"underlying":[6,14,15,18],"underneath":2,"understand":[6,14,18],"unicod":4,"unicodeencodeerror":4,"union":[6,14,18],"uniqu":[3,6,7,14],"unit":18,"univers":[6,14],"unless":[2,4,6,9,18,19],"unnecessari":17,"unset":[2,18],"unspecifi":1,"untouch":4,"unzip":2,"updat":[0,2,8,15],"updated_attribut":18,"upgrad":[0,6,8,12,17],"upload":[2,18],"upper":[3,4,5,17],"uppercas":4,"upstream":[1,2,7],"uri":[0,3,5,6,7,8,14,18],"url":[2,3,5,6,7,14,16],"urllib":2,"urlopen":2,"us":[2,3,5,6,14,15,16,18],"usabl":4,"usag":17,"use":[0,1,4,6,7,8,9,10,11,12,13,14,15,17],"user":[0,1,3,4,5,6,7,9,10,11,12,13,14,15,16,17,18,19],"user_ai_agent_task_histori":[3,4,5],"user_ai_agent_team_histori":[3,4,5],"user_ai_agent_tool_histori":[3,4,5],"user_ocid":11,"user_or_role_nam":[3,5,6,14,18],"user_queri":[3,5],"userenv":17,"usernam":[2,3,5,11,16,17],"usual":[3,6,14,17,18,19],"utc":18,"utf":4,"util":[3,5],"uuid":[2,3,5,17],"uuid4":[2,3,5,17],"uvicorn":[2,7,8,19],"v":[6,14],"v0":[1,2,7],"v1":16,"v4":18,"valid":[0,1,2,3,4,5,7,15,19],"validate_config":11,"valu":[1,2,3,4,5,6,8,9,10,11,14,15,16,17,18,19],"valueerror":4,"varchar2":17,"vari":[2,3,5,16,17],"variabl":[0,2,3,5,7,10,12,16,17,18,19],"variant":2,"varieti":[3,5],"various":[3,5,6,14],"vector":[1,3,5,7,8,10,11,13,15,16],"vector_db_credential_nam":[0,18],"vector_db_endpoint":[0,18],"vector_db_provid":[0,18],"vector_dimens":[0,18],"vector_distance_metr":[0,18],"vector_index":[4,18],"vector_index_cr":18,"vector_index_nam":[6,14,15,18],"vector_table_nam":[0,18],"vectordbprovid":[0,4,18],"vectordistancemetr":[0,4,18],"vectorindex":[0,3,4,5],"vectorindexattribut":0,"vectorindexnotfounderror":[0,4,18],"venv":[12,17],"verifi":[1,2,6,7,12,14,16,18],"version":[0,1,2,3,4,5,6,8,9,12,17],"via":[4,6,14],"view":[2,3,4,5,6,7,8,14,15,19],"virtual":[1,2,12,17],"visibl":[2,6,7,14,18],"vpc":2,"wait":[0,2,3,5,7,9,18],"wait_for_complet":18,"wait_timeout":19,"waiting_for_human":[3,5],"walkthrough":17,"wallet":[0,7,8,17,19],"wallet_databas":2,"wallet_loc":10,"wallet_password":[2,10],"want":[3,5,6,7,11,12,13,14,18],"war":[3,5],"water":[6,14],"wave":[6,14],"way":[5,6,14,19],"web":[4,10,13,14],"web_search_ag":[3,5],"web_search_task":[3,5],"web_search_team":[3,5],"web_search_tool":[3,5],"websearch":[3,4,5],"websit":[3,5],"well":[2,4,7,11],"western":[3,5],"whenev":2,"whether":[3,4,5,7,10,15,16,18],"whi":[0,7],"while_serv":19,"whitespac":4,"whose":[2,3,4,11,18],"wide":[3,5,6,14,19],"width":4,"wiki":[6,14],"wikipedia":[6,14],"will":[2,3,4,5,6,11,15],"window":[12,17,18],"wire":3,"with_traceback":4,"within":[3,4,5,6,10,18],"without":[1,2,3,4,5,6,7,9,10,11,14,16,17,18,19],"word":[4,18],"work":[2,3,4,5,7,9,10,13,19],"worker":[0,3,5,8,9,10,12,19],"worker_address":[2,7],"worker_count":9,"worker_endpoint":[2,7],"worker_exit":19,"worker_id":[2,7],"worker_servic":[2,7],"worker_tls_ca_fil":7,"worker_tls_cert_fil":7,"worker_tls_key_fil":7,"workflow":[0,2,3,5,6,7,8,13,15,17,18],"workload":[2,3,5,9,13,19],"world":[3,5],"writabl":2,"write":[3,5,6,7],"written":[2,3,5],"wrong":2,"wsgi":19,"www":[3,5],"x":[2,4,6,14,16],"xai":16,"xai_api_key":16,"xai_credenti":16,"xai_profil":16,"xmlcharrefreplac":4,"xxx":11,"y":4,"yaml":2,"year":[3,5],"yes":2,"yield":[3,4,5,19],"your_api_key":17,"your_database_admin_password":17,"your_database_admin_us":17,"your_database_connect_str":17,"your_database_password":17,"your_database_us":17,"your_db_dsn":10,"your_db_password":10,"your_db_us":10,"zero":4,"zfill":4,"zip":[2,9]},"titles":["select_ai documentation","What\u2019s new in 1.5.0","Agent2Agent (A2A)","Tool","Errors, sessions, and result objects","AsyncTool","Async profile lifecycle","Command line interface","Compatibility and requirements","Concurrent prompt processing","Connecting to Oracle Database","Create credential","Installing select_ai","Introduction to Select AI for Python","Profile lifecycle","ProfileAttributes","Provider","Run your first SQL query","Vector Index","Using select_ai with Python web frameworks"],"titleterms":{"What":[1,13],"When":3,"a2a":[0,2,7],"a2ui":2,"access":11,"account":17,"action":[0,6,14],"addit":15,"agent":[0,2,3,4,5,7],"agent2ag":2,"ai":[0,3,5,11,13,16],"ani":2,"anthropicprovid":16,"api":[0,1,5,6,11,13,14,16,18],"async":[0,5,6,11,16,18],"async_as_complet":9,"async_gath":9,"async_pipelin":9,"async_queue_work":9,"asyncag":5,"asynchron":[6,10,13,19],"asyncprofil":6,"asynctask":5,"asyncteam":5,"asynctool":5,"asyncvectorindex":18,"attribut":[0,6,14,15,18],"authent":7,"aw":16,"awsprovid":16,"azur":16,"azureprovid":16,"back":2,"base":[2,14],"bedrock":16,"behavior":19,"build":13,"ca":2,"call":2,"can":13,"cancel":2,"card":[2,7],"certif":2,"chat":[6,7,14],"choos":2,"class":16,"cleanup":2,"cli":[2,7],"cloud":[2,16],"cluster":[2,7],"cohereprovid":16,"combin":2,"command":[0,7],"common":[2,6,14,15],"compat":[2,6,8],"concept":13,"concurr":[0,6,9],"configur":[2,17],"connect":[2,6,7,10,12,14],"consider":1,"consul":2,"control":15,"convers":0,"core":13,"creat":[3,5,11,14,18],"creation":6,"credenti":[0,11],"data":0,"databas":[2,10,17],"definit":[3,5],"delet":[6,11,14,18],"depend":[12,19],"deploy":[2,7],"disabl":[6,14,16],"discoveri":[2,7],"django":19,"document":[0,12],"dynam":[2,7],"enabl":[6,14,16],"end":2,"endpoint":19,"enumer":4,"environ":9,"error":4,"exampl":[3,5,7,15,16,17,19],"expand":1,"expect":11,"explain":[6,14],"export":[3,5],"fastapi":19,"featur":8,"feedback":[6,14],"fetch":18,"file":2,"filter":[6,14],"find":[6,14],"first":17,"fix":[2,7],"flask":19,"form":[2,7],"format":11,"framework":[0,19],"gateway":[2,7],"gcp":16,"gemini":16,"generat":[6,7,14,15],"get":0,"give":[6,14],"googl":[2,16],"googleprovid":16,"gpt":[3,5],"group":15,"handl":[4,19],"health":10,"helper":[3,5],"histori":[3,4,5],"http":2,"https":2,"huggingfaceprovid":16,"import":[3,5],"index":[0,18],"inspect":[3,5],"instal":[2,12,17,19],"instruct":15,"integr":0,"interact":7,"interfac":[0,7],"introduct":13,"isol":2,"level":[6,14],"lifecycl":[3,5,6,14],"lifetim":7,"line":[0,7],"list":[3,5,6,14,15,18],"model":[3,5,14,18],"name":[6,14],"narrat":14,"new":1,"note":2,"oauth":2,"object":[3,4,5,14,15,18],"ocigenaiprovid":16,"openai":[3,5,16],"openaiprovid":16,"oper":2,"option":[2,7],"oracl":[2,10],"oraclevectorindexattribut":18,"overrid":[6,14],"ownership":[0,2],"pattern":19,"persist":2,"pipelin":6,"poll":2,"pool":[9,10,19],"prepar":17,"prerequisit":2,"privat":2,"privileg":0,"process":[0,9],"profil":[0,6,7,14,15,16],"profileattribut":15,"prompt":[0,6,9,14],"protocol":2,"provid":[0,11,16],"public":2,"py":9,"python":[13,17,19],"queri":17,"rag":18,"recip":9,"refer":[0,7],"registr":7,"repl":7,"request":[6,14,19],"requir":[8,12,15],"result":[4,6],"reus":[6,14],"rout":2,"run":[3,5,6,14,17],"s":[1,3,5],"sampl":16,"search":[3,5],"secur":2,"select":[3,5,13],"select_ai":[0,12,19],"server":2,"servic":16,"session":[2,4,7],"share":[0,3,5,6,14,18],"show":[6,14],"size":[9,19],"smoke":12,"sql":[6,7,14,17],"standalon":[2,7],"start":0,"state":2,"stream":[2,6,14],"summar":[6,14],"summari":[0,7,9],"supervis":[3,5],"support":[2,3,13],"sync":[5,11,16],"sync_ordered_result":9,"sync_queue_work":9,"sync_thread_pool":9,"synchron":[10,13,19],"synthet":0,"task":[2,3,5],"team":[3,5],"test":12,"timeout":[2,7,17],"tls":[2,7],"tool":[3,5],"topolog":2,"translat":[6,14],"troubleshoot":2,"updat":[6,14,18],"upgrad":1,"use":[2,3,5,16,18,19],"user":2,"valu":7,"variabl":9,"vector":[0,18],"vectorindex":18,"vectorindexattribut":18,"version":13,"wait":[17,19],"wallet":[2,10],"web":[0,3,5,19],"whi":2,"worker":[2,7]}}) diff --git a/docs/user_guide/a2a.html b/docs/user_guide/a2a.html new file mode 100644 index 0000000..b253c9d --- /dev/null +++ b/docs/user_guide/a2a.html @@ -0,0 +1,1523 @@ + + + + + + + + 1. Agent2Agent (A2A) — Select AI for Python 1.5.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    + +
    +

    1. 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.

    +

    Make two independent choices: where the server runs (topology) and +when database connection values are supplied (fixed or dynamic). +For example, a clustered deployment can use fixed credentials, and a +standalone deployment can ask each client for credentials.

    +
    +Select AI A2A server demo from the project README + +
    +

    A Select AI agent team accessed through an A2A client.

    +
    +
    +
    +

    1.1. Choose a topology

    +
      +
    • Standalone: run one public select-ai a2a serve process. It handles +A2A requests and database work locally. Start here for a single server; +Consul and a separate worker are not required.

    • +
    • Clustered: run a public gateway with +select-ai a2a serve --deployment clustered and one or more +select-ai a2a worker processes. Consul lets the gateway discover healthy +workers and route each session back to its worker. Use this when database +sessions need to run across multiple workers. See +Why clustered deployments use Consul for the role of +this additional service.

    • +
    +
    +
    +

    1.2. Why clustered deployments use Consul

    +

    Consul is a separate service that keeps a directory of running workers and +records which worker owns each A2A session. The gateway consults this directory +to answer two questions: “Where can I start a new session?” and “Where should I +send the next request for an existing session?”

    +

    This matters because each database session runs in a child process on one +worker. That process holds the session’s database connection and credentials. +Later prompts, task-status requests, and cancellation requests must reach that +same worker. A different worker cannot continue using its live connection.

    +

    Consul provides three things for the gateway:

    +
      +
    • Worker discovery: each worker registers its address when it starts. The +gateway can find workers without a manually maintained list of addresses.

    • +
    • Worker health: workers send regular heartbeats to Consul to report that +they are running. The gateway selects a worker whose health check is passing +when opening a new session.

    • +
    • Session and task routing: the gateway records the worker assigned to a +session and the session associated with each task. Subsequent requests can +reach the worker that owns them, even when other workers are available.

    • +
    +

    For example, suppose two workers, A and B, are running. The gateway opens your +session on worker A and records that assignment in Consul. Your next prompt +and a request to check your task’s status both go to A. Another client’s new +session can run on B. If A stops, the gateway cannot move its live database +connection to B; the client must reconnect to open a new session. See +Session isolation, routing, and cleanup +for expiry and reconnection behavior.

    +

    Consul stores worker addresses and temporary routing records. Database +passwords are not stored in Consul; task results and conversation history are +stored in Oracle Database. See Persistent Oracle-backed state +for the database storage details.

    +

    When do you need it? The current clustered implementation requires Consul, +even with only one worker or fixed database credentials. Standalone deployment +handles sessions locally and does not require Consul. For a first deployment, +use standalone unless you need separate worker processes across machines.

    +
    +
    +

    1.3. Choose how connections are configured

    +

    Each session needs four values: a database DSN, username, password, and an +existing AI Agent Team name. Configure these on a2a serve with --dsn, +--user, --password, and --team, or their environment fallbacks.

    +
      +
    • Fixed: configure all four values before starting the server. Clients +send prompts without completing a connection form. In standalone topology, +the server uses a shared connection pool. In clustered topology, the gateway +automatically opens a session on a worker using the configured values.

    • +
    • Dynamic: leave one or more values unset. The client’s first message +receives an A2UI connection form asking for those missing values. After the +client submits the form, the server opens an isolated database session and +the client can send prompts using that A2A context ID. The client must +support the A2UI handshake; see the runnable samples below.

    • +
    +

    For example, configure --dsn and --team for an application that always +uses one database and one team, but lets each user connect with their own +username and password. The form then asks only for username and password. +Leave all four values unset to let the client choose the database and team +as well. Values configured on the server cannot be changed by the client.

    +

    “Dynamic” describes collecting connection values when a session starts. +Changing a database or user requires a new session. It does not switch the +connection underneath an existing session.

    + + +++++ + + + + + + + + + + + + + + + + +
    Supported combinations

    Topology

    Fixed connection

    Dynamic connection

    Standalone

    Shared async pool; streaming supported.

    Local child process per session; task polling supported.

    Clustered

    Worker session opened automatically; task polling supported.

    Worker session opened after form submission; task polling supported.

    +

    Dynamic standalone and all clustered sessions do not support streaming. +Standalone dynamic mode requires one Uvicorn process. Clustered mode requires +Consul and at least one worker even when the connection is fixed. +See A2A CLI examples for commands and +Command Line Interface for options.

    +
    +Standalone A2A server and clustered gateway-worker architecture + +
    +

    Select AI A2A topologies. Dynamic sessions can run inside the standalone +server or through the clustered gateway and worker processes.

    +
    +
    +
    +
    +

    1.4. 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. Clustered deployments also need +Consul; standalone deployments do not.

    +

    The --deployment option defaults to standalone. Set it to +clustered to use a gateway and workers. There is no separate connection-mode +CLI flag: the connection is fixed when all four connection values are supplied +and dynamic when any are missing. Environment values count as supplied values. +See A2A CLI examples for complete commands.

    +

    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 all supported combinations:

    +
    +
    /.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.

    +
    +
    +
    +
    +

    1.5. Installation and prerequisites

    +

    Install the A2A dependencies before running the server, gateway, or worker:

    +
    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 database user also needs CREATE TABLE, CREATE VIEW, and sufficient +quota in its default tablespace for A2A task and context storage. Ask a database +administrator to grant these privileges to each database user that will open +an A2A session. The SDK’s grant_privileges() helper grants package execute +privileges; it does not grant these storage privileges or tablespace quota.

    +

    At initialization, the runtime creates missing internal tables and executes +CREATE OR REPLACE VIEW for its read-only views. CREATE VIEW is therefore +required even when the tables already exist. See +Persistent Oracle-backed state.

    +

    The fixed standalone server needs all database values at startup. Dynamic +standalone and clustered sessions can collect missing values through A2UI. +Clustered deployment additionally requires Consul and at least one worker; +workers must be able to reach both Consul and Oracle Database.

    +
    +
    +

    1.6. 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.

    + + +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Common A2A operations

    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.

    +
    +
    +

    1.7. Standalone A2A server

    +
    +

    1.7.1. Fixed database connection

    +

    When all four connection values are configured at startup, the standalone +server uses one shared asynchronous connection pool for the selected team. +This mode supports streaming and does not need Consul or a separate worker. +Replace the example values with your database username, password, host, port, +service name, team, and CA-file path. Supply all four connection values to +use fixed standalone mode; omitting any value selects dynamic standalone mode.

    +
    select-ai a2a serve \
    +    --deployment standalone \
    +    --team ORACLE_AI_DATABASE_AGENT \
    +    --user DB_USERNAME \
    +    --password 'DB_PASSWORD' \
    +    --dsn 'dbhost.example.com:1521/db_service' \
    +    --database-tls-ca-file /run/secrets/database-ca.pem \
    +    --host 0.0.0.0 \
    +    --port 8000 \
    +    --public-url https://a2a.example.com
    +
    +
    +

    The --database-tls-ca-file option is only needed when the database +certificate is signed by a CA not already trusted by the system. The +--public-url value 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:

    +
    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.

    +
    +
    +

    1.7.2. Dynamic standalone sessions

    +

    If one or more of --dsn, --user, --password, and --team are +omitted, the standalone server asks the A2A client for those values through an +A2UI form. The server creates an isolated child process for each session. This +is still standalone: it needs no Consul service and no a2a worker process. +Run one Uvicorn process for this mode. Dynamic standalone sessions support +task polling but do not support streaming.

    +
    select-ai a2a serve \
    +    --deployment standalone \
    +    --host 0.0.0.0 \
    +    --port 8000 \
    +    --public-url https://a2a.example.com
    +
    +
    +

    If you set some connection values at startup, the form asks only for the +remaining values. See A2A CLI examples for the +clustered gateway and worker commands.

    +
    +
    +

    1.7.3. End-user OAuth and session ownership

    +

    By default, select-ai a2a serve does not require an application OAuth +token. Each A2A context maps to its own Select AI conversation, while records +use a shared owner scope (anonymous in fixed standalone mode and +a2a-conversation in session-based modes). A hosting platform such as +private Cloud Run can authenticate the calling service, but the A2A server +does not verify which human user owns the conversation.

    +

    Add --require-oauth to require a bearer token and derive the owner scope +from it. Configure a trusted upstream service to validate the token first:

    +
    select-ai a2a serve \
    +    --deployment standalone \
    +    --require-oauth \
    +    --team ORACLE_AI_DATABASE_AGENT \
    +    --user DB_USERNAME \
    +    --password 'DB_PASSWORD' \
    +    --dsn 'dbhost.example.com:1521/db_service' \
    +    --database-tls-ca-file /run/secrets/database-ca.pem \
    +    --host 0.0.0.0 \
    +    --port 8000 \
    +    --public-url https://a2a.example.com
    +
    +
    +

    Every request to the A2A JSON-RPC endpoint must then contain +Authorization: Bearer .... Agent Card discovery remains public. The SDK +requires bearer syntax and derives an owner scope, but does not validate token +signatures, expiration, issuer, or audience. It accepts arbitrary nonempty +bearer values. This flag alone does not establish a verified end-user identity.

    +

    The deployment must validate tokens in a trusted authenticating proxy or +service before forwarding them, and prevent callers from reaching the A2A +listener through a path that bypasses that validation. Cloud Run IAM validation +of a calling service is separate from validation of the end-user token.

    +

    The server uses the derived owner and A2A context to scope task and context +records and the Select AI conversation. For an upstream-validated JWT with +iss and sub claims, ownership remains stable across token refreshes. +For an opaque access token, ownership is derived from a hash of the token; +refreshing it starts a new owner scope. Tokens are not persisted by this +authentication backend.

    +

    The Agent Card advertises bearer security only with --require-oauth.

    +
    +
    +

    1.7.4. Agent Card discovery

    +

    Fetch the card after starting the server:

    +
    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:

    +
    select-ai a2a agent-card \
    +    --team ORACLE_AI_DATABASE_AGENT \
    +    --public-url https://a2a.example.com
    +
    +
    +

    This command prints JSON similar to:

    +
    {
    +  "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.

    +
    +
    +

    1.7.5. Tasks, polling, streaming, and cancellation

    +

    The fixed 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:

    +
    import json
    +import uuid
    +from urllib.request import Request, urlopen
    +
    +ENDPOINT = "http://127.0.0.1:8000/a2a/jsonrpc/"
    +PROMPT = "What were last month's sales by product category?"
    +
    +
    +# There is intentionally no "configuration": {"blocking": false} here.
    +# Omitting it is blocking by default, so this call waits for the database work
    +# to finish before the server returns the Task.
    +request = Request(
    +    ENDPOINT,
    +    data=json.dumps(
    +        {
    +            "jsonrpc": "2.0",
    +            "id": str(uuid.uuid4()),
    +            "method": "message/send",
    +            "params": {
    +                "message": {
    +                    "messageId": str(uuid.uuid4()),
    +                    "role": "user",
    +                    "parts": [{"kind": "text", "text": PROMPT}],
    +                }
    +            },
    +        }
    +    ).encode(),
    +    headers={"Content-Type": "application/json"},
    +    method="POST",
    +)
    +
    +with urlopen(request) as response:  # noqa: S310
    +    body = json.load(response)
    +if "error" in body:
    +    raise RuntimeError(body["error"])
    +
    +task = body["result"]
    +print(f"Task {task['id']}: {task['status']['state']}")
    +print(json.dumps(task, indent=2))
    +
    +
    +

    The blocking sample omits configuration. Example output (task IDs and model responses vary):

    +
    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.

    +

    First, define the endpoint, prompt, terminal states, and JSON-RPC helper:

    +
    import json
    +import time
    +import uuid
    +from urllib.request import Request, urlopen
    +
    +ENDPOINT = "http://127.0.0.1:8000/a2a/jsonrpc/"
    +PROMPT = "What were last month's sales by product category?"
    +TERMINAL_STATES = {"completed", "failed", "canceled", "rejected"}
    +
    +
    +def call(method, params):
    +    """Make one A2A v0.3 JSON-RPC call."""
    +    request = Request(
    +        ENDPOINT,
    +        data=json.dumps(
    +            {
    +                "jsonrpc": "2.0",
    +                "id": str(uuid.uuid4()),
    +                "method": method,
    +                "params": params,
    +            }
    +        ).encode(),
    +        headers={"Content-Type": "application/json"},
    +        method="POST",
    +    )
    +    with urlopen(request) as response:  # noqa: S310
    +        body = json.load(response)
    +    if "error" in body:
    +        raise RuntimeError(body["error"])
    +    return body["result"]
    +
    +
    +
    +
    +

    Append the send-and-poll loop to that setup:

    +
    # blocking=False returns immediately with a Task. Database work continues on
    +# the server while this client polls tasks/get.
    +task = call(
    +    "message/send",
    +    {
    +        "message": {
    +            "messageId": str(uuid.uuid4()),
    +            "role": "user",
    +            "parts": [{"kind": "text", "text": PROMPT}],
    +        },
    +        "configuration": {"blocking": False},
    +    },
    +)
    +
    +task_id = task["id"]
    +print(f"Task {task_id}: {task['status']['state']}")
    +
    +while task["status"]["state"] not in TERMINAL_STATES:
    +    time.sleep(1)
    +    task = call("tasks/get", {"id": task_id})
    +    print(f"Task {task_id}: {task['status']['state']}")
    +
    +print(json.dumps(task, indent=2))
    +
    +
    +

    The polling sample sends a non-blocking request and then calls tasks/get:

    +
    Task 42b...: submitted
    +Task 42b...: working
    +Task 42b...: completed
    +{
    +  "id": "42b...",
    +  "status": {"state": "completed", "timestamp": "..."},
    +  "artifacts": [{"name": "database-agent-result", "parts": ["..."]}]
    +}
    +
    +
    +
    +
    +
    +

    1.8. Persistent Oracle-backed state

    +

    Both standalone and clustered deployments use Oracle implementations of the +A2A task and context stores. In fixed standalone mode, the server initializes +these stores when the application starts. In dynamic standalone mode, each +session child initializes the stores after connecting. In clustered mode, +select-ai a2a serve --deployment clustered does not connect to Oracle +itself; the select-ai a2a worker command starts the internal worker, and +each connected worker session initializes the stores after it opens its +database connection. On first initialization, the stores create these tables +if they do not already exist:

    +
    +
    DBMS_AI_A2A_TASKS$ and DBMS_AI_A2A_TASKS

    Stores the task ID, context ID, serialized task JSON, owner, and update +timestamp. It supports task retrieval, filtering, listing, pagination, and +deletion. The object with the $ suffix is the writable internal table; +the object without it is a read-only view of the same columns.

    +
    +
    DBMS_AI_A2A_CONTEXTS$ and DBMS_AI_A2A_CONTEXTS

    Maps an A2A context and owner to an Oracle conversation ID. The object with +the $ suffix is the writable internal table; the object without it is a +read-only view of the same columns.

    +
    +
    +

    When a request starts a new context, Select AI creates an +AsyncConversation and passes its ID to AsyncTeam.run. Later messages in +the same A2A context reuse that conversation, so the team can use the +conversation history. In standalone modes, task and context ownership is +resolved from the A2A request context in fixed standalone mode; an absent owner +is stored as anonymous. Dynamic standalone and clustered runtimes pass the +gateway’s derived owner scope to the child runtime, using a2a-conversation +when OAuth is not required. Routing binds the owner and A2A context ID to an +internal session ID. That internal ID identifies the child process; it is +separate from the client-visible A2A context ID.

    +

    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.

    +
    +
    +

    1.9. Wallet-based database connections

    +

    Wallet-based connections apply to the fixed standalone +select-ai a2a serve path. Dynamic standalone sessions and clustered worker +sessions do not currently accept wallet settings. Pass the unzipped wallet +directory and password with CLI options:

    +
    select-ai a2a serve \
    +    --deployment standalone \
    +    --team ORACLE_AI_DATABASE_AGENT \
    +    --user DB_USERNAME \
    +    --password 'DB_PASSWORD' \
    +    --dsn 'dbhost.example.com:1521/db_service' \
    +    --wallet-location /secure/wallet/Wallet_database \
    +    --wallet-password 'WALLET_PASSWORD' \
    +    --host 0.0.0.0 \
    +    --port 8000 \
    +    --public-url https://a2a.example.com
    +
    +
    +

    --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.

    +
    +
    +

    1.10. Private database CA certificates

    +

    For a database whose server certificate is signed by a private CA, add +--database-tls-ca-file /path/to/database-ca.pem to the relevant +select-ai a2a serve command. The CLI reads PEM from that file and uses it +to validate Oracle Database TLS in fixed standalone, dynamic standalone, and +clustered sessions. In clustered mode, the gateway sends the certificate to +the worker when it opens a session. The separate --worker-tls-ca-file +option validates the worker certificate on the gateway-to-worker connection.

    +
    +
    +

    1.11. Database call timeout

    +

    Use --database-call-timeout-seconds to configure the maximum duration of +an Oracle Database call. It defaults to 120 seconds and sets the timeout for +fixed standalone connections and dynamic standalone or clustered worker +sessions. In clustered mode, the gateway uses this value to derive its request +deadline, so set the same value on the gateway and worker.

    +
    +
    +

    1.12. Clustered A2A gateway and worker

    +

    The clustered topology separates the public A2A protocol endpoint from the +database-bearing runtime. The gateway can start without database credentials; +an A2UI form collects them from the client. Values configured on the gateway +are prefilled and the form asks only for the missing values. A worker registers +with Consul and waits for the gateway to assign sessions. Each worker session +uses the persistent Oracle-backed task, context, and conversation state +described in Persistent Oracle-backed state; Consul stores only 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:

    +
    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:

    +
    # Terminal 1: Consul
    +consul agent -dev -bind=127.0.0.1 -client=127.0.0.1
    +
    +# Terminal 2: worker
    +select-ai a2a worker \
    +    --host 127.0.0.1 \
    +    --port 8081 \
    +    --consul-url http://127.0.0.1:8500 \
    +    --worker-id local-worker \
    +    --worker-endpoint http://127.0.0.1:8081
    +
    +# Terminal 3: public gateway
    +select-ai a2a serve \
    +    --deployment clustered \
    +    --host 127.0.0.1 \
    +    --port 8000 \
    +    --public-url http://127.0.0.1:8000 \
    +    --consul-url http://127.0.0.1:8500
    +
    +
    +

    The worker uses --consul-url, --worker-id, and the optional +--worker-endpoint when registering with Consul. Their environment +fallbacks are CONSUL_HTTP_URL, WORKER_ID, and WORKER_ENDPOINT; +explicit command-line values take precedence. --port is both the listening +port and the registered port. Cluster manifests can set WORKER_ADDRESS to +the pod IP when no advertised endpoint is supplied. Clustered serve uses +PUBLIC_URL, CONSUL_HTTP_URL, WORKER_SERVICE (default +select-ai-a2a-worker), and SESSION_TTL_SECONDS.

    +

    Pass any fixed connection values with --dsn, --user, --password, +and --team. The generated form contains only the remaining values. Use +--a2ui-form to supply a validated custom form for those missing values.

    +

    The worker must be able to resolve the submitted DSN. For a TNS alias, set +TNS_ADMIN in the worker terminal before starting it. The gateway session +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.

    +
    +

    1.12.1. Clustered deployment with a fixed connection

    +

    Yes, a clustered deployment can use fixed connection information. Start +Consul and the worker as shown above, then run this gateway command in place +of the dynamic gateway command. Supply the connection values on the gateway; +the gateway passes them to the selected worker when opening each session:

    +
    select-ai a2a serve \
    +    --deployment clustered \
    +    --host 127.0.0.1 \
    +    --port 8000 \
    +    --public-url http://127.0.0.1:8000 \
    +    --consul-url http://127.0.0.1:8500 \
    +    --dsn 'dbhost.example.com:1521/db_service' \
    +    --user APP_USER \
    +    --password "$SELECT_AI_PASSWORD" \
    +    --team ORACLE_AI_DATABASE_AGENT
    +
    +
    +

    Set SELECT_AI_PASSWORD to the database password before running the +command. The client sends an ordinary A2A prompt; the gateway skips the form +and creates the worker session automatically. Workers still isolate sessions +in child processes. All clients use the configured database identity and +team. Fixed clustered connections retain the clustered restrictions: no +streaming and no wallet parameters for the database session.

    +

    To collect each user’s credentials instead, remove --user and +--password and unset their environment fallbacks SELECT_AI_USER and +SELECT_AI_PASSWORD in the gateway environment. Keep --dsn and +--team. The same gateway and workers now serve dynamic sessions whose +forms ask only for username and password.

    +
    +
    +

    1.12.2. A2UI database-connection form

    +

    When connection properties are missing, the first message/send for a +context returns a temporary task containing a database connection form. The +form is an A2UI v0.9 data artifact with the application/json+a2ui MIME +type. It contains only the values not configured on the server:

    + + ++++ + + + + + + + + + + + + + + + + + + + + + + +
    Dynamic connection form

    Field

    Purpose

    connection_url

    Oracle connect descriptor, simplified connect string, or TNS alias +available to the worker.

    username

    Database user for this session.

    password

    Database password. The form renders this field as obscured input.

    ai_agent

    Select AI Agent Team to run in this session.

    submit_database_connection

    A2UI action that submits exactly the displayed values.

    +

    For example, a clustered server started with --dsn and --team renders +only username and password. Server-configured values are immutable and cannot +be overridden by a form submission. When all four values are configured, the +server skips the form and opens the worker session automatically.

    +

    Use --a2ui-form PATH to replace the generated form. The file must be a +top-level JSON array of A2UI v0.9 operations using the advertised catalog and +one consistent template surfaceId. It must contain exactly one +submit_database_connection action whose context submits exactly the +missing canonical properties: connection_url, username, password, +and/or ai_agent. A collected password must use an obscured TextField +and the +template must not contain a password default. The server validates the file at +startup and replaces its template surface ID with a fresh ID every time the +form is displayed.

    +

    After the action is submitted in clustered mode, the gateway selects a healthy +worker through Consul and opens a session. In dynamic standalone mode, the +server opens the session locally. 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 dynamic-session samples perform this handshake automatically. They use the +repository-local helper module +samples/a2a/dynamic/_common.py; +_common +is not a package that users install with pip. Running the samples from the +repository root as shown below makes that helper available automatically.

    +

    The helper functions are:

    +
    +
    call(method, params)

    Sends one A2A v0.3 JSON-RPC request to the configured server endpoint.

    +
    +
    connect(prompt)

    Sends the initial prompt, reads the A2UI connection form, submits the +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 dynamic session. Passing +blocking=False adds the non-blocking request option.

    +
    +
    print_task_summary(task)

    Prints the task state, result artifact name, and text parts without dumping +the connection-form details.

    +
    +
    +

    If you copy a dynamic-session sample into another directory, copy the +_common.py helper +with it, or replace these helpers with an A2A client implementation of your +own.

    +

    The complete dynamic-session setup is also documented in the +dynamic sample README. +The examples below are the +blocking_task.py sample +and the +task_poll.py sample.

    +
    from _common import connect, print_task_summary, send_prompt
    +
    +PROMPT = "What were last month's sales by product category?"
    +
    +
    +context_id = connect(PROMPT)
    +task = send_prompt(PROMPT, context_id)
    +if task["artifacts"][0]["name"] != "database-agent-result":
    +    raise RuntimeError(
    +        "Expected a database result, got " f"{task['artifacts'][0]['name']}"
    +    )
    +print_task_summary(task)
    +
    +
    +

    Set the database values before running the sample:

    +
    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:

    +
    python samples/a2a/dynamic/blocking_task.py
    +
    +
    +

    Example output (task IDs and model responses vary):

    +
    Task 15c...: completed
    +Artifact: database-agent-result
    +The database contains ...
    +
    +
    +

    The dynamic polling sample uses the same connection-form handshake, then +passes configuration.blocking: false and polls tasks/get:

    +
    import time
    +
    +from _common import call, connect, print_task_summary, send_prompt
    +
    +PROMPT = "What were last month's sales by product category?"
    +TERMINAL_STATES = {"completed", "failed", "canceled", "rejected"}
    +
    +
    +context_id = connect(PROMPT)
    +task = send_prompt(PROMPT, context_id, blocking=False)
    +print(f"Task {task['id']}: {task['status']['state']}")
    +
    +while task["status"]["state"] not in TERMINAL_STATES:
    +    time.sleep(1)
    +    task = call("tasks/get", {"id": task["id"]})
    +    print(f"Task {task['id']}: {task['status']['state']}")
    +
    +if task["artifacts"][0]["name"] != "database-agent-result":
    +    raise RuntimeError(
    +        "Expected a database result, got " f"{task['artifacts'][0]['name']}"
    +    )
    +print_task_summary(task, include_task=False)
    +
    +
    +
    Task 91a...: submitted
    +Task 91a...: working
    +Task 91a...: completed
    +Artifact: database-agent-result
    +The database contains ...
    +
    +
    +
    +
    +

    1.12.3. Session isolation, routing, and cleanup

    +

    Each successful connection action binds the owner and A2A context ID to one +worker session with its own internal session 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-a2a/sessions/ and select-ai-a2a/tasks/ key-value records, +which +route a context or task back to the worker that owns it.

    • +
    +

    The task and conversation contents are not stored in Consul. They are stored +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.

    +
    +
    +

    1.12.4. Public gateway HTTPS

    +

    The --tls-* options on select-ai a2a serve configure the public A2A +listener. They do not configure gateway-to-worker traffic.

    +
      +
    • With no TLS options, Uvicorn listens over HTTP.

    • +
    • Supply --tls-cert-file and --tls-key-file together to serve HTTPS. +Clients verify the gateway certificate using their configured trust store.

    • +
    • Add --tls-ca-file to require clients to present a certificate signed by +that CA. This enables mTLS on the public connection. The CA file requires +the certificate and key.

    • +
    +

    For example, this command serves HTTPS without requiring client certificates:

    +
    select-ai a2a serve \
    +    --team ORACLE_AI_DATABASE_AGENT \
    +    --user DB_USERNAME \
    +    --password DB_PASSWORD \
    +    --dsn 'dbhost.example.com:1521/db_service' \
    +    --host 0.0.0.0 \
    +    --port 8000 \
    +    --public-url https://a2a.example.com \
    +    --tls-cert-file /run/secrets/gateway.crt \
    +    --tls-key-file /run/secrets/gateway.key
    +
    +
    +

    Add --tls-ca-file /run/secrets/client-ca.pem when public clients must +authenticate with client certificates. --public-url controls the URL +advertised in the Agent Card; the TLS options control Uvicorn’s listener.

    +
    +
    +

    1.12.5. Gateway-to-worker HTTP and TLS

    +

    The gateway-to-worker connection is independent of the public A2A listener. +The public listener can use HTTPS while workers use HTTP, or the gateway can +use HTTPS to workers while its public listener uses HTTP.

    + + +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
    Gateway-to-worker TLS options

    Option

    Process

    Purpose

    --tls-cert-file and --tls-key-file

    Worker

    Enable HTTPS on the worker. Supply both together.

    --tls-ca-file

    Worker

    Optional CA for requiring and verifying gateway client certificates. +Supplying it enables the worker side of mTLS.

    --worker-tls-ca-file

    Gateway

    Optional CA bundle for verifying the worker’s server certificate. +Supply it alone for HTTPS with a private CA and no mTLS. If omitted, +Requests uses its default CA bundle (which can be overridden with +REQUESTS_CA_BUNDLE).

    --worker-tls-cert-file and --worker-tls-key-file

    Gateway

    Optional gateway client certificate and key. Supply both together; +the worker must require and trust this client certificate for mTLS.

    +

    Whenever a gateway --worker-tls-* option is supplied, register the worker +with an HTTPS URL using --worker-endpoint. If no gateway worker TLS option +is supplied, Requests still verifies an HTTPS worker certificate against its +default CA bundle. When the worker has no advertised endpoint, the gateway +builds an HTTP URL from the worker address and port in the Consul record.

    +

    For HTTPS without mTLS, configure the worker’s certificate and key, then +advertise its HTTPS URL. If the certificate uses a private CA, give that CA +to the gateway with --worker-tls-ca-file. Do not set the gateway client +certificate or the worker’s --tls-ca-file in this mode:

    +
    select-ai a2a worker \
    +    --worker-endpoint https://worker.example.com:8443 \
    +    --tls-cert-file /run/secrets/worker.crt \
    +    --tls-key-file /run/secrets/worker.key
    +
    +select-ai a2a serve \
    +    --deployment clustered \
    +    --worker-tls-ca-file /run/secrets/private-worker-ca.pem
    +
    +
    +

    For mTLS, the worker requires a client certificate with --tls-ca-file. +The gateway presents its certificate and key with --worker-tls-cert-file +and --worker-tls-key-file. The gateway also needs +--worker-tls-ca-file when the worker certificate is signed by a private +CA; omit it when the Requests default CA bundle trusts the worker certificate. +The two directions can use the same CA or different CAs:

    +
    select-ai a2a worker \
    +    --worker-endpoint https://worker.example.com:8443 \
    +    --tls-cert-file /run/secrets/worker.crt \
    +    --tls-key-file /run/secrets/worker.key \
    +    --tls-ca-file /run/secrets/gateway-client-ca.pem
    +
    +select-ai a2a serve \
    +    --deployment clustered \
    +    --worker-tls-ca-file /run/secrets/worker-server-ca.pem \
    +    --worker-tls-cert-file /run/secrets/gateway-client.crt \
    +    --worker-tls-key-file /run/secrets/gateway-client.key
    +
    +
    +

    The worker CA validates the gateway’s client certificate. The gateway CA +validates the worker’s server certificate. This protects only the +gateway-to-worker connection; it does not configure TLS for the public A2A +listener or Oracle Database connections.

    +
    +
    +
    +

    1.13. 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.

    +
    +

    1.13.1. Standalone Google Cloud deployment

    +

    gcloud/standalone/deploy.sh +deploys one private Cloud Run service. Select its connection provisioning +explicitly:

    +
    gcloud/standalone/deploy.sh \
    +    --project PROJECT_ID \
    +    --connection-mode dynamic \
    +    --build
    +
    +
    +

    The script creates or reuses an Artifact Registry repository, runtime service +account, any requested Secret Manager secrets, and Cloud Run service. With no +database options, dynamic standalone mode fixes neither the Connection URL nor +AI Agent; its A2UI form requests Connection URL, username, password, and AI +Agent. Pass +--db-dsn-secret or --a2a-team to fix either value independently, or +--rotate-db-config to configure the deployment’s Connection URL. Fixed +mode prompts for Connection URL, username, and password, requires +--a2a-team, and creates the shared startup pool. The service is private; the +script grants +run.routes.invoke to the active deployment identity and the Gemini +Enterprise Discovery Engine service agent.

    +

    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:

    +
      +
    • --connection-mode: required fixed or dynamic provisioning.

    • +
    • --service: Cloud Run service name; use a different service for each +deployment profile.

    • +
    • --a2a-team: fix the AI Agent in the deployment; required in fixed mode.

    • +
    • --require-oauth: require an upstream-validated end-user bearer token +and derive an owner scope as well as the A2A conversation scope. The SDK +does not validate the token itself; see End-user OAuth and session ownership.

    • +
    • --pool-max-size: maximum Oracle connections per Cloud Run instance.

    • +
    • --max-instances: Cloud Run instance limit; dynamic standalone mode +requires one.

    • +
    • --wallet-archive: upload or replace an Autonomous Database wallet ZIP +in fixed mode.

    • +
    • --db-dsn-secret: fix the Connection URL using an existing Secret Manager +secret.

    • +
    • --rotate-db-config: prompt for and create or rotate the deployment’s +database values.

    • +
    +

    For a wallet deployment, --wallet-archive stores the ZIP and wallet +password in service-specific Secret Manager secrets. Cloud Run mounts the ZIP +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.

    +

    The Google Cloud scripts do not require end-user OAuth by default. Cloud Run +IAM authenticates Gemini Enterprise, and a2a serve separates database +sessions by A2A conversation without asserting a verified human owner. Add +--require-oauth to the deployment command when user ownership is required, +and ensure a trusted upstream service validates the end-user token. The flag +alone does not verify identity. Gemini Enterprise must then be configured with end-user OAuth rather +than Skip & Finish. It uses X-Serverless-Authorization for Cloud Run +IAM and sends the user token separately in Authorization.

    +
    +
    +

    1.13.2. Clustered Google Cloud deployment

    +

    gcloud/cluster/deploy.sh +deploys the clustered topology: Cloud Run for the +public server, GKE Autopilot for worker replicas, and Consul for discovery and +routing.

    +
    gcloud/cluster/deploy.sh \
    +    --project PROJECT_ID \
    +    --region us-central1 \
    +    --worker-replicas 3
    +
    +
    +

    The script and +gcloud/cluster/cloudbuild.yaml +perform these steps:

    +
      +
    1. Enable the required Google Cloud APIs and create or reuse Artifact Registry +and a GKE Autopilot cluster.

    2. +
    3. Configure additive VPC DNS so StatefulSet worker names resolve to current +Pod IPs after a worker is recreated.

    4. +
    5. Deploy the A2A namespace, internal Consul service, and worker service.

    6. +
    7. Build and publish the image with Cloud Build.

    8. +
    9. Deploy the requested worker replicas with select-ai a2a worker and the +selected session TTL.

    10. +
    11. Deploy Cloud Run with select-ai a2a serve --deployment clustered and +set PUBLIC_URL to the final Cloud Run URL.

    12. +
    +

    The Cloud Run server uses direct VPC egress to reach the internal Consul +load-balancer address and worker endpoints. The GKE worker service is headless +service discovery, not a public load balancer. Consul selects a healthy worker +when a session opens; the route is then pinned to that worker.

    +

    Important clustered 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.

    • +
    • --conversation-retention-days: number of days to retain Oracle +conversations (default 7; use 0 to disable automatic removal).

    • +
    • --enable-worker-mtls: create and use server-to-worker certificates.

    • +
    • --rotate-worker-mtls: replace the existing test certificates and restart +the worker workload.

    • +
    +

    The default clustered deployment uses private-VPC HTTP between Cloud Run and +GKE. With --enable-worker-mtls, the script creates short-lived test PKI +material in Secret Manager, creates the Kubernetes TLS secrets, deploys the +worker StatefulSet variant, and mounts the server client certificate into +Cloud Run. The worker certificate uses the GKE StatefulSet DNS name, so the +--gke-dns-domain value must remain consistent with the cluster.

    +

    The clustered deployment does not upload an Oracle wallet because the dynamic +worker session path currently accepts only DSN/user/password. If database mTLS +is required, use the standalone deployment or provide a separate database +connection mechanism to the worker implementation.

    +

    The deployment files are intended to be read together:

    + + ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Google Cloud A2A deployment files

    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/cluster/deploy.sh

    Creates or reuses the clustered GKE/Cloud Run topology and supplies build +substitutions.

    gcloud/cluster/cloudbuild.yaml

    Builds the image, deploys Consul and workers, and deploys Cloud Run.

    gcloud/cluster/gke manifests

    Namespace, Consul, headless worker service, and HTTP or mTLS worker +workloads.

    gcloud/cluster/README.md

    Explains the topology, DNS, mTLS test mode, and operational details.

    +
    +
    +
    +

    1.14. Troubleshooting and security notes

    +
      +
    • If the Agent Card advertises the wrong URL, set --public-url or +PUBLIC_URL. The URL must be the client-visible base URL, not an internal +container address.

    • +
    • If the gateway returns the connection form repeatedly, check that the worker +is passing in Consul, that the worker can resolve and connect to Oracle, and +that the session TTL has not expired.

    • +
    • 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 Command Line Interface for the common CLI connection options +and Connection for Select AI database connection setup.

    +
    +
    + + +
    +
    +
    +
    + +
    +
    + + + + + diff --git a/docs/user_guide/actions.html b/docs/user_guide/actions.html index 99355c1..5c9600d 100644 --- a/docs/user_guide/actions.html +++ b/docs/user_guide/actions.html @@ -5,16 +5,16 @@ - 1. Supported Actions — Select AI for Python 1.4.0 documentation + 1. Supported Actions — Select AI for Python 1.5.0 documentation + - - + - - - + + + @@ -116,7 +116,7 @@

    Navigation

  • previous |
  • -
  • Python logo
  • +
  • Python logo
  • Python »
  • @@ -124,7 +124,7 @@

    Navigation

  • - Select AI for Python 1.4.0 documentation » + Select AI for Python 1.5.0 documentation »
  • @@ -223,6 +223,19 @@

    1. Supported Actions

    1.1. Action methods

    +

    Request-specific profile attributes can be supplied with the optional +attributes mapping. For example, override additional_instructions for +a single chat request:

    +
    response = profile.chat(
    +    prompt="Write sample Python code to build and test a regression model",
    +    attributes={
    +        "additional_instructions": (
    +            "Return executable code as plain text without Markdown fences."
    +        )
    +    },
    +)
    +
    +
    @@ -283,28 +296,28 @@

    1.2. Choosing an action

    1.3. Examples

    Use a convenience method:

    -
    profile = select_ai.Profile(profile_name="oci_ai_profile")
    -sql = profile.show_sql(prompt="How many promotions?")
    +
    profile = select_ai.Profile(profile_name="oci_ai_profile")
    +sql = profile.show_sql(prompt="How many promotions?")
     

    Use generate with an explicit action:

    -
    profile = select_ai.Profile(profile_name="oci_ai_profile")
    +
    profile = select_ai.Profile(profile_name="oci_ai_profile")
     
     sql = profile.generate(
    -    prompt="How many promotions?",
    +    prompt="How many promotions?",
         action=select_ai.Action.SHOWSQL,
     )
     
     df = profile.generate(
    -    prompt="How many promotions?",
    +    prompt="How many promotions?",
         action=select_ai.Action.RUNSQL,
     )
     

    Use an action selected at runtime:

    -
    action = select_ai.Action("showsql")
    +
    action = select_ai.Action("showsql")
     result = profile.generate(
    -    prompt="How many promotions?",
    +    prompt="How many promotions?",
         action=action,
     )
     
    @@ -317,12 +330,12 @@

    1.4. StreamingRUNSQL because it returns a pandas.DataFrame.

    for chunk in profile.generate(
    -    prompt="What is OCI?",
    +    prompt="What is OCI?",
         action=select_ai.Action.CHAT,
         stream=True,
         chunk_size=4096,
     ):
    -    print(chunk, end="")
    +    print(chunk, end="")
     
    @@ -391,7 +404,7 @@

    Navigation

  • previous |
  • -
  • Python logo
  • +
  • Python logo
  • Python »
  • @@ -399,7 +412,7 @@

    Navigation

  • - Select AI for Python 1.4.0 documentation » + Select AI for Python 1.5.0 documentation »
  • @@ -444,7 +457,7 @@

    Navigation


    - Created using Sphinx 8.2.3. + Created using Sphinx 9.1.0.
    diff --git a/docs/user_guide/agent.html b/docs/user_guide/agent.html index 773024d..71ebc8a 100644 --- a/docs/user_guide/agent.html +++ b/docs/user_guide/agent.html @@ -5,16 +5,16 @@ - 1. Tool — Select AI for Python 1.4.0 documentation + 1. Tool — Select AI for Python 1.5.0 documentation + - - + - - - + + + @@ -66,9 +66,36 @@

    Table of Contents

  • 1. Tool
  • @@ -749,9 +974,9 @@

    1.4. List Toolsimport select_ai import select_ai.agent -user = os.getenv("SELECT_AI_USER") -password = os.getenv("SELECT_AI_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +user = os.getenv("SELECT_AI_USER") +password = os.getenv("SELECT_AI_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") select_ai.connect(user=user, password=password, dsn=dsn) @@ -766,6 +991,74 @@

    1.4. List Tools

    +
    +

    1.5. 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.

    +
    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:

    +
    
    +import os
    +import uuid
    +
    +import select_ai
    +from select_ai.agent import Tool
    +
    +user = os.getenv("SELECT_AI_USER")
    +password = os.getenv("SELECT_AI_PASSWORD")
    +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING")
    +
    +select_ai.connect(user=user, password=password, dsn=dsn)
    +
    +suffix = uuid.uuid4().hex.upper()
    +function_name = f"SAMPLE_CALCULATE_AGE_{suffix}"
    +tool_name = f"SAMPLE_AGE_TOOL_{suffix}"
    +
    +with select_ai.cursor() as cursor:
    +    cursor.execute(
    +        f"""
    +        CREATE OR REPLACE FUNCTION {function_name}(p_birth_date DATE)
    +        RETURN NUMBER IS
    +        BEGIN
    +            RETURN TRUNC(MONTHS_BETWEEN(SYSDATE, p_birth_date) / 12);
    +        END;
    +        """
    +    )
    +
    +tool = Tool.create_pl_sql_tool(
    +    tool_name=tool_name,
    +    function=function_name,
    +    description="Calculate age from a birth date",
    +)
    +
    +try:
    +    print("Tool description:")
    +    print(tool.describe_tool())
    +    print("Tool result:")
    +    print(tool.run_tool('{"p_birth_date":"2000-01-01"}'))
    +finally:
    +    tool.delete(force=True)
    +    with select_ai.cursor() as cursor:
    +        cursor.execute(f"DROP FUNCTION {function_name}")
    +
    +
    +

    The generated tool name and calculated age vary between runs. Representative +output is:

    +

    output:

    +
    Tool description:
    +{"tool_name": "SAMPLE_AGE_TOOL_<generated suffix>", ...}
    +Tool result:
    +<calculated age in years>
    +
    +
    +

    2. Task

    @@ -778,50 +1071,85 @@

    2.
    -class select_ai.agent.TaskAttributes(instruction: str, tools: List[str] | None = None, input: str | None = None, enable_human_tool: bool | None = True)
    +class select_ai.agent.TaskAttributes(instruction: str, tools: List[str] | None = None, input: str | None = None, enable_human_tool: bool | None = True)

    AI Task attributes

    Parameters:
      -
    • instruction (str) – Statement describing what the task is +

    • instruction (str) – Statement describing what the task is meant to accomplish

    • -
    • tools (List[str]) – List of tools the agent can use to +

    • tools (List[str]) – List of tools the agent can use to execute the task

    • -
    • input (str) – Task name whose output will be automatically +

    • input (str) – Task name whose output will be automatically provided by select ai to LLM

    • -
    • enable_human_tool (bool) – Enable agent to ask question +

    • enable_human_tool (bool) – Enable agent to ask question to user when it requires information or clarification during a task. Default value is True.

    +
    +
    +dict(exclude_null=True)
    +
    + +
    +
    +enable_human_tool: bool | None = True
    +
    + +
    +
    +input: str | None = None
    +
    + +
    +
    +instruction: str
    +
    + +
    +
    +json(exclude_null=True)
    +
    + +
    +
    +classmethod keys()
    +
    + +
    +
    +tools: List[str] | None = None
    +
    +
    -class select_ai.agent.Task(task_name: str | None = None, description: str | None = None, attributes: TaskAttributes | None = None)
    +class select_ai.agent.Task(task_name: str | None = None, description: str | None = None, attributes: TaskAttributes | None = None)

    select_ai.agent.Task class lets you create, delete, enable, disable and list AI Tasks

    Parameters:
      -
    • task_name (str) – The name of the AI task

    • -
    • description (str) – Optional description of the AI task

    • +
    • task_name (str) – The name of the AI task

    • +
    • description (str) – Optional description of the AI task

    • attributes (select_ai.agent.TaskAttributes) – AI task attributes

    -create(enabled: bool | None = True, replace: bool | None = False)
    +create(enabled: bool | None = True, replace: bool | None = False)

    Create a task that a Select AI agent can include in its reasoning process

    Parameters:
      -
    • enabled (bool) – Whether the AI Task should be enabled. +

    • enabled (bool) – Whether the AI Task should be enabled. Default value is True.

    • -
    • replace (bool) – Whether the AI Task should be replaced. +

    • replace (bool) – Whether the AI Task should be replaced. Default value is False.

    @@ -830,24 +1158,24 @@

    2.
    -delete(force: bool = False)
    +delete(force: bool = False)

    Delete AI Task from the database

    Parameters:
    -

    force (bool) – Force the deletion. Default value is False.

    +

    force (bool) – Force the deletion. Default value is False.

    -classmethod delete_task(task_name: str, force: bool = False)
    +classmethod delete_task(task_name: str, force: bool = False)

    Class method to delete AI Task from the database

    Parameters:
      -
    • task_name (str) – The name of the AI Task

    • -
    • force (bool) – Force the deletion. Default value is False.

    • +
    • task_name (str) – The name of the AI Task

    • +
    • force (bool) – Force the deletion. Default value is False.

    @@ -867,29 +1195,29 @@

    2.
    -classmethod fetch(task_name: str) → Task
    +classmethod fetch(task_name: str) → Task

    Fetch AI Task attributes from the Database and build a proxy object in the Python layer

    Parameters:
    -

    task_name (str) – The name of the AI Task

    +

    task_name (str) – The name of the AI Task

    Returns:

    select_ai.agent.Task

    Raises:
    -

    select_ai.errors.AgentTaskNotFoundError – If the AI Task is not found

    +

    select_ai.errors.AgentTaskNotFoundError – If the AI Task is not found

    -classmethod list(task_name_pattern: str | None = '.*') → Iterator[Task]
    +classmethod list(task_name_pattern: str | None = '.*') → Iterator[Task]

    List AI Tasks

    Parameters:
    -

    task_name_pattern (str) – Regular expressions can be used +

    task_name_pattern (str) – Regular expressions can be used to specify a pattern. Function REGEXP_LIKE is used to perform the match. Default value is “.*” i.e. match all tasks.

    @@ -901,13 +1229,13 @@

    2.
    -set_attribute(attribute_name: str, attribute_value: Any)
    +set_attribute(attribute_name: str, attribute_value: Any)

    Set a single AI Task attribute specified using name and value

    Parameters:
      -
    • attribute_name (str) – The name of the AI Task attribute

    • -
    • attribute_value (str) – The value of the AI Task attribute

    • +
    • attribute_name (str) – The name of the AI Task attribute

    • +
    • attribute_value (str) – The value of the AI Task attribute

    @@ -941,19 +1269,19 @@

    2.1. Create Taskimport select_ai.agent from select_ai.agent import Task, TaskAttributes -user = os.getenv("SELECT_AI_USER") -password = os.getenv("SELECT_AI_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +user = os.getenv("SELECT_AI_USER") +password = os.getenv("SELECT_AI_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") select_ai.connect(user=user, password=password, dsn=dsn) task = Task( - task_name="ANALYZE_MOVIE_TASK", - description="Search for movies in the database", + task_name="ANALYZE_MOVIE_TASK", + description="Search for movies in the database", attributes=TaskAttributes( - instruction="Help the user with their request about movies. " - "User question: {query}. " - "You can use SQL tool to search the data from database", - tools=["MOVIE_SQL_TOOL"], + instruction="Help the user with their request about movies. " + "User question: {query}. " + "You can use SQL tool to search the data from database", + tools=["MOVIE_SQL_TOOL"], enable_human_tool=False, ), ) @@ -965,10 +1293,10 @@

    2.1. Create Taskoutput:

    ANALYZE_MOVIE_TASK
     
    -TaskAttributes(instruction='Help the user with their request about movies. '
    -                           'User question: {query}. You can use SQL tool to '
    -                           'search the data from database',
    -               tools=['MOVIE_SQL_TOOL'],
    +TaskAttributes(instruction='Help the user with their request about movies. '
    +                           'User question: {query}. You can use SQL tool to '
    +                           'search the data from database',
    +               tools=['MOVIE_SQL_TOOL'],
                    input=None,
                    enable_human_tool=False)
     
    @@ -981,9 +1309,9 @@

    2.2. List Tasksimport select_ai import select_ai.agent -user = os.getenv("SELECT_AI_USER") -password = os.getenv("SELECT_AI_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +user = os.getenv("SELECT_AI_USER") +password = os.getenv("SELECT_AI_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") select_ai.connect(user=user, password=password, dsn=dsn) @@ -1009,45 +1337,82 @@

    3.
    -class select_ai.agent.AgentAttributes(profile_name: str, role: str, enable_human_tool: bool | None = True)
    +class select_ai.agent.AgentAttributes(profile_name: str, role: str, enable_human_tool: bool | None = True, supervisor: bool | None = None)

    AI Agent Attributes

    Parameters:
      -
    • profile_name (str) – Name of the AI Profile which agent will +

    • profile_name (str) – Name of the AI Profile which agent will use to send request to LLM

    • -
    • role (str) – Agent’s role also sent to LLM

    • -
    • enable_human_tool (bool) – Enable human tool support. Agent +

    • role (str) – Agent’s role also sent to LLM

    • +
    • enable_human_tool (bool) – Enable human tool support. Agent will ask question to the user for any clarification

    • +
    • supervisor (bool) – Whether the agent supervises other agents in a +supervised team.

    +
    +
    +dict(exclude_null=True)
    +
    + +
    +
    +enable_human_tool: bool | None = True
    +
    + +
    +
    +json(exclude_null=True)
    +
    + +
    +
    +classmethod keys()
    +
    + +
    +
    +profile_name: str
    +
    + +
    +
    +role: str
    +
    + +
    +
    +supervisor: bool | None = None
    +
    +

    -class select_ai.agent.Agent(agent_name: str | None = None, description: str | None = None, attributes: AgentAttributes | None = None)
    +class select_ai.agent.Agent(agent_name: str | None = None, description: str | None = None, attributes: AgentAttributes | None = None)

    select_ai.agent.Agent class lets you create, delete, enable, disable and list AI agents

    Parameters:
      -
    • agent_name (str) – The name of the AI Agent

    • -
    • description (str) – Optional description of the AI agent

    • +
    • agent_name (str) – The name of the AI Agent

    • +
    • description (str) – Optional description of the AI agent

    • attributes (select_ai.agent.AgentAttributes) – AI agent attributes

    -create(enabled: bool | None = True, replace: bool | None = False)
    +create(enabled: bool | None = True, replace: bool | None = False)

    Register a new AI Agent within the Select AI framework

    Parameters:
      -
    • enabled (bool) – Whether the AI Agent should be enabled. +

    • enabled (bool) – Whether the AI Agent should be enabled. Default value is True.

    • -
    • replace (bool) – Whether the AI Agent should be replaced. +

    • replace (bool) – Whether the AI Agent should be replaced. Default value is False.

    @@ -1056,24 +1421,24 @@

    3.
    -delete(force: bool | None = False)
    +delete(force: bool | None = False)

    Delete AI Agent from the database

    Parameters:
    -

    force (bool) – Force the deletion. Default value is False.

    +

    force (bool) – Force the deletion. Default value is False.

    -classmethod delete_agent(agent_name: str, force: bool | None = False)
    +classmethod delete_agent(agent_name: str, force: bool | None = False)

    Class method to delete AI Agent from the database

    Parameters:
      -
    • agent_name (str) – The name of the AI Agent

    • -
    • force (bool) – Force the deletion. Default value is False.

    • +
    • agent_name (str) – The name of the AI Agent

    • +
    • force (bool) – Force the deletion. Default value is False.

    @@ -1093,29 +1458,29 @@

    3.
    -classmethod fetch(agent_name: str) → Agent
    +classmethod fetch(agent_name: str) → Agent

    Fetch AI Agent attributes from the Database and build a proxy object in the Python layer

    Parameters:
    -

    agent_name (str) – The name of the AI Agent

    +

    agent_name (str) – The name of the AI Agent

    Returns:

    select_ai.agent.Agent

    Raises:
    -

    select_ai.errors.AgentNotFoundError – If the AI Agent is not found

    +

    select_ai.errors.AgentNotFoundError – If the AI Agent is not found

    -classmethod list(agent_name_pattern: str | None = '.*') → Iterator[Agent]
    +classmethod list(agent_name_pattern: str | None = '.*') → Iterator[Agent]

    List AI agents matching a pattern

    Parameters:
    -

    agent_name_pattern (str) – Regular expressions can be used +

    agent_name_pattern (str) – Regular expressions can be used to specify a pattern. Function REGEXP_LIKE is used to perform the match. Default value is “.*” i.e. match all agent names.

    @@ -1127,13 +1492,13 @@

    3.
    -set_attribute(attribute_name: str, attribute_value: Any) → None
    +set_attribute(attribute_name: str, attribute_value: Any) → None

    Set a single AI Agent attribute specified using name and value

    -set_attributes(attributes: AgentAttributes) → None
    +set_attributes(attributes: AgentAttributes) → None

    Set AI Agent attributes

    Parameters:
    @@ -1155,29 +1520,29 @@

    3.1. Create AgentAgentAttributes, ) -user = os.getenv("SELECT_AI_USER") -password = os.getenv("SELECT_AI_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +user = os.getenv("SELECT_AI_USER") +password = os.getenv("SELECT_AI_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") select_ai.connect(user=user, password=password, dsn=dsn) agent_attributes = AgentAttributes( - profile_name="LLAMA_4_MAVERICK", - role="You are an AI Movie Analyst. " - "Your can help answer a variety of questions related to movies. ", + profile_name="LLAMA_4_MAVERICK", + role="You are an AI Movie Analyst. " + "Your can help answer a variety of questions related to movies. ", enable_human_tool=False, ) agent = Agent( - agent_name="MOVIE_ANALYST", + agent_name="MOVIE_ANALYST", attributes=agent_attributes, ) agent.create(enabled=True, replace=True) -print("Created Agent:", agent) +print("Created Agent:", agent)

    output:

    Created Agent: Agent(agent_name=MOVIE_ANALYST,
    -attributes=AgentAttributes(profile_name='LLAMA_4_MAVERICK',
    -role='You are an AI Movie Analyst.
    -Your can help answer a variety of questions related to movies. ',
    +attributes=AgentAttributes(profile_name='LLAMA_4_MAVERICK',
    +role='You are an AI Movie Analyst.
    +Your can help answer a variety of questions related to movies. ',
     enable_human_tool=False), description=None)
     
    @@ -1198,16 +1563,16 @@

    4. Team
    attributes = TeamAttributes(
         agents=[
    -        {"name": "MOVIE_ANALYST", "task": "ANALYZE_MOVIE_TASK"},
    -        {"name": "MOVIE_ANALYST", "task": "SUMMARIZE_MOVIE_TASK"},
    +        {"name": "MOVIE_ANALYST", "task": "ANALYZE_MOVIE_TASK"},
    +        {"name": "MOVIE_ANALYST", "task": "SUMMARIZE_MOVIE_TASK"},
         ],
    -    process="sequential",
    +    process="sequential",
     )
     
    -class select_ai.agent.TeamAttributes(agents: List[Mapping], process: str = 'sequential')
    +class select_ai.agent.TeamAttributes(agents: List[Mapping], process: str = 'sequential', supervisor_agent: str | None = None, supervisor_task: str | None = None)

    AI agent team attributes

    Parameters:
    @@ -1215,38 +1580,240 @@

    4. Teamstr) – Execution order of tasks. Currently only “sequential” is supported.

    +
  • supervisor_agent (str) – Name of the agent that supervises the team +when using a supervised execution process.

  • +
  • supervisor_task (str) – Database-generated task associated with the +supervisor agent.

  • +
    +
    +agents: List[Mapping]
    +
    + +
    +
    +dict(exclude_null=True)
    +
    + +
    +
    +json(exclude_null=True)
    +
    + +
    +
    +classmethod keys()
    +
    + +
    +
    +process: str = 'sequential'
    +
    + +
    +
    +supervisor_agent: str | None = None
    +
    + +
    +
    +supervisor_task: str | None = None
    +
    + +
    +

    4.1. 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.

    +
    from select_ai.agent import Agent, AgentAttributes, Team, TeamAttributes
    +
    +# MOVIE_ANALYST and ANALYZE_MOVIE_TASK must already exist.
    +supervisor = Agent(
    +    agent_name="MOVIE_SUPERVISOR",
    +    attributes=AgentAttributes(
    +        profile_name="oci_ai_profile",
    +        role="You supervise and coordinate the team.",
    +        supervisor=True,
    +    ),
    +)
    +
    +supervisor.create()
    +
    +team = Team(
    +    team_name="MOVIE_AGENT_TEAM",
    +    attributes=TeamAttributes(
    +        agents=[
    +            {"name": "MOVIE_ANALYST", "task": "ANALYZE_MOVIE_TASK"}
    +        ],
    +        process="sequential",
    +        supervisor_agent=supervisor.agent_name,
    +    ),
    +)
    +team.create()
    +
    +
    +

    Agent and team constructors build Python objects; create() persists them +in Oracle Database. The example uses new names and raises an error if those +objects already exist. Use replace=True on create() only when you +intend to replace existing definitions.

    +

    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.

    +

    Set the common database connection variables and SELECT_AI_PROFILE_NAME +(default oci_ai_profile) to an existing profile. Run +python samples/agent/team_supervisor_inspect.py from the repository root. +The script removes the objects and conversation it creates during cleanup.

    +

    First, configure the connection and define the task and worker agent:

    +
    import os
    +import uuid
    +
    +import select_ai
    +from select_ai.agent import (
    +    Agent,
    +    AgentAttributes,
    +    Task,
    +    TaskAttributes,
    +    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", "oci_ai_profile")
    +suffix = uuid.uuid4().hex.upper()
    +
    +select_ai.connect(user=user, password=password, dsn=dsn)
    +
    +task = Task(
    +    task_name=f"SAMPLE_TASK_{suffix}",
    +    attributes=TaskAttributes(
    +        instruction="Answer the user's question: {query}",
    +        tools=[],
    +        enable_human_tool=False,
    +    ),
    +)
    +worker = Agent(
    +    agent_name=f"SAMPLE_WORKER_{suffix}",
    +    attributes=AgentAttributes(
    +        profile_name=profile_name,
    +        role="You answer user questions.",
    +        enable_human_tool=False,
    +    ),
    +)
    +
    +
    +

    Append the supervisor, team, and conversation definitions:

    +
    supervisor = Agent(
    +    agent_name=f"SAMPLE_SUPERVISOR_{suffix}",
    +    attributes=AgentAttributes(
    +        profile_name=profile_name,
    +        role="You supervise and coordinate the team.",
    +        enable_human_tool=False,
    +        supervisor=True,
    +    ),
    +)
    +team = Team(
    +    team_name=f"SAMPLE_TEAM_{suffix}",
    +    attributes=TeamAttributes(
    +        agents=[{"name": worker.agent_name, "task": task.task_name}],
    +        process="sequential",
    +        supervisor_agent=supervisor.agent_name,
    +    ),
    +)
    +
    +conversation = Conversation(
    +    attributes=ConversationAttributes(
    +        title="Supervised team sample",
    +        description="Conversation for the supervised team sample",
    +    )
    +)
    +
    +
    +
    +

    Finally, create the objects, run the team, inspect its metadata, and clean up:

    +
    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:
    +    if conversation_created:
    +        conversation.delete(force=True)
    +    for obj in reversed(created_objects):
    +        obj.delete(force=True)
    +
    +
    +

    Example output (generated names, model responses, and JSON metadata vary):

    +

    output:

    +
    Team response: <answer from the supervised team>
    +Supervisor agent: SAMPLE_SUPERVISOR_<generated suffix>
    +Supervisor task: <generated supervisor task>
    +Team description: <JSON team metadata>
    +Team tools: <JSON tool metadata>
    +
    +
    -class select_ai.agent.Team(team_name: str, attributes: TeamAttributes | None = None, description: str | None = None)
    +class select_ai.agent.Team(team_name: str, attributes: TeamAttributes | None = None, description: str | None = None)

    A Team of AI agents work together to accomplish tasks select_ai.agent.Team class lets you create, delete, enable, disable and list AI Tasks.

    Parameters:
      -
    • team_name (str) – The name of the AI team

    • -
    • description (str) – Optional description of the AI team

    • +
    • team_name (str) – The name of the AI team

    • +
    • description (str) – Optional description of the AI team

    • attributes (select_ai.agent.TeamAttributes) – AI team attributes

    -create(enabled: bool | None = True, replace: bool | None = False)
    +create(enabled: bool | None = True, replace: bool | None = False)

    Create a team of AI agents that work together to accomplish tasks.

    Parameters:
      -
    • enabled (bool) – Whether the AI agent team should be enabled. +

    • enabled (bool) – Whether the AI agent team should be enabled. Default value is True.

    • -
    • replace (bool) – Whether the AI agent team should be replaced. +

    • replace (bool) – Whether the AI agent team should be replaced. Default value is False.

    @@ -1255,29 +1822,35 @@

    4. Team
    -delete(force: bool | None = False)
    +delete(force: bool | None = False)

    Delete an AI agent team from the database

    Parameters:
    -

    force (bool) – Force the deletion. Default value is False.

    +

    force (bool) – Force the deletion. Default value is False.

    -classmethod delete_team(team_name: str, force: bool | None = False)
    +classmethod delete_team(team_name: str, force: bool | None = False)

    Class method to delete an AI agent team from the database

    Parameters:
      -
    • team_name (str) – The name of the AI team

    • -
    • force (bool) – Force the deletion. Default value is False.

    • +
    • team_name (str) – The name of the AI team

    • +
    • force (bool) – Force the deletion. Default value is False.

    +
    +
    +describe_team() → str | None
    +

    Return this team’s JSON metadata and aggregated skills.

    +
    +
    disable()
    @@ -1292,7 +1865,7 @@

    4. Team
    -export(object_storage_credential_name: str | None = None, location: str | None = None, params: str | Mapping | None = None) → str | None
    +export(object_storage_credential_name: str | None = None, location: str | None = None, params: str | Mapping | None = None) → str | None

    Export this AI agent team specification.

    If object storage details are provided, the specification is written to the given location and None is returned. Otherwise, the specification is @@ -1300,13 +1873,13 @@

    4. Team
    Parameters:
      -
    • object_storage_credential_name (str) – Optional credential name +

    • object_storage_credential_name (str) – Optional credential name used to write the exported specification to object storage. Must be specified together with location.

    • -
    • location (str) – Optional object storage URI where the exported +

    • location (str) – Optional object storage URI where the exported specification should be written. Must be specified together with object_storage_credential_name.

    • -
    • params (str or Mapping) – Optional export parameters. May be a JSON string or a +

    • params (str or Mapping) – Optional export parameters. May be a JSON string or a Python mapping.

    @@ -1315,14 +1888,14 @@

    4. TeamReturn type: -

    str or None

    +

    str or None

    -classmethod export_team(team_name: str, object_storage_credential_name: str | None = None, location: str | None = None, params: str | Mapping | None = None) → str | None
    +classmethod export_team(team_name: str, object_storage_credential_name: str | None = None, location: str | None = None, params: str | Mapping | None = None) → str | None

    Export an AI agent team specification.

    If object storage details are provided, the specification is written to the given location and None is returned. Otherwise, the specification is @@ -1330,14 +1903,14 @@

    4. Team
    Parameters:
      -
    • team_name (str) – Name of the AI agent team to export.

    • -
    • object_storage_credential_name (str) – Optional credential name +

    • team_name (str) – Name of the AI agent team to export.

    • +
    • object_storage_credential_name (str) – Optional credential name used to write the exported specification to object storage. Must be specified together with location.

    • -
    • location (str) – Optional object storage URI where the exported +

    • location (str) – Optional object storage URI where the exported specification should be written. Must be specified together with object_storage_credential_name.

    • -
    • params (str or Mapping) – Optional export parameters. May be a JSON string or a +

    • params (str or Mapping) – Optional export parameters. May be a JSON string or a Python mapping.

    @@ -1346,53 +1919,59 @@

    4. TeamReturn type: -

    str or None

    +

    str or None

    -classmethod fetch(team_name: str) → Team
    +classmethod fetch(team_name: str) → Team

    Fetch AI Team attributes from the Database and build a proxy object in the Python layer

    Parameters:
    -

    team_name (str) – The name of the AI Team

    +

    team_name (str) – The name of the AI Team

    Returns:

    select_ai.agent.Team

    Raises:
    -

    select_ai.errors.AgentTeamNotFoundError – If the AI Team is not found

    +

    select_ai.errors.AgentTeamNotFoundError – If the AI Team is not found

    +
    +
    +grant_access(user_or_role_name: str) → None
    +

    Grant a database user or role access to this agent team.

    +
    +
    -classmethod import_team(profile_name: str, team_name: str | None = None, specification: str | Mapping | None = None, object_storage_credential_name: str | None = None, location: str | None = None, force: bool | None = False, params: str | Mapping | None = None) → None
    +classmethod import_team(profile_name: str, team_name: str | None = None, specification: str | Mapping | None = None, object_storage_credential_name: str | None = None, location: str | None = None, force: bool | None = False, params: str | Mapping | None = None) → None

    Import an AI agent team specification and create the associated team, agents, tasks, and tools in the database.

    Parameters:
      -
    • profile_name (str) – Name of the Select AI profile to use for the +

    • profile_name (str) – Name of the Select AI profile to use for the imported team and agents in the target database.

    • -
    • team_name (str) – Optional name for the imported team. If omitted, +

    • team_name (str) – Optional name for the imported team. If omitted, the team name from the specification is used.

    • -
    • specification (str or Mapping) – Team specification to import. May be a JSON +

    • specification (str or Mapping) – Team specification to import. May be a JSON string or a Python mapping. Omit this when importing from object storage.

    • -
    • object_storage_credential_name (str) – Optional credential name +

    • object_storage_credential_name (str) – Optional credential name used to read the specification from object storage. Must be specified together with location.

    • -
    • location (str) – Optional object storage URI of the specification +

    • location (str) – Optional object storage URI of the specification to import. Must be specified together with object_storage_credential_name.

    • -
    • force (bool) – Whether to replace conflicting database objects +

    • force (bool) – Whether to replace conflicting database objects during import. Default value is False.

    • -
    • params (str or Mapping) – Optional import parameters. May be a JSON string or a +

    • params (str or Mapping) – Optional import parameters. May be a JSON string or a Python mapping.

    @@ -1401,11 +1980,11 @@

    4. Team
    -classmethod list(team_name_pattern: str | None = '.*') → Iterator[Team]
    +classmethod list(team_name_pattern: str | None = '.*') → Iterator[Team]

    List AI Agent Teams

    Parameters:
    -

    team_name_pattern (str) – Regular expressions can be used +

    team_name_pattern (str) – Regular expressions can be used to specify a pattern. Function REGEXP_LIKE is used to perform the match. Default value is “.*” i.e. match all teams.

    @@ -1415,9 +1994,21 @@

    4. Team +
    +list_tools() → str | None
    +

    Return JSON metadata for the tools available to this team.

    +

    + +
    +
    +revoke_access(user_or_role_name: str) → None
    +

    Revoke a database user or role’s access to this agent team.

    +
    +
    -run(prompt: str = None, params: Mapping = None)
    +run(prompt: str = None, params: Mapping = None)

    Start a new AI agent team or resume a paused one that is waiting for human input. If you provide an existing process ID and the associated team process is in the WAITING_FOR_HUMAN state, the @@ -1426,11 +2017,11 @@

    4. Team
    Parameters:
      -
    • prompt (str) – Optional prompt for the user. If the task is +

    • prompt (str) – Optional prompt for the user. If the task is in the RUNNING state, the input acts as a placeholder for the {query} in the task instruction. If the task is in the WAITING_FOR_HUMAN state, the input serves as the human response.

    • -
    • params (Mapping[str, str]) – Optional parameters for the task. +

    • params (Mapping[str, str]) – Optional parameters for the task. Supported keys include conversation_id, which identifies the conversation session associated with the agent team, and variables, which provides additional key-value input to the agent @@ -1442,30 +2033,43 @@

      4. Team
      -set_attribute(attribute_name: str, attribute_value: Any) → None
      +set_attribute(attribute_name: str, attribute_value: Any) → None

      Set the attribute of the AI Agent team specified by attribute_name and attribute_value.

    -set_attributes(attributes: TeamAttributes) → None
    +set_attributes(attributes: TeamAttributes) → None

    Set the attributes of the AI Agent team

    +
    +
    +

    4.2. Share a team

    +

    Grant or revoke access for a database user or role with Team methods. Run +these methods as the team owner:

    +
    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.

    +
    -

    4.1. Run Team

    +

    4.3. Run Team

    Team.run(...) starts the team workflow. The prompt argument is passed to the task and can be referenced by task instructions using {query}. params can include conversation_id to associate multiple runs with the same conversation and variables to pass additional key-value inputs.

    @@ -1528,7 +2132,7 @@

    4.1. Run Team

    -

    4.2. Export and Import Team

    +

    4.4. Export and Import Team

    Select AI agent teams can be exported into a portable specification and imported into the same database, a different database, or another Select AI service. The specification describes the team composition and the associated @@ -1559,18 +2163,18 @@

    4.2. Export and Import TeamTeamAttributes, ) -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") +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") select_ai.connect(user=user, password=password, dsn=dsn) task = Task( - task_name="EXPORT_IMPORT_MOVIE_TASK", - description="Task used by the team export/import sample", + task_name="EXPORT_IMPORT_MOVIE_TASK", + description="Task used by the team export/import sample", attributes=TaskAttributes( - instruction="Help the user with movie questions. Question: {query}", + instruction="Help the user with movie questions. Question: {query}", tools=[], enable_human_tool=False, ), @@ -1578,64 +2182,64 @@

    4.2. Export and Import Teamtask.create(replace=True) agent = Agent( - agent_name="EXPORT_IMPORT_MOVIE_ANALYST", - description="Agent used by the team export/import sample", + agent_name="EXPORT_IMPORT_MOVIE_ANALYST", + description="Agent used by the team export/import sample", attributes=AgentAttributes( profile_name=profile_name, - role="You are an AI Movie Analyst.", + role="You are an AI Movie Analyst.", enable_human_tool=False, ), ) agent.create(enabled=True, replace=True) source_team = Team( - team_name="EXPORT_IMPORT_MOVIE_TEAM", + team_name="EXPORT_IMPORT_MOVIE_TEAM", attributes=TeamAttributes( agents=[ { - "name": agent.agent_name, - "task": task.task_name, + "name": agent.agent_name, + "task": task.task_name, } ], - process="sequential", + process="sequential", ), ) source_team.create(enabled=True, replace=True) specification = json.loads(source_team.export()) -print("Exported specification:") +print("Exported specification:") print(json.dumps(specification, indent=2)) -specification["name"] = "IMPORTED_MOVIE_ANALYST" -specification["task"]["task_name"] = "IMPORTED_ANALYZE_MOVIE_TASK" +specification["name"] = "IMPORTED_MOVIE_ANALYST" +specification["task"]["task_name"] = "IMPORTED_ANALYZE_MOVIE_TASK" Team.import_team( profile_name=profile_name, - team_name="IMPORTED_MOVIE_AGENT_TEAM", + team_name="IMPORTED_MOVIE_AGENT_TEAM", specification=specification, force=True, ) -team = Team.fetch("IMPORTED_MOVIE_AGENT_TEAM") -print("Imported team:", team) +team = Team.fetch("IMPORTED_MOVIE_AGENT_TEAM") +print("Imported team:", team)

    output:

    +
    +

    4.5. 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.

    +
    team = Team.fetch("MOVIE_AGENT_TEAM")
    +print(team.describe_team())
    +print(team.list_tools())
    +
    +
    +

    The supervised-team sample also demonstrates both inspection methods.

    +
    -

    4.3. Lifecycle helpers

    +

    4.6. Lifecycle helpers

    All agent object types support list, fetch, enable, disable, and delete operations.

    for tool in select_ai.agent.Tool.list():
         print(tool.tool_name)
     
    -task = select_ai.agent.Task.fetch("ANALYZE_MOVIE_TASK")
    -agent = select_ai.agent.Agent.fetch("MOVIE_ANALYST")
    -team = select_ai.agent.Team.fetch("MOVIE_AGENT_TEAM")
    +task = select_ai.agent.Task.fetch("ANALYZE_MOVIE_TASK")
    +agent = select_ai.agent.Agent.fetch("MOVIE_ANALYST")
    +team = select_ai.agent.Team.fetch("MOVIE_AGENT_TEAM")
     
     team.disable()
     team.enable()
     team.delete(force=True)
     
    +
    +

    4.6.1. 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.

    +
    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.

    +
    import os
    +import uuid
    +
    +import select_ai
    +from select_ai.agent import Task, TaskAttributes, get_definition
    +
    +user = os.getenv("SELECT_AI_USER")
    +password = os.getenv("SELECT_AI_PASSWORD")
    +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING")
    +
    +select_ai.connect(user=user, password=password, dsn=dsn)
    +
    +task = Task(
    +    task_name=f"SAMPLE_DEFINITION_TASK_{uuid.uuid4().hex.upper()}",
    +    attributes=TaskAttributes(
    +        instruction="Answer the user's question: {query}", tools=[]
    +    ),
    +)
    +task.create(replace=True)
    +
    +try:
    +    print(get_definition("TASK", task.task_name))
    +finally:
    +    task.delete(force=True)
    +
    +
    +

    The task name and exact PL/SQL formatting vary between runs and database +versions. Example output (generated IDs and model responses vary):

    +

    output:

    +
    BEGIN
    +  DBMS_CLOUD_AI_AGENT.CREATE_TASK(...);
    +END;
    +/
    +
    +
    +
    +
    +

    5. Agent history

    +

    TeamHistory, TaskHistory, and ToolHistory provide typed, +read-only access to the current user’s Select AI Agent history views. They +query only USER_AI_AGENT_TEAM_HISTORY, USER_AI_AGENT_TASK_HISTORY, +and USER_AI_AGENT_TOOL_HISTORY respectively. Results are yielded newest +first. Tool input and output values are decoded to Python objects when +they contain valid JSON; other CLOB payloads are returned as strings.

    +
    from select_ai.agent import TaskHistory, TeamHistory, ToolHistory
    +
    +for run in TeamHistory.list(team_name="MOVIE_AGENT_TEAM", limit=10):
    +    print(run.team_exec_id, run.state)
    +
    +for run in TaskHistory.list(team_exec_id="<team execution id>"):
    +    print(run.task_name, run.result)
    +
    +for call in ToolHistory.list(tool_name="MOVIE_SQL_TOOL", limit=20):
    +    print(call.input, call.output)
    +
    +
    +

    The sample retrieves a team’s latest execution and uses its team_exec_id +to retrieve the associated task and tool history.

    +
    +
    +class select_ai.agent.TeamHistory
    +

    Read runs from C##CLOUD$SERVICE.USER_AI_AGENT_TEAM_HISTORY.

    +
    +
    +classmethod list(team_name: str | None = None, team_exec_id: str | None = None, limit: int | None = None) → Iterator[TeamHistoryEvent]
    +

    Yield team runs ordered from newest to oldest.

    +
    + +
    + +
    +
    +class select_ai.agent.TaskHistory
    +

    Read task runs from C##CLOUD$SERVICE.USER_AI_AGENT_TASK_HISTORY.

    +
    +
    +classmethod list(team_name: str | None = None, task_name: str | None = None, agent_name: str | None = None, team_exec_id: str | None = None, limit: int | None = None) → Iterator[TaskHistoryEvent]
    +

    Yield task runs ordered from newest to oldest.

    +
    + +
    + +
    +
    +class select_ai.agent.ToolHistory
    +

    Read tool calls from C##CLOUD$SERVICE.USER_AI_AGENT_TOOL_HISTORY.

    +
    +
    +classmethod list(tool_name: str | None = None, task_name: str | None = None, agent_name: str | None = None, team_exec_id: str | None = None, limit: int | None = None) → Iterator[ToolHistoryEvent]
    +

    Yield tool calls ordered from newest to oldest.

    +
    + +
    + +

    The complete sample prints the latest team execution and the task and tool +events associated with that execution:

    +
    from select_ai.agent import TaskHistory, TeamHistory, ToolHistory
    +
    +user = os.getenv("SELECT_AI_USER")
    +password = os.getenv("SELECT_AI_PASSWORD")
    +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING")
    +team_name = "ORACLE_AI_DATABASE_AGENT"
    +
    +select_ai.connect(user=user, password=password, dsn=dsn)
    +
    +# Replace team_name with team_exec_id when the application has recorded it.
    +for team_run in TeamHistory.list(team_name=team_name, limit=1):
    +    pprint(team_run)
    +
    +    # team_exec_id scopes the remaining history to the same execution.
    +    for task_run in TaskHistory.list(team_exec_id=team_run.team_exec_id):
    +        pprint(task_run)
    +
    +    for tool_run in ToolHistory.list(team_exec_id=team_run.team_exec_id):
    +        pprint(tool_run)
    +
    +
    +

    History depends on an existing execution for the configured team. Representative +output is:

    +

    output:

    +
    TeamHistoryEvent(team_exec_id='<team execution id>', team_name='ORACLE_AI_DATABASE_AGENT', state='<state>', ...)
    +TaskHistoryEvent(team_exec_id='<team execution id>', task_name='<task name>', state='<state>', ...)
    +ToolHistoryEvent(invocation_id=<invocation id>, team_exec_id='<team execution id>', tool_name='<tool name>', ...)
    +
    +
    +
    -

    5. AI agent examples

    +

    6. AI agent examples

    -

    5.1. Web Search Agent using OpenAI’s GPT model

    +

    6.1. Web Search Agent using OpenAI’s GPT model

    import os
     
     import select_ai
    @@ -1682,54 +2437,54 @@ 

    5.1. Web Search Agent using OpenAI’s G Tool, ) -OPEN_AI_CREDENTIAL_NAME = "OPENAI_CRED" -OPEN_AI_PROFILE_NAME = "OPENAI_PROFILE" -SELECT_AI_AGENT_NAME = "WEB_SEARCH_AGENT" -SELECT_AI_TASK_NAME = "WEB_SEARCH_TASK" -SELECT_AI_TOOL_NAME = "WEB_SEARCH_TOOL" -SELECT_AI_TEAM_NAME = "WEB_SEARCH_TEAM" +OPEN_AI_CREDENTIAL_NAME = "OPENAI_CRED" +OPEN_AI_PROFILE_NAME = "OPENAI_PROFILE" +SELECT_AI_AGENT_NAME = "WEB_SEARCH_AGENT" +SELECT_AI_TASK_NAME = "WEB_SEARCH_TASK" +SELECT_AI_TOOL_NAME = "WEB_SEARCH_TOOL" +SELECT_AI_TEAM_NAME = "WEB_SEARCH_TEAM" USER_QUERIES = { - "d917b055-e8a1-463a-a489-d4328a7b2210": "What are the key features for the product highlighted at " - "this URL https://www.oracle.com/artificial-intelligence/database-machine-learning", - "c2e3ff20-f56d-40e7-987c-cc72740c75a5": "What is the main topic at this URL https://www.oracle.com/artificial-intelligence/database-machine-learning", - "25e23a25-07b9-4ed7-be11-f7e5e445d286": "What is the main topic at this URL https://openai.com", + "d917b055-e8a1-463a-a489-d4328a7b2210": "What are the key features for the product highlighted at " + "this URL https://www.oracle.com/artificial-intelligence/database-machine-learning", + "c2e3ff20-f56d-40e7-987c-cc72740c75a5": "What is the main topic at this URL https://www.oracle.com/artificial-intelligence/database-machine-learning", + "25e23a25-07b9-4ed7-be11-f7e5e445d286": "What is the main topic at this URL https://openai.com", } # connect -user = os.getenv("SELECT_AI_USER") -password = os.getenv("SELECT_AI_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +user = os.getenv("SELECT_AI_USER") +password = os.getenv("SELECT_AI_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") select_ai.connect(user=user, password=password, dsn=dsn) # Create Open AI credential select_ai.create_credential( credential={ - "credential_name": OPEN_AI_CREDENTIAL_NAME, - "username": "OPENAI", - "password": os.getenv("OPEN_AI_API_KEY"), + "credential_name": OPEN_AI_CREDENTIAL_NAME, + "username": "OPENAI", + "password": os.getenv("OPEN_AI_API_KEY"), }, replace=True, ) -print("Created credential: ", OPEN_AI_CREDENTIAL_NAME) +print("Created credential: ", OPEN_AI_CREDENTIAL_NAME) # # Create Open AI Profile profile = select_ai.Profile( profile_name=OPEN_AI_PROFILE_NAME, attributes=select_ai.ProfileAttributes( credential_name=OPEN_AI_CREDENTIAL_NAME, - provider=select_ai.OpenAIProvider(model="gpt-4.1"), + provider=select_ai.OpenAIProvider(model="gpt-4.1"), ), - description="My Open AI Profile", + description="My Open AI Profile", replace=True, ) -print("Created profile: ", OPEN_AI_PROFILE_NAME) +print("Created profile: ", OPEN_AI_PROFILE_NAME) # Create an AI Agent team team = Team( team_name=SELECT_AI_TEAM_NAME, attributes=TeamAttributes( - agents=[{"name": SELECT_AI_AGENT_NAME, "task": SELECT_AI_TASK_NAME}] + agents=[{"name": SELECT_AI_AGENT_NAME, "task": SELECT_AI_TASK_NAME}] ), ) team.create(replace=True) @@ -1740,8 +2495,8 @@

    5.1. Web Search Agent using OpenAI’s G attributes=AgentAttributes( profile_name=OPEN_AI_PROFILE_NAME, enable_human_tool=False, - role="You are a specialized web search agent that can access web page " - "contents and respond to questions based on its content.", + role="You are a specialized web search agent that can access web page " + "contents and respond to questions based on its content.", ), ) agent.create(replace=True) @@ -1750,7 +2505,7 @@

    5.1. Web Search Agent using OpenAI’s G task = Task( task_name=SELECT_AI_TASK_NAME, attributes=TaskAttributes( - instruction="Answer the user question about the provided URL:{query}", + instruction="Answer the user question about the provided URL:{query}", enable_human_tool=False, tools=[SELECT_AI_TOOL_NAME], ), @@ -1761,16 +2516,14 @@

    5.1. Web Search Agent using OpenAI’s G web_search_tool = Tool.create_websearch_tool( tool_name=SELECT_AI_TOOL_NAME, credential_name=OPEN_AI_CREDENTIAL_NAME, - description="Web Search Tool using OpenAI", + description="Web Search Tool using OpenAI", replace=True, ) -print("Created tool: ", SELECT_AI_TOOL_NAME) +print("Created tool: ", SELECT_AI_TOOL_NAME) # Run the Agent Team for conversation_id, prompt in USER_QUERIES.items(): - response = team.run( - prompt=prompt, params={"conversation_id": conversation_id} - ) + response = team.run(prompt=prompt) print(response)

    @@ -1803,7 +2556,7 @@

    5.1. Web Search Agent using OpenAI’s G scale, improve productivity, and maintain data security and compliance. The main topic at the URL https://www.oracle.com/artificial-intelligence/database-machine-learning -is Oracle's database machine learning capabilities, specifically how Oracle +is Oracle's database machine learning capabilities, specifically how Oracle integrates artificial intelligence and machine learning features directly into its database products. The page highlights how users can leverage these built-in AI and ML tools to analyze data, build predictive models, and enhance @@ -1833,9 +2586,36 @@

    Table of Contents

  • 1. Tool @@ -158,8 +186,8 @@

    Previous topic

    This Page

    @@ -189,12 +217,15 @@

    Navigation

    index
  • - modules |
  • +
  • + next |
  • previous |
  • -
  • Python logo
  • +
  • Python logo
  • Python »
  • @@ -202,7 +233,7 @@

    Navigation

  • - Select AI for Python 1.4.0 documentation » + Select AI for Python 1.5.0 documentation »
  • @@ -239,9 +270,21 @@

    Navigation

    async / await keywords. Use these classes in applications that already use asyncio and select_ai.async_connect() or select_ai.create_pool_async().

    +

    The history API follows the same pattern. AsyncTeamHistory, +AsyncTaskHistory, and AsyncToolHistory query only the current user’s +history views and yield typed events newest first. +See Async agent history for the complete API reference and sample.

    +
    from select_ai.agent import AsyncToolHistory
    +
    +async for call in AsyncToolHistory.list(limit=10):
    +    print(call.tool_name, call.output)
    +
    +
    +

    The async sample retrieves a team’s latest execution and uses its +team_exec_id to retrieve the associated task and tool history.

    The async agent object model mirrors the synchronous agent object model:

    -

    Action to method mapping
    - +
    Sync and async agent APIs
    +@@ -277,8 +320,8 @@

    Navigation

    Tools, tasks, agents, and teams are database objects. Use replace=True when you want to recreate an existing object with the same name, and force=True when cleanup should succeed even if the object does not exist.

    -
    Sync and async agent APIs
    - +
    Select AI Async Agent Tools
    +@@ -355,17 +398,17 @@

    Navigation

    1. AsyncTool

    -class select_ai.agent.AsyncTool(tool_name: str | None = None, description: str | None = None, attributes: ToolAttributes | None = None)
    +class select_ai.agent.AsyncTool(tool_name: str | None = None, description: str | None = None, attributes: ToolAttributes | None = None)
    -async create(enabled: bool | None = True, replace: bool | None = False)
    +async create(enabled: bool | None = True, replace: bool | None = False)

    Create an AI Tool in the database

    Parameters:
      -
    • enabled (Optional[bool]) – Whether the tool should be enabled. +

    • enabled (Optional[bool]) – Whether the tool should be enabled. Default: True

    • -
    • replace (Optional[bool]) – Whether the tool should be replaced. +

    • replace (Optional[bool]) – Whether the tool should be replaced. Default: False

    @@ -374,18 +417,18 @@

    1.
    -async classmethod create_built_in_tool(tool_name: str, tool_params: ToolParams, tool_type: ToolType, description: str | None = None, replace: bool | None = False, instruction: str | None = None) → AsyncTool
    +async classmethod create_built_in_tool(tool_name: str, tool_params: ToolParams, tool_type: ToolType, description: str | None = None, replace: bool | None = False, instruction: str | None = None) → AsyncTool

    Register a built-in tool

    Parameters:
      -
    • tool_name (str) – The name of the tool

    • +
    • tool_name (str) – The name of the tool

    • tool_params (select_ai.agent.ToolParams) – Parameters required by built-in tool

    • -
    • tool_type (select_ai.agent.ToolType) – The built-in tool type

    • -
    • description (str) – Description of the tool

    • -
    • replace (bool) – Whether to replace the existing tool. +

    • tool_type (select_ai.agent.tool.ToolType) – The built-in tool type

    • +
    • description (str) – Description of the tool

    • +
    • replace (bool) – Whether to replace the existing tool. Default value is False

    • -
    • instruction (str) – A clear, concise statement that describes +

    • instruction (str) – A clear, concise statement that describes what the tool should accomplish and how to do it. This text is included in the prompt sent to the LLM.

    @@ -398,21 +441,21 @@

    1.
    -async classmethod create_email_notification_tool(tool_name: str, credential_name: str, recipient: str, sender: str, smtp_host: str, description: str | None, subject: str | None = None, replace: bool = False, instruction: str | None = None) → AsyncTool
    +async classmethod create_email_notification_tool(tool_name: str, credential_name: str, recipient: str, sender: str, smtp_host: str, description: str | None, subject: str | None = None, replace: bool = False, instruction: str | None = None) → AsyncTool

    Register an email notification tool

    Parameters:
      -
    • tool_name (str) – The name of the tool

    • -
    • credential_name (str) – The name of the credential

    • -
    • recipient (str) – The recipient of the email

    • -
    • sender (str) – The sender of the email

    • -
    • smtp_host (str) – The SMTP host of the email server

    • -
    • description (str) – The description of the tool

    • -
    • subject (str) – Subject of the email.

    • -
    • replace (bool) – Whether to replace the existing tool. +

    • tool_name (str) – The name of the tool

    • +
    • credential_name (str) – The name of the credential

    • +
    • recipient (str) – The recipient of the email

    • +
    • sender (str) – The sender of the email

    • +
    • smtp_host (str) – The SMTP host of the email server

    • +
    • description (str) – The description of the tool

    • +
    • subject (str) – Subject of the email.

    • +
    • replace (bool) – Whether to replace the existing tool. Default value is False

    • -
    • instruction (str) – A clear, concise statement that describes +

    • instruction (str) – A clear, concise statement that describes what the tool should accomplish and how to do it. This text is included in the prompt sent to the LLM.

    @@ -425,17 +468,17 @@

    1.
    -async classmethod create_pl_sql_tool(tool_name: str, function: str, description: str | None = None, replace: bool = False, instruction: str | None = None) → AsyncTool
    +async classmethod create_pl_sql_tool(tool_name: str, function: str, description: str | None = None, replace: bool = False, instruction: str | None = None) → AsyncTool

    Create a custom tool to invoke PL/SQL procedure or function

    Parameters:
      -
    • tool_name (str) – The name of the tool

    • -
    • function (str) – The name of the PL/SQL procedure or function

    • -
    • description (str) – The description of the tool

    • -
    • replace (bool) – Whether to replace existing tool. Default value +

    • tool_name (str) – The name of the tool

    • +
    • function (str) – The name of the PL/SQL procedure or function

    • +
    • description (str) – The description of the tool

    • +
    • replace (bool) – Whether to replace existing tool. Default value is False

    • -
    • instruction (str) – A clear, concise statement that describes +

    • instruction (str) – A clear, concise statement that describes what the tool should accomplish and how to do it. This text is included in the prompt sent to the LLM.

    @@ -445,18 +488,18 @@

    1.
    -async classmethod create_rag_tool(tool_name: str, profile_name: str, description: str | None = None, replace: bool = False, instruction: str | None = None) → AsyncTool
    +async classmethod create_rag_tool(tool_name: str, profile_name: str, description: str | None = None, replace: bool = False, instruction: str | None = None) → AsyncTool

    Register a RAG tool, which will use a VectorIndex linked AI Profile

    Parameters:
      -
    • tool_name (str) – The name of the tool

    • -
    • profile_name (str) – The name of the profile to +

    • tool_name (str) – The name of the tool

    • +
    • profile_name (str) – The name of the profile to use for Vector Index based RAG

    • -
    • description (str) – The description of the tool

    • -
    • replace (bool) – Whether to replace existing tool. Default value +

    • description (str) – The description of the tool

    • +
    • replace (bool) – Whether to replace existing tool. Default value is False

    • -
    • instruction (str) – A clear, concise statement that describes +

    • instruction (str) – A clear, concise statement that describes what the tool should accomplish and how to do it. This text is included in the prompt sent to the LLM.

    @@ -466,18 +509,18 @@

    1.
    -async classmethod create_slack_notification_tool(tool_name: str, credential_name: str, channel: str, description: str | None = None, replace: bool = False, instruction: str | None = None) → AsyncTool
    +async classmethod create_slack_notification_tool(tool_name: str, credential_name: str, channel: str, description: str | None = None, replace: bool = False, instruction: str | None = None) → AsyncTool

    Register a Slack notification tool

    Parameters:
      -
    • tool_name (str) – The name of the Slack notification tool

    • -
    • credential_name (str) – The name of the Slack credential

    • -
    • channel (str) – The name of the Slack channel

    • -
    • description (str) – The description of the Slack notification tool

    • -
    • replace (bool) – Whether to replace existing tool. Default value +

    • tool_name (str) – The name of the Slack notification tool

    • +
    • credential_name (str) – The name of the Slack credential

    • +
    • channel (str) – The name of the Slack channel

    • +
    • description (str) – The description of the Slack notification tool

    • +
    • replace (bool) – Whether to replace existing tool. Default value is False

    • -
    • instruction (str) – A clear, concise statement that describes +

    • instruction (str) – A clear, concise statement that describes what the tool should accomplish and how to do it. This text is included in the prompt sent to the LLM.

    @@ -487,18 +530,18 @@

    1.
    -async classmethod create_sql_tool(tool_name: str, profile_name: str, description: str | None = None, replace: bool = False, instruction: str | None = None) → AsyncTool
    +async classmethod create_sql_tool(tool_name: str, profile_name: str, description: str | None = None, replace: bool = False, instruction: str | None = None) → AsyncTool

    Register a SQL tool to perform natural language to SQL translation

    Parameters:
      -
    • tool_name (str) – The name of the tool

    • -
    • profile_name (str) – The name of the profile to use for SQL +

    • tool_name (str) – The name of the tool

    • +
    • profile_name (str) – The name of the profile to use for SQL translation

    • -
    • description (str) – The description of the tool

    • -
    • replace (bool) – Whether to replace existing tool. Default value +

    • description (str) – The description of the tool

    • +
    • replace (bool) – Whether to replace existing tool. Default value is False

    • -
    • instruction (str) – A clear, concise statement that describes +

    • instruction (str) – A clear, concise statement that describes what the tool should accomplish and how to do it. This text is included in the prompt sent to the LLM.

    @@ -508,18 +551,18 @@

    1.
    -async classmethod create_websearch_tool(tool_name: str, credential_name: str, description: str | None, replace: bool = False, instruction: str | None = None) → AsyncTool
    +async classmethod create_websearch_tool(tool_name: str, credential_name: str, description: str | None, replace: bool = False, instruction: str | None = None) → AsyncTool

    Register a built-in websearch tool to search information on the web

    Parameters:
      -
    • tool_name (str) – The name of the tool

    • -
    • credential_name (str) – The name of the credential object +

    • tool_name (str) – The name of the tool

    • +
    • credential_name (str) – The name of the credential object storing OpenAI credentials

    • -
    • description (str) – The description of the tool

    • -
    • replace (bool) – Whether to replace the existing tool

    • -
    • instruction (str) – A clear, concise statement that describes +

    • description (str) – The description of the tool

    • +
    • replace (bool) – Whether to replace the existing tool

    • +
    • instruction (str) – A clear, concise statement that describes what the tool should accomplish and how to do it. This text is included in the prompt sent to the LLM.

    @@ -529,66 +572,72 @@

    1.
    -async delete(force: bool = False)
    +async delete(force: bool = False)

    Delete AI Tool from the database

    Parameters:
    -

    force (bool) – Force the deletion. Default value is False.

    +

    force (bool) – Force the deletion. Default value is False.

    -async classmethod delete_tool(tool_name: str, force: bool = False)
    +async classmethod delete_tool(tool_name: str, force: bool = False)

    Class method ot delete AI Tool from the database

    Parameters:
      -
    • tool_name (str) – The name of the tool

    • -
    • force (bool) – Force the deletion. Default value is False.

    • +
    • tool_name (str) – The name of the tool

    • +
    • force (bool) – Force the deletion. Default value is False.

    +
    +
    +async describe_tool() → str | None
    +

    Asynchronously return this tool’s JSON metadata.

    +
    +
    -async disable()
    +async disable()

    Disable AI Tool

    -async enable()
    +async enable()

    Enable AI Tool

    -async classmethod fetch(tool_name: str) → AsyncTool
    +async classmethod fetch(tool_name: str) → AsyncTool

    Fetch AI Tool attributes from the Database and build a proxy object in the Python layer

    Parameters:
    -

    tool_name (str) – The name of the AI Task

    +

    tool_name (str) – The name of the AI Task

    Returns:

    select_ai.agent.Tool

    Raises:
    -

    select_ai.errors.AgentToolNotFoundError – If the AI Tool is not found

    +

    select_ai.errors.AgentToolNotFoundError – If the AI Tool is not found

    -classmethod list(tool_name_pattern: str = '.*') → AsyncGenerator[AsyncTool, None]
    +classmethod list(tool_name_pattern: str = '.*') → AsyncGenerator[AsyncTool, None]

    List AI Tools

    Parameters:
    -

    tool_name_pattern (str) – Regular expressions can be used +

    tool_name_pattern (str) – Regular expressions can be used to specify a pattern. Function REGEXP_LIKE is used to perform the match. Default value is “.*” i.e. match all tool name.

    @@ -598,16 +647,22 @@

    1. +
    +async run_tool(input: str) → str | None
    +

    Asynchronously run this tool directly and return its result.

    +

    +
    -async set_attribute(attribute_name: str, attribute_value: Any) → None
    +async set_attribute(attribute_name: str, attribute_value: Any) → None

    Set the attribute of the AI Agent tool specified by attribute_name and attribute_value.

    -async set_attributes(attributes: ToolAttributes) → None
    +async set_attributes(attributes: ToolAttributes) → None

    Set the attributes of the AI Agent tool

    @@ -625,9 +680,9 @@

    1.1. Create Toolimport select_ai.agent from select_ai.agent import AsyncTool, ToolAttributes -user = os.getenv("SELECT_AI_USER") -password = os.getenv("SELECT_AI_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +user = os.getenv("SELECT_AI_USER") +password = os.getenv("SELECT_AI_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") async def main(): @@ -635,31 +690,31 @@

    1.1. Create Toolawait select_ai.async_connect(user=user, password=password, dsn=dsn) profile_attributes = select_ai.ProfileAttributes( - credential_name="my_oci_ai_profile_key", + credential_name="my_oci_ai_profile_key", object_list=[ - {"owner": user, "name": "MOVIE"}, - {"owner": user, "name": "ACTOR"}, - {"owner": user, "name": "DIRECTOR"}, + {"owner": user, "name": "MOVIE"}, + {"owner": user, "name": "ACTOR"}, + {"owner": user, "name": "DIRECTOR"}, ], provider=select_ai.OCIGenAIProvider( - region="us-chicago-1", - oci_apiformat="GENERIC", - model="meta.llama-4-maverick-17b-128e-instruct-fp8", + region="us-chicago-1", + oci_apiformat="GENERIC", + model="meta.llama-4-maverick-17b-128e-instruct-fp8", ), ) profile = await select_ai.AsyncProfile( - profile_name="LLAMA_4_MAVERICK", + profile_name="LLAMA_4_MAVERICK", attributes=profile_attributes, - description="MY OCI AI Profile", + description="MY OCI AI Profile", replace=True, ) # Create a tool which uses the OCI AI Profile to # perform natural language SQL translation sql_tool = await AsyncTool.create_sql_tool( - tool_name="MOVIE_SQL_TOOL", - description="My Select AI MOVIE SQL agent tool", - profile_name="LLAMA_4_MAVERICK", + tool_name="MOVIE_SQL_TOOL", + description="My Select AI MOVIE SQL agent tool", + profile_name="LLAMA_4_MAVERICK", replace=True, ) print(sql_tool.tool_name) @@ -678,13 +733,13 @@

    1.1. Create Toolcredential_name=None, endpoint=None, notification_type=None, - profile_name='oci_ai_profile', + profile_name='oci_ai_profile', recipient=None, sender=None, channel=None, smtp_host=None), tool_inputs=None, - tool_type=<ToolType.SQL: 'SQL'>) + tool_type=<ToolType.SQL: 'SQL'>) @@ -695,9 +750,9 @@

    1.2. List Toolsimport select_ai from select_ai.agent import AsyncTool -user = os.getenv("SELECT_AI_USER") -password = os.getenv("SELECT_AI_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +user = os.getenv("SELECT_AI_USER") +password = os.getenv("SELECT_AI_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") async def main(): @@ -716,34 +771,106 @@

    1.2. List Tools +
    +

    1.3. 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.

    +
    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:

    +
    
    +import asyncio
    +import os
    +import uuid
    +
    +import select_ai
    +from select_ai.agent import AsyncTool
    +
    +user = os.getenv("SELECT_AI_USER")
    +password = os.getenv("SELECT_AI_PASSWORD")
    +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING")
    +
    +
    +async def main():
    +    await select_ai.async_connect(user=user, password=password, dsn=dsn)
    +    suffix = uuid.uuid4().hex.upper()
    +    function_name = f"SAMPLE_CALCULATE_AGE_{suffix}"
    +    tool_name = f"SAMPLE_AGE_TOOL_{suffix}"
    +
    +    async with select_ai.async_cursor() as cursor:
    +        await cursor.execute(
    +            f"""
    +            CREATE OR REPLACE FUNCTION {function_name}(p_birth_date DATE)
    +            RETURN NUMBER IS
    +            BEGIN
    +                RETURN TRUNC(MONTHS_BETWEEN(SYSDATE, p_birth_date) / 12);
    +            END;
    +            """
    +        )
    +
    +    tool = await AsyncTool.create_pl_sql_tool(
    +        tool_name=tool_name,
    +        function=function_name,
    +        description="Calculate age from a birth date",
    +    )
    +
    +    try:
    +        print("Tool description:")
    +        print(await tool.describe_tool())
    +        print("Tool result:")
    +        print(await tool.run_tool('{"p_birth_date":"2000-01-01"}'))
    +    finally:
    +        await tool.delete(force=True)
    +        async with select_ai.async_cursor() as cursor:
    +            await cursor.execute(f"DROP FUNCTION {function_name}")
    +
    +
    +asyncio.run(main())
    +
    +
    +

    The generated tool name and calculated age vary between runs. Representative +output is:

    +

    output:

    +
    Tool description:
    +{"tool_name": "SAMPLE_AGE_TOOL_<generated suffix>", ...}
    +Tool result:
    +<calculated age in years>
    +
    +
    +

    2. AsyncTask

    -class select_ai.agent.AsyncTask(task_name: str | None = None, description: str | None = None, attributes: TaskAttributes | None = None)
    +class select_ai.agent.AsyncTask(task_name: str | None = None, description: str | None = None, attributes: TaskAttributes | None = None)

    select_ai.agent.AsyncTask class lets you create, delete, enable, disable and list AI Tasks asynchronously

    Parameters:
      -
    • task_name (str) – The name of the AI task

    • -
    • description (str) – Optional description of the AI task

    • +
    • task_name (str) – The name of the AI task

    • +
    • description (str) – Optional description of the AI task

    • attributes (select_ai.agent.TaskAttributes) – AI task attributes

    -async create(enabled: bool | None = True, replace: bool | None = False)
    +async create(enabled: bool | None = True, replace: bool | None = False)

    Create a task that a Select AI agent can include in its reasoning process

    Parameters:
      -
    • enabled (bool) – Whether the AI Task should be enabled. +

    • enabled (bool) – Whether the AI Task should be enabled. Default value is True.

    • -
    • replace (bool) – Whether the AI Task should be replaced. +

    • replace (bool) – Whether the AI Task should be replaced. Default value is False.

    @@ -752,24 +879,24 @@

    2.
    -async delete(force: bool = False)
    +async delete(force: bool = False)

    Delete AI Task from the database

    Parameters:
    -

    force (bool) – Force the deletion. Default value is False.

    +

    force (bool) – Force the deletion. Default value is False.

    -async classmethod delete_task(task_name: str, force: bool = False)
    +async classmethod delete_task(task_name: str, force: bool = False)

    Class method to delete AI Task from the database

    Parameters:
      -
    • task_name (str) – The name of the AI Task

    • -
    • force (bool) – Force the deletion. Default value is False.

    • +
    • task_name (str) – The name of the AI Task

    • +
    • force (bool) – Force the deletion. Default value is False.

    @@ -777,41 +904,41 @@

    2.
    -async disable()
    +async disable()

    Disable AI Task

    -async enable()
    +async enable()

    Enable AI Task

    -async classmethod fetch(task_name: str) → AsyncTask
    +async classmethod fetch(task_name: str) → AsyncTask

    Fetch AI Task attributes from the Database and build a proxy object in the Python layer

    Parameters:
    -

    task_name (str) – The name of the AI Task

    +

    task_name (str) – The name of the AI Task

    Returns:

    select_ai.agent.Task

    Raises:
    -

    select_ai.errors.AgentTaskNotFoundError – If the AI Task is not found

    +

    select_ai.errors.AgentTaskNotFoundError – If the AI Task is not found

    -classmethod list(task_name_pattern: str | None = '.*') → AsyncGenerator[AsyncTask, None]
    +classmethod list(task_name_pattern: str | None = '.*') → AsyncGenerator[AsyncTask, None]

    List AI Tasks

    Parameters:
    -

    task_name_pattern (str) – Regular expressions can be used +

    task_name_pattern (str) – Regular expressions can be used to specify a pattern. Function REGEXP_LIKE is used to perform the match. Default value is “.*” i.e. match all tasks.

    @@ -823,13 +950,13 @@

    2.
    -async set_attribute(attribute_name: str, attribute_value: Any)
    +async set_attribute(attribute_name: str, attribute_value: Any)

    Set a single AI Task attribute specified using name and value

    Parameters:
      -
    • attribute_name (str) – The name of the AI Task attribute

    • -
    • attribute_value (str) – The value of the AI Task attribute

    • +
    • attribute_name (str) – The name of the AI Task attribute

    • +
    • attribute_value (str) – The value of the AI Task attribute

    @@ -837,7 +964,7 @@

    2.
    -async set_attributes(attributes: TaskAttributes)
    +async set_attributes(attributes: TaskAttributes)

    Set AI Task attributes

    Parameters:
    @@ -864,21 +991,21 @@

    2.1. Create Taskimport select_ai.agent from select_ai.agent import AsyncTask, TaskAttributes -user = os.getenv("SELECT_AI_USER") -password = os.getenv("SELECT_AI_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +user = os.getenv("SELECT_AI_USER") +password = os.getenv("SELECT_AI_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") async def main(): await select_ai.async_connect(user=user, password=password, dsn=dsn) task = AsyncTask( - task_name="ANALYZE_MOVIE_TASK", - description="Search for movies in the database", + task_name="ANALYZE_MOVIE_TASK", + description="Search for movies in the database", attributes=TaskAttributes( - instruction="Help the user with their request about movies. " - "User question: {query}. " - "You can use SQL tool to search the data from database", - tools=["MOVIE_SQL_TOOL"], + instruction="Help the user with their request about movies. " + "User question: {query}. " + "You can use SQL tool to search the data from database", + tools=["MOVIE_SQL_TOOL"], enable_human_tool=False, ), ) @@ -892,10 +1019,10 @@

    2.1. Create Task

    output:

    ANALYZE_MOVIE_TASK
    -TaskAttributes(instruction='Help the user with their request about movies. '
    -                           'User question: {query}. You can use SQL tool to '
    -                           'search the data from database',
    -               tools=['MOVIE_SQL_TOOL'],
    +TaskAttributes(instruction='Help the user with their request about movies. '
    +                           'User question: {query}. You can use SQL tool to '
    +                           'search the data from database',
    +               tools=['MOVIE_SQL_TOOL'],
                    input=None,
                    enable_human_tool=False)
     
    @@ -909,9 +1036,9 @@

    2.2. List Tasksimport select_ai from select_ai.agent import AsyncTask -user = os.getenv("SELECT_AI_USER") -password = os.getenv("SELECT_AI_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +user = os.getenv("SELECT_AI_USER") +password = os.getenv("SELECT_AI_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") async def main(): @@ -934,28 +1061,28 @@

    2.2. List Tasks3. AsyncAgent

    -class select_ai.agent.AsyncAgent(agent_name: str | None = None, description: str | None = None, attributes: AgentAttributes | None = None)
    +class select_ai.agent.AsyncAgent(agent_name: str | None = None, description: str | None = None, attributes: AgentAttributes | None = None)

    select_ai.agent.AsyncAgent class lets you create, delete, enable, disable and list AI agents asynchronously

    Parameters:
      -
    • agent_name (str) – The name of the AI Agent

    • -
    • description (str) – Optional description of the AI agent

    • +
    • agent_name (str) – The name of the AI Agent

    • +
    • description (str) – Optional description of the AI agent

    • attributes (select_ai.agent.AgentAttributes) – AI agent attributes

    -async create(enabled: bool | None = True, replace: bool | None = False)
    +async create(enabled: bool | None = True, replace: bool | None = False)

    Register a new AI Agent within the Select AI framework

    Parameters:
      -
    • enabled (bool) – Whether the AI Agent should be enabled. +

    • enabled (bool) – Whether the AI Agent should be enabled. Default value is True.

    • -
    • replace (bool) – Whether the AI Agent should be replaced. +

    • replace (bool) – Whether the AI Agent should be replaced. Default value is False.

    @@ -964,24 +1091,24 @@

    3.
    -async delete(force: bool | None = False)
    +async delete(force: bool | None = False)

    Delete AI Agent from the database

    Parameters:
    -

    force (bool) – Force the deletion. Default value is False.

    +

    force (bool) – Force the deletion. Default value is False.

    -async classmethod delete_agent(agent_name: str, force: bool | None = False)
    +async classmethod delete_agent(agent_name: str, force: bool | None = False)

    Class method to delete AI Agent from the database

    Parameters:
      -
    • agent_name (str) – The name of the AI Agent

    • -
    • force (bool) – Force the deletion. Default value is False.

    • +
    • agent_name (str) – The name of the AI Agent

    • +
    • force (bool) – Force the deletion. Default value is False.

    @@ -989,41 +1116,41 @@

    3.
    -async disable()
    +async disable()

    Disable AI Agent

    -async enable()
    +async enable()

    Enable AI Agent

    -async classmethod fetch(agent_name: str) → AsyncAgent
    +async classmethod fetch(agent_name: str) → AsyncAgent

    Fetch AI Agent attributes from the Database and build a proxy object in the Python layer

    Parameters:
    -

    agent_name (str) – The name of the AI Agent

    +

    agent_name (str) – The name of the AI Agent

    Returns:

    select_ai.agent.Agent

    Raises:
    -

    select_ai.errors.AgentNotFoundError – If the AI Agent is not found

    +

    select_ai.errors.AgentNotFoundError – If the AI Agent is not found

    -classmethod list(agent_name_pattern: str | None = '.*') → AsyncGenerator[AsyncAgent, None]
    +classmethod list(agent_name_pattern: str | None = '.*') → AsyncGenerator[AsyncAgent, None]

    List AI agents matching a pattern

    Parameters:
    -

    agent_name_pattern (str) – Regular expressions can be used +

    agent_name_pattern (str) – Regular expressions can be used to specify a pattern. Function REGEXP_LIKE is used to perform the match. Default value is “.*” i.e. match all agent names.

    @@ -1035,13 +1162,13 @@

    3.
    -async set_attribute(attribute_name: str, attribute_value: Any) → None
    +async set_attribute(attribute_name: str, attribute_value: Any) → None

    Set a single AI Agent attribute specified using name and value

    -async set_attributes(attributes: AgentAttributes) → None
    +async set_attributes(attributes: AgentAttributes) → None

    Set AI Agent attributes

    Parameters:
    @@ -1066,22 +1193,22 @@

    3.1. Create Agentasync def main(): - user = os.getenv("SELECT_AI_USER") - password = os.getenv("SELECT_AI_PASSWORD") - dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") + user = os.getenv("SELECT_AI_USER") + password = os.getenv("SELECT_AI_PASSWORD") + dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") await select_ai.async_connect(user=user, password=password, dsn=dsn) agent_attributes = AgentAttributes( - profile_name="LLAMA_4_MAVERICK", - role="You are an AI Movie Analyst. " - "Your can help answer a variety of questions related to movies. ", + profile_name="LLAMA_4_MAVERICK", + role="You are an AI Movie Analyst. " + "Your can help answer a variety of questions related to movies. ", enable_human_tool=False, ) agent = AsyncAgent( - agent_name="MOVIE_ANALYST", + agent_name="MOVIE_ANALYST", attributes=agent_attributes, ) await agent.create(enabled=True, replace=True) - print("Created Agent:", agent) + print("Created Agent:", agent) asyncio.run(main()) @@ -1089,9 +1216,9 @@

    3.1. Create Agent

    output:

    Created Agent: Agent(agent_name=MOVIE_ANALYST,
    -attributes=AgentAttributes(profile_name='LLAMA_4_MAVERICK',
    -role='You are an AI Movie Analyst.
    -Your can help answer a variety of questions related to movies. ',
    +attributes=AgentAttributes(profile_name='LLAMA_4_MAVERICK',
    +role='You are an AI Movie Analyst.
    +Your can help answer a variety of questions related to movies. ',
     enable_human_tool=False), description=None)
     
    @@ -1106,9 +1233,9 @@

    3.2. List Agentsasync def main(): - user = os.getenv("SELECT_AI_USER") - password = os.getenv("SELECT_AI_PASSWORD") - dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") + user = os.getenv("SELECT_AI_USER") + password = os.getenv("SELECT_AI_PASSWORD") + dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") await select_ai.async_connect(user=user, password=password, dsn=dsn) async for agent in AsyncAgent.list(): print(agent.agent_name) @@ -1128,29 +1255,29 @@

    3.2. List Agents4. AsyncTeam

    -class select_ai.agent.AsyncTeam(team_name: str, attributes: TeamAttributes | None = None, description: str | None = None)
    +class select_ai.agent.AsyncTeam(team_name: str, attributes: TeamAttributes | None = None, description: str | None = None)

    A Team of AI agents work together to accomplish tasks select_ai.agent.Team class lets you create, delete, enable, disable and list AI Tasks.

    Parameters:
      -
    • team_name (str) – The name of the AI team

    • -
    • description (str) – Optional description of the AI team

    • +
    • team_name (str) – The name of the AI team

    • +
    • description (str) – Optional description of the AI team

    • attributes (select_ai.agent.TeamAttributes) – AI team attributes

    -async create(enabled: bool | None = True, replace: bool | None = False)
    +async create(enabled: bool | None = True, replace: bool | None = False)

    Create a team of AI agents that work together to accomplish tasks.

    Parameters:
      -
    • enabled (bool) – Whether the AI agent team should be enabled. +

    • enabled (bool) – Whether the AI agent team should be enabled. Default value is True.

    • -
    • replace (bool) – Whether the AI agent team should be replaced. +

    • replace (bool) – Whether the AI agent team should be replaced. Default value is False.

    @@ -1159,44 +1286,50 @@

    4. AsyncTeam
    -async delete(force: bool | None = False)
    +async delete(force: bool | None = False)

    Delete an AI agent team from the database

    Parameters:
    -

    force (bool) – Force the deletion. Default value is False.

    +

    force (bool) – Force the deletion. Default value is False.

    -async classmethod delete_team(team_name: str, force: bool | None = False)
    +async classmethod delete_team(team_name: str, force: bool | None = False)

    Class method to delete an AI agent team from the database

    Parameters:
      -
    • team_name (str) – The name of the AI team

    • -
    • force (bool) – Force the deletion. Default value is False.

    • +
    • team_name (str) – The name of the AI team

    • +
    • force (bool) – Force the deletion. Default value is False.

    +
    +
    +async describe_team() → str | None
    +

    Asynchronously return this team’s JSON metadata and skills.

    +
    +
    -async disable()
    +async disable()

    Disable the AI agent team

    -async enable()
    +async enable()

    Enable the AI agent team

    -async export(object_storage_credential_name: str | None = None, location: str | None = None, params: str | Mapping | None = None) → str | None
    +async export(object_storage_credential_name: str | None = None, location: str | None = None, params: str | Mapping | None = None) → str | None

    Export this AI agent team specification.

    If object storage details are provided, the specification is written to the given location and None is returned. Otherwise, the specification is @@ -1204,13 +1337,13 @@

    4. AsyncTeam
    Parameters:
      -
    • object_storage_credential_name (str) – Optional credential name +

    • object_storage_credential_name (str) – Optional credential name used to write the exported specification to object storage. Must be specified together with location.

    • -
    • location (str) – Optional object storage URI where the exported +

    • location (str) – Optional object storage URI where the exported specification should be written. Must be specified together with object_storage_credential_name.

    • -
    • params (str or Mapping) – Optional export parameters. May be a JSON string or a +

    • params (str or Mapping) – Optional export parameters. May be a JSON string or a Python mapping.

    @@ -1219,14 +1352,14 @@

    4. AsyncTeamReturn type: -

    str or None

    +

    str or None

    -async classmethod export_team(team_name: str, object_storage_credential_name: str | None = None, location: str | None = None, params: str | Mapping | None = None) → str | None
    +async classmethod export_team(team_name: str, object_storage_credential_name: str | None = None, location: str | None = None, params: str | Mapping | None = None) → str | None

    Export an AI agent team specification.

    If object storage details are provided, the specification is written to the given location and None is returned. Otherwise, the specification is @@ -1234,14 +1367,14 @@

    4. AsyncTeam
    Parameters:
      -
    • team_name (str) – Name of the AI agent team to export.

    • -
    • object_storage_credential_name (str) – Optional credential name +

    • team_name (str) – Name of the AI agent team to export.

    • +
    • object_storage_credential_name (str) – Optional credential name used to write the exported specification to object storage. Must be specified together with location.

    • -
    • location (str) – Optional object storage URI where the exported +

    • location (str) – Optional object storage URI where the exported specification should be written. Must be specified together with object_storage_credential_name.

    • -
    • params (str or Mapping) – Optional export parameters. May be a JSON string or a +

    • params (str or Mapping) – Optional export parameters. May be a JSON string or a Python mapping.

    @@ -1250,53 +1383,59 @@

    4. AsyncTeamReturn type: -

    str or None

    +

    str or None

    -async classmethod fetch(team_name: str) → AsyncTeam
    +async classmethod fetch(team_name: str) → AsyncTeam

    Fetch AI Team attributes from the Database and build a proxy object in the Python layer

    Parameters:
    -

    team_name (str) – The name of the AI Team

    +

    team_name (str) – The name of the AI Team

    Returns:

    select_ai.agent.Team

    Raises:
    -

    select_ai.errors.AgentTeamNotFoundError – If the AI Team is not found

    +

    select_ai.errors.AgentTeamNotFoundError – If the AI Team is not found

    +
    +
    +async grant_access(user_or_role_name: str) → None
    +

    Asynchronously grant a user or role access to this agent team.

    +
    +
    -async classmethod import_team(profile_name: str, team_name: str | None = None, specification: str | Mapping | None = None, object_storage_credential_name: str | None = None, location: str | None = None, force: bool | None = False, params: str | Mapping | None = None) → None
    +async classmethod import_team(profile_name: str, team_name: str | None = None, specification: str | Mapping | None = None, object_storage_credential_name: str | None = None, location: str | None = None, force: bool | None = False, params: str | Mapping | None = None) → None

    Import an AI agent team specification and create the associated team, agents, tasks, and tools in the database.

    Parameters:
      -
    • profile_name (str) – Name of the Select AI profile to use for the +

    • profile_name (str) – Name of the Select AI profile to use for the imported team and agents in the target database.

    • -
    • team_name (str) – Optional name for the imported team. If omitted, +

    • team_name (str) – Optional name for the imported team. If omitted, the team name from the specification is used.

    • -
    • specification (str or Mapping) – Team specification to import. May be a JSON +

    • specification (str or Mapping) – Team specification to import. May be a JSON string or a Python mapping. Omit this when importing from object storage.

    • -
    • object_storage_credential_name (str) – Optional credential name +

    • object_storage_credential_name (str) – Optional credential name used to read the specification from object storage. Must be specified together with location.

    • -
    • location (str) – Optional object storage URI of the specification +

    • location (str) – Optional object storage URI of the specification to import. Must be specified together with object_storage_credential_name.

    • -
    • force (bool) – Whether to replace conflicting database objects +

    • force (bool) – Whether to replace conflicting database objects during import. Default value is False.

    • -
    • params (str or Mapping) – Optional import parameters. May be a JSON string or a +

    • params (str or Mapping) – Optional import parameters. May be a JSON string or a Python mapping.

    @@ -1305,11 +1444,11 @@

    4. AsyncTeam
    -classmethod list(team_name_pattern: str | None = '.*') → AsyncGenerator[AsyncTeam, None]
    +classmethod list(team_name_pattern: str | None = '.*') → AsyncGenerator[AsyncTeam, None]

    List AI Agent Teams

    Parameters:
    -

    team_name_pattern (str) – Regular expressions can be used +

    team_name_pattern (str) – Regular expressions can be used to specify a pattern. Function REGEXP_LIKE is used to perform the match. Default value is “.*” i.e. match all teams.

    @@ -1319,9 +1458,21 @@

    4. AsyncTeam +
    +async list_tools() → str | None
    +

    Asynchronously return JSON metadata for this team’s tools.

    +

    + +
    +
    +async revoke_access(user_or_role_name: str) → None
    +

    Asynchronously revoke a user or role’s agent team access.

    +
    +
    -async run(prompt: str = None, params: Mapping = None)
    +async run(prompt: str = None, params: Mapping = None)

    Start a new AI agent team or resume a paused one that is waiting for human input. If you provide an existing process ID and the associated team process is in the WAITING_FOR_HUMAN state, the @@ -1330,11 +1481,11 @@

    4. AsyncTeam
    Parameters:
      -
    • prompt (str) – Optional prompt for the user. If the task is +

    • prompt (str) – Optional prompt for the user. If the task is in the RUNNING state, the input acts as a placeholder for the {query} in the task instruction. If the task is in the WAITING_FOR_HUMAN state, the input serves as the human response.

    • -
    • params (Mapping[str, str]) – Optional parameters for the task. +

    • params (Mapping[str, str]) – Optional parameters for the task. Supported keys include conversation_id, which identifies the conversation session associated with the agent team, and variables, which provides additional key-value input to the agent @@ -1346,31 +1497,220 @@

      4. AsyncTeam
      -async set_attribute(attribute_name: str, attribute_value: Any) → None
      +async set_attribute(attribute_name: str, attribute_value: Any) → None

      Set the attribute of the AI Agent team specified by attribute_name and attribute_value.

    -async set_attributes(attributes: TeamAttributes) → None
    +async set_attributes(attributes: TeamAttributes) → None

    Set the attributes of the AI Agent team

    +
    +

    4.1. Share an async team

    +

    Grant or revoke access for a database user or role with AsyncTeam methods. +Run these methods as the team owner:

    +
    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.

    +
    +
    +

    4.2. 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.

    +
    from select_ai.agent import (
    +    AgentAttributes, AsyncAgent, AsyncTeam, TeamAttributes,
    +)
    +
    +# Inside an async function, with an open async connection.
    +# MOVIE_ANALYST and ANALYZE_MOVIE_TASK must already exist.
    +supervisor = AsyncAgent(
    +    agent_name="MOVIE_SUPERVISOR",
    +    attributes=AgentAttributes(
    +        profile_name="oci_ai_profile",
    +        role="You supervise and coordinate the team.",
    +        supervisor=True,
    +    ),
    +)
    +
    +await supervisor.create()
    +
    +team = AsyncTeam(
    +    team_name="MOVIE_AGENT_TEAM",
    +    attributes=TeamAttributes(
    +        agents=[
    +            {"name": "MOVIE_ANALYST", "task": "ANALYZE_MOVIE_TASK"}
    +        ],
    +        process="sequential",
    +        supervisor_agent=supervisor.agent_name,
    +    ),
    +)
    +await team.create()
    +
    +
    +

    Constructors build Python objects. Await create() to persist each object +in Oracle Database. The example expects new supervisor and team names; pass +replace=True to create() only when replacement is intended.

    +

    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.

    +

    Set the common database connection variables and SELECT_AI_PROFILE_NAME +(default oci_ai_profile) to an existing profile. Run +python samples/agent/async/team_supervisor_inspect.py from the repository +root. The script removes its generated objects and conversation during cleanup.

    +

    Configure the imports and connection values:

    +
    import asyncio
    +import os
    +import uuid
    +
    +import select_ai
    +from select_ai.agent import (
    +    AgentAttributes,
    +    AsyncAgent,
    +    AsyncTask,
    +    AsyncTeam,
    +    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", "oci_ai_profile")
    +
    +
    +
    +
    +

    Append the async function and its task, agent, and team definitions:

    +
    async def main():
    +    suffix = uuid.uuid4().hex.upper()
    +    await select_ai.async_connect(user=user, password=password, dsn=dsn)
    +
    +    task = AsyncTask(
    +        task_name=f"SAMPLE_TASK_{suffix}",
    +        attributes=TaskAttributes(
    +            instruction="Answer the user's question: {query}",
    +            tools=[],
    +            enable_human_tool=False,
    +        ),
    +    )
    +    worker = AsyncAgent(
    +        agent_name=f"SAMPLE_WORKER_{suffix}",
    +        attributes=AgentAttributes(
    +            profile_name=profile_name,
    +            role="You answer user questions.",
    +            enable_human_tool=False,
    +        ),
    +    )
    +    supervisor = AsyncAgent(
    +        agent_name=f"SAMPLE_SUPERVISOR_{suffix}",
    +        attributes=AgentAttributes(
    +            profile_name=profile_name,
    +            role="You supervise and coordinate the team.",
    +            enable_human_tool=False,
    +            supervisor=True,
    +        ),
    +    )
    +    team = AsyncTeam(
    +        team_name=f"SAMPLE_TEAM_{suffix}",
    +        attributes=TeamAttributes(
    +            agents=[{"name": worker.agent_name, "task": task.task_name}],
    +            process="sequential",
    +            supervisor_agent=supervisor.agent_name,
    +        ),
    +    )
    +
    +    conversation = AsyncConversation(
    +        attributes=ConversationAttributes(
    +            title="Supervised team sample",
    +            description="Conversation for the supervised team sample",
    +        )
    +    )
    +
    +
    +
    +

    Continue inside main() with creation, execution, inspection, and cleanup. +Keep this block indented as part of the function, then add the entry point:

    +
        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:
    +        if conversation_created:
    +            await conversation.delete(force=True)
    +        for obj in reversed(created_objects):
    +            await obj.delete(force=True)
    +
    +
    +asyncio.run(main())
    +
    +
    +

    Example output (generated names, model responses, and JSON metadata vary):

    +

    output:

    +
    Team response: <answer from the supervised team>
    +Supervisor agent: SAMPLE_SUPERVISOR_<generated suffix>
    +Supervisor task: <generated supervisor task>
    +Team description: <JSON team metadata>
    +Team tools: <JSON tool metadata>
    +
    +
    +
    -

    4.1. Run Team

    +

    4.3. Run Team

    AsyncTeam.run(...) starts the team workflow. The prompt argument is passed to the task and can be referenced by task instructions using {query}. params can include conversation_id to associate multiple runs with the same conversation and variables to pass additional key-value inputs.

    -

    4.2. Export and Import Team

    +

    4.4. Export and Import Team

    Select AI agent teams can be exported into a portable specification and imported into the same database, a different database, or another Select AI service. The specification describes the team composition and the associated @@ -1466,20 +1806,20 @@

    4.2. Export and Import TeamTeamAttributes, ) -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") +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") async def main(): await select_ai.async_connect(user=user, password=password, dsn=dsn) task = AsyncTask( - task_name="EXPORT_IMPORT_MOVIE_TASK", - description="Task used by the team export/import sample", + task_name="EXPORT_IMPORT_MOVIE_TASK", + description="Task used by the team export/import sample", attributes=TaskAttributes( - instruction="Help the user with movie questions. Question: {query}", + instruction="Help the user with movie questions. Question: {query}", tools=[], enable_human_tool=False, ), @@ -1487,46 +1827,46 @@

    4.2. Export and Import Teamawait task.create(replace=True) agent = AsyncAgent( - agent_name="EXPORT_IMPORT_MOVIE_ANALYST", - description="Agent used by the team export/import sample", + agent_name="EXPORT_IMPORT_MOVIE_ANALYST", + description="Agent used by the team export/import sample", attributes=AgentAttributes( profile_name=profile_name, - role="You are an AI Movie Analyst.", + role="You are an AI Movie Analyst.", enable_human_tool=False, ), ) await agent.create(enabled=True, replace=True) source_team = AsyncTeam( - team_name="EXPORT_IMPORT_MOVIE_TEAM", + team_name="EXPORT_IMPORT_MOVIE_TEAM", attributes=TeamAttributes( agents=[ { - "name": agent.agent_name, - "task": task.task_name, + "name": agent.agent_name, + "task": task.task_name, } ], - process="sequential", + process="sequential", ), ) await source_team.create(enabled=True, replace=True) specification = json.loads(await source_team.export()) - print("Exported specification:") + print("Exported specification:") print(json.dumps(specification, indent=2)) - specification["name"] = "IMPORTED_MOVIE_ANALYST" - specification["task"]["task_name"] = "IMPORTED_ANALYZE_MOVIE_TASK" + specification["name"] = "IMPORTED_MOVIE_ANALYST" + specification["task"]["task_name"] = "IMPORTED_ANALYZE_MOVIE_TASK" await AsyncTeam.import_team( profile_name=profile_name, - team_name="IMPORTED_MOVIE_AGENT_TEAM", + team_name="IMPORTED_MOVIE_AGENT_TEAM", specification=specification, force=True, ) - team = await AsyncTeam.fetch("IMPORTED_MOVIE_AGENT_TEAM") - print("Imported team:", team) + team = await AsyncTeam.fetch("IMPORTED_MOVIE_AGENT_TEAM") + print("Imported team:", team) asyncio.run(main()) @@ -1535,19 +1875,19 @@

    4.2. Export and Import Team

    +
    +

    4.5. 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.

    +
    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.

    +
    -

    4.3. Lifecycle helpers

    +

    4.6. Lifecycle helpers

    All async agent object types support list, fetch, enable, disable, and delete operations.

    async for tool in select_ai.agent.AsyncTool.list():
         print(tool.tool_name)
     
    -task = await select_ai.agent.AsyncTask.fetch("ANALYZE_MOVIE_TASK")
    -agent = await select_ai.agent.AsyncAgent.fetch("MOVIE_ANALYST")
    -team = await select_ai.agent.AsyncTeam.fetch("MOVIE_AGENT_TEAM")
    +task = await select_ai.agent.AsyncTask.fetch("ANALYZE_MOVIE_TASK")
    +agent = await select_ai.agent.AsyncAgent.fetch("MOVIE_ANALYST")
    +team = await select_ai.agent.AsyncTeam.fetch("MOVIE_AGENT_TEAM")
     
     await team.disable()
     await team.enable()
     await team.delete(force=True)
     
    +
    +

    4.6.1. 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.

    +
    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.

    +
    import asyncio
    +import os
    +import uuid
    +
    +import select_ai
    +from select_ai.agent import AsyncTask, TaskAttributes, async_get_definition
    +
    +user = os.getenv("SELECT_AI_USER")
    +password = os.getenv("SELECT_AI_PASSWORD")
    +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING")
    +
    +
    +async def main():
    +    await select_ai.async_connect(user=user, password=password, dsn=dsn)
    +    task = AsyncTask(
    +        task_name=f"SAMPLE_DEFINITION_TASK_{uuid.uuid4().hex.upper()}",
    +        attributes=TaskAttributes(
    +            instruction="Answer the user's question: {query}", tools=[]
    +        ),
    +    )
    +    await task.create(replace=True)
    +
    +    try:
    +        print(await async_get_definition("TASK", task.task_name))
    +    finally:
    +        await task.delete(force=True)
    +
    +
    +asyncio.run(main())
    +
    +
    +

    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;
    +/
    +
    +
    +
    +

    +
    +

    5. 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.

    +
    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:

    +
    +
    +class select_ai.agent.AsyncTeamHistory
    +

    Asynchronously read runs from +C##CLOUD$SERVICE.USER_AI_AGENT_TEAM_HISTORY.

    +
    +
    +classmethod list(team_name: str | None = None, team_exec_id: str | None = None, limit: int | None = None) → AsyncGenerator[TeamHistoryEvent, None]
    +

    Yield team runs ordered from newest to oldest.

    +
    + +
    + +
    +
    +class select_ai.agent.AsyncTaskHistory
    +

    Asynchronously read task runs from +C##CLOUD$SERVICE.USER_AI_AGENT_TASK_HISTORY.

    +
    +
    +classmethod list(team_name: str | None = None, task_name: str | None = None, agent_name: str | None = None, team_exec_id: str | None = None, limit: int | None = None) → AsyncGenerator[TaskHistoryEvent, None]
    +

    Yield task runs ordered from newest to oldest.

    +
    + +
    + +
    +
    +class select_ai.agent.AsyncToolHistory
    +

    Asynchronously read tool calls from +C##CLOUD$SERVICE.USER_AI_AGENT_TOOL_HISTORY.

    +
    +
    +classmethod list(tool_name: str | None = None, task_name: str | None = None, agent_name: str | None = None, team_exec_id: str | None = None, limit: int | None = None) → AsyncGenerator[ToolHistoryEvent, None]
    +

    Yield tool calls ordered from newest to oldest.

    +
    + +
    + +
    import select_ai
    +from select_ai.agent import (
    +    AsyncTaskHistory,
    +    AsyncTeamHistory,
    +    AsyncToolHistory,
    +)
    +
    +user = os.getenv("SELECT_AI_USER")
    +password = os.getenv("SELECT_AI_PASSWORD")
    +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING")
    +team_name = "ORACLE_AI_DATABASE_AGENT"
    +
    +
    +async def main():
    +    await select_ai.async_connect(user=user, password=password, dsn=dsn)
    +
    +    # Replace team_name with team_exec_id when the application has recorded it.
    +    async for team_run in AsyncTeamHistory.list(team_name=team_name, limit=1):
    +        pprint(team_run)
    +
    +        # team_exec_id scopes the remaining history to the same execution.
    +        async for task_run in AsyncTaskHistory.list(
    +            team_exec_id=team_run.team_exec_id
    +        ):
    +            pprint(task_run)
    +
    +        async for tool_run in AsyncToolHistory.list(
    +            team_exec_id=team_run.team_exec_id
    +        ):
    +            pprint(tool_run)
    +
    +
    +asyncio.run(main())
    +
    +
    +

    History depends on an existing execution for the configured team. Representative +output is:

    +

    output:

    +
    TeamHistoryEvent(team_exec_id='<team execution id>', team_name='ORACLE_AI_DATABASE_AGENT', state='<state>', ...)
    +TaskHistoryEvent(team_exec_id='<team execution id>', task_name='<task name>', state='<state>', ...)
    +ToolHistoryEvent(invocation_id=<invocation id>, team_exec_id='<team execution id>', tool_name='<tool name>', ...)
    +
    +
    -

    4.4. List Teams

    +

    5.1. List Teams

    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")
    +user = os.getenv("SELECT_AI_USER")
    +password = os.getenv("SELECT_AI_PASSWORD")
    +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING")
     
     
     async def main():
    @@ -1606,9 +2131,9 @@ 

    4.4. List Teams

    -

    5. Async AI agent examples

    +

    6. Async AI agent examples

    -

    5.1. Web Search Agent using OpenAI’s GPT model

    +

    6.1. Web Search Agent using OpenAI’s GPT model

    import asyncio
     import os
     
    @@ -1623,24 +2148,24 @@ 

    5.1. Web Search Agent using OpenAI’s G TeamAttributes, ) -OPEN_AI_CREDENTIAL_NAME = "OPENAI_CRED" -OPEN_AI_PROFILE_NAME = "OPENAI_PROFILE" -SELECT_AI_AGENT_NAME = "WEB_SEARCH_AGENT" -SELECT_AI_TASK_NAME = "WEB_SEARCH_TASK" -SELECT_AI_TOOL_NAME = "WEB_SEARCH_TOOL" -SELECT_AI_TEAM_NAME = "WEB_SEARCH_TEAM" +OPEN_AI_CREDENTIAL_NAME = "OPENAI_CRED" +OPEN_AI_PROFILE_NAME = "OPENAI_PROFILE" +SELECT_AI_AGENT_NAME = "WEB_SEARCH_AGENT" +SELECT_AI_TASK_NAME = "WEB_SEARCH_TASK" +SELECT_AI_TOOL_NAME = "WEB_SEARCH_TOOL" +SELECT_AI_TEAM_NAME = "WEB_SEARCH_TEAM" USER_QUERIES = { - "d917b055-e8a1-463a-a489-d4328a7b2210": "What are the key features for the product highlighted at " - "this URL https://www.oracle.com/artificial-intelligence/database-machine-learning", - "c2e3ff20-f56d-40e7-987c-cc72740c75a5": "What is the main topic at this URL https://www.oracle.com/artificial-intelligence/database-machine-learning", - "25e23a25-07b9-4ed7-be11-f7e5e445d286": "What is the main topic at this URL https://openai.com", + "d917b055-e8a1-463a-a489-d4328a7b2210": "What are the key features for the product highlighted at " + "this URL https://www.oracle.com/artificial-intelligence/database-machine-learning", + "c2e3ff20-f56d-40e7-987c-cc72740c75a5": "What is the main topic at this URL https://www.oracle.com/artificial-intelligence/database-machine-learning", + "25e23a25-07b9-4ed7-be11-f7e5e445d286": "What is the main topic at this URL https://openai.com", } # connect -user = os.getenv("SELECT_AI_USER") -password = os.getenv("SELECT_AI_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +user = os.getenv("SELECT_AI_USER") +password = os.getenv("SELECT_AI_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") async def main(): @@ -1649,32 +2174,32 @@

    5.1. Web Search Agent using OpenAI’s G # Create Open AI credential await select_ai.async_create_credential( credential={ - "credential_name": OPEN_AI_CREDENTIAL_NAME, - "username": "OPENAI", - "password": os.getenv("OPEN_AI_API_KEY"), + "credential_name": OPEN_AI_CREDENTIAL_NAME, + "username": "OPENAI", + "password": os.getenv("OPEN_AI_API_KEY"), }, replace=True, ) - print("Created credential: ", OPEN_AI_CREDENTIAL_NAME) + print("Created credential: ", OPEN_AI_CREDENTIAL_NAME) # # Create Open AI Profile profile = await select_ai.AsyncProfile( profile_name=OPEN_AI_PROFILE_NAME, attributes=select_ai.ProfileAttributes( credential_name=OPEN_AI_CREDENTIAL_NAME, - provider=select_ai.OpenAIProvider(model="gpt-4.1"), + provider=select_ai.OpenAIProvider(model="gpt-4.1"), ), - description="My Open AI Profile", + description="My Open AI Profile", replace=True, ) - print("Created profile: ", OPEN_AI_PROFILE_NAME) + print("Created profile: ", OPEN_AI_PROFILE_NAME) # Create an AI Agent team team = AsyncTeam( team_name=SELECT_AI_TEAM_NAME, attributes=TeamAttributes( agents=[ - {"name": SELECT_AI_AGENT_NAME, "task": SELECT_AI_TASK_NAME} + {"name": SELECT_AI_AGENT_NAME, "task": SELECT_AI_TASK_NAME} ] ), ) @@ -1686,8 +2211,8 @@

    5.1. Web Search Agent using OpenAI’s G attributes=AgentAttributes( profile_name=OPEN_AI_PROFILE_NAME, enable_human_tool=False, - role="You are a specialized web search agent that can access web page " - "contents and respond to questions based on its content.", + role="You are a specialized web search agent that can access web page " + "contents and respond to questions based on its content.", ), ) await agent.create(replace=True) @@ -1696,7 +2221,7 @@

    5.1. Web Search Agent using OpenAI’s G task = AsyncTask( task_name=SELECT_AI_TASK_NAME, attributes=TaskAttributes( - instruction="Answer the user question about the provided URL:{query}", + instruction="Answer the user question about the provided URL:{query}", enable_human_tool=False, tools=[SELECT_AI_TOOL_NAME], ), @@ -1707,15 +2232,15 @@

    5.1. Web Search Agent using OpenAI’s G web_search_tool = await AsyncTool.create_websearch_tool( tool_name=SELECT_AI_TOOL_NAME, credential_name=OPEN_AI_CREDENTIAL_NAME, - description="Web Search Tool using OpenAI", + description="Web Search Tool using OpenAI", replace=True, ) - print("Created tool: ", SELECT_AI_TOOL_NAME) + print("Created tool: ", SELECT_AI_TOOL_NAME) # Run the Agent Team for conversation_id, prompt in USER_QUERIES.items(): response = await team.run( - prompt=prompt, params={"conversation_id": conversation_id} + prompt=prompt, params={"conversation_id": conversation_id} ) print(response) @@ -1752,7 +2277,7 @@

    5.1. Web Search Agent using OpenAI’s G scale, improve productivity, and maintain data security and compliance. The main topic at the URL https://www.oracle.com/artificial-intelligence/database-machine-learning -is Oracle's database machine learning capabilities, specifically how Oracle +is Oracle's database machine learning capabilities, specifically how Oracle integrates artificial intelligence and machine learning features directly into its database products. The page highlights how users can leverage these built-in AI and ML tools to analyze data, build predictive models, and enhance @@ -1791,16 +2316,19 @@

    Table of Contents

  • AsyncTool.create_websearch_tool()
  • AsyncTool.delete()
  • AsyncTool.delete_tool()
  • +
  • AsyncTool.describe_tool()
  • AsyncTool.disable()
  • AsyncTool.enable()
  • AsyncTool.fetch()
  • AsyncTool.list()
  • +
  • AsyncTool.run_tool()
  • AsyncTool.set_attribute()
  • AsyncTool.set_attributes()
  • 1.1. Create Tool
  • 1.2. List Tools
  • +
  • 1.3. Inspect and run a tool
  • 2. AsyncTask
  • -
  • 4.1. Run Team
  • -
  • 4.2. Export and Import Team
  • -
  • 4.3. Lifecycle helpers
  • -
  • 4.4. List Teams
  • +
  • 4.1. Share an async team
  • +
  • 4.2. Supervised teams
  • +
  • 4.3. Run Team
  • +
  • 4.4. Export and Import Team
  • +
  • 4.5. Inspect a team
  • +
  • 4.6. Lifecycle helpers +
  • -
  • 5. Async AI agent examples @@ -1874,8 +2427,8 @@

    Previous topic

  • This Page

    @@ -1905,12 +2458,15 @@

    Navigation

    index
  • - modules |
  • +
  • + next |
  • previous |
  • -
  • Python logo
  • +
  • Python logo
  • Python »
  • @@ -1918,7 +2474,7 @@

    Navigation

  • - Select AI for Python 1.4.0 documentation » + Select AI for Python 1.5.0 documentation »
  • @@ -1963,7 +2519,7 @@

    Navigation


    - Created using Sphinx 8.2.3. + Created using Sphinx 9.1.0.
    diff --git a/docs/user_guide/async_profile.html b/docs/user_guide/async_profile.html index e72fbd3..db75af0 100644 --- a/docs/user_guide/async_profile.html +++ b/docs/user_guide/async_profile.html @@ -5,16 +5,16 @@ - 1. Async profile lifecycle — Select AI for Python 1.4.0 documentation + 1. Async profile lifecycle — Select AI for Python 1.5.0 documentation + - - + - - - + + + @@ -75,13 +75,18 @@

    Table of Contents

  • AsyncProfile.delete()
  • AsyncProfile.delete_feedback()
  • AsyncProfile.delete_profile()
  • +
  • AsyncProfile.disable()
  • +
  • AsyncProfile.enable()
  • AsyncProfile.explain_sql()
  • AsyncProfile.fetch()
  • AsyncProfile.generate()
  • AsyncProfile.generate_synthetic_data()
  • AsyncProfile.get_attributes()
  • +
  • AsyncProfile.grant_access()
  • AsyncProfile.list()
  • AsyncProfile.narrate()
  • +
  • AsyncProfile.qualified_name
  • +
  • AsyncProfile.revoke_access()
  • AsyncProfile.run_pipeline()
  • AsyncProfile.run_sql()
  • AsyncProfile.set_attribute()
  • @@ -96,19 +101,31 @@

    Table of Contents

  • 2.2. Async Profile creation
  • 2.3. Reuse Async Profile
  • -
  • 2.4. Update Async Profile
  • -
  • 2.5. Delete Async Profile
  • -
  • 2.6. Async explain SQL
  • -
  • 2.7. Async run SQL
  • -
  • 2.8. Async show SQL
  • -
  • 2.9. Async show prompt
  • -
  • 2.10. Async concurrent SQL
  • -
  • 2.11. Async chat
  • -
  • 2.12. Async streaming chat
  • -
  • 2.13. Summarize
  • -
  • 2.14. Translate
  • -
  • 2.15. Async pipeline
  • -
  • 2.16. List profiles asynchronously
  • +
  • 2.4. Enable and disable AsyncProfile
  • +
  • 2.5. Update Async Profile
  • +
  • 2.6. Delete Async Profile
  • +
  • 2.7. Async explain SQL
  • +
  • 2.8. Async run SQL
  • +
  • 2.9. Async show SQL
  • +
  • 2.10. Async show prompt
  • +
  • 2.11. Async concurrent SQL
  • +
  • 2.12. Async chat
  • +
  • 2.13. Async streaming chat
  • +
  • 2.14. Async request-level profile attribute overrides
  • +
  • 2.15. Summarize
  • +
  • 2.16. Translate
  • +
  • 2.17. Async pipeline +
  • +
  • 2.18. Share a profile asynchronously
  • +
  • 2.19. Give async feedback on generated SQL
  • +
  • 2.20. List profiles asynchronously +
  • @@ -151,13 +168,16 @@

    Navigation

  • index
  • +
  • + modules |
  • next |
  • previous |
  • -
  • Python logo
  • +
  • Python logo
  • Python »
  • @@ -165,7 +185,7 @@

    Navigation

  • - Select AI for Python 1.4.0 documentation » + Select AI for Python 1.5.0 documentation »
  • @@ -209,7 +229,7 @@

    Navigation

    Because AsyncProfile initializes itself from the database, create or fetch instances with await:

    async_profile = await select_ai.AsyncProfile(
    -    profile_name="async_oci_ai_profile",
    +    profile_name="async_oci_ai_profile",
     )
     
    @@ -229,6 +249,7 @@

    1. Async profile lifecycleawait 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.

  • @@ -280,21 +301,20 @@

    2. Async profile actions2.1. AsyncProfile API

    -class select_ai.AsyncProfile(*args, **kwargs)
    +class select_ai.AsyncProfile(*args, **kwargs)

    AsyncProfile defines methods to interact with the underlying AI Provider asynchronously.

    -async add_negative_feedback(prompt_spec: Tuple[str, Action] | None = None, sql_id: str | None = None, response: str | None = None, feedback_content: str | None = None)
    +async add_negative_feedback(prompt_spec: Tuple[str, Action] | None = None, sql_id: str | None = None, response: str | None = None, feedback_content: str | None = None)

    Give negative feedback to the LLM

    Parameters:
      -
    • prompt_spec (Tuple[str, Action]) – First element is the prompt and -second is the corresponding action

    • -
    • sql_id (str) – SQL identifier from V$MAPPED_SQL view

    • -
    • response (str) – Expected SQL from LLM

    • -
    • feedback_content (str) – Actual feedback in natural language

    • +
    • prompt_spec (Tuple[str, select_ai.action.Action]) – First element is the prompt; second is the corresponding action

    • +
    • sql_id (str) – SQL identifier from V$MAPPED_SQL view

    • +
    • response (str) – Expected SQL from LLM

    • +
    • feedback_content (str) – Actual feedback in natural language

    @@ -302,14 +322,13 @@

    2.1.
    -async add_positive_feedback(prompt_spec: Tuple[str, Action] | None = None, sql_id: str | None = None)
    +async add_positive_feedback(prompt_spec: Tuple[str, Action] | None = None, sql_id: str | None = None)

    Give positive feedback to the LLM

    Parameters:
      -
    • prompt_spec (Tuple[str, Action]) – First element is the prompt and -second is the corresponding action

    • -
    • sql_id (str) – SQL identifier from V$MAPPED_SQL view

    • +
    • prompt_spec (Tuple[str, select_ai.action.Action]) – First element is the prompt; second is the corresponding action

    • +
    • sql_id (str) – SQL identifier from V$MAPPED_SQL view

    @@ -317,15 +336,17 @@

    2.1.
    -async chat(prompt, params: Mapping = None, stream: bool = False, chunk_size: int = 8192) → str | AsyncGenerator[str, None]
    +async chat(prompt, params: Mapping = None, stream: bool = False, chunk_size: int = 8192, *, attributes: Mapping = None) → str | AsyncGenerator[str, None]

    Asynchronously chat with the LLM

    Parameters:
      -
    • prompt (str) – Natural language prompt

    • +
    • prompt (str) – Natural language prompt

    • params – Parameters to include in the LLM request

    • -
    • stream (bool) – Return an async iterator of response chunks

    • -
    • chunk_size (int) – Number of characters to read per stream chunk

    • +
    • attributes (Mapping) – Profile attributes to override for this +request

    • +
    • stream (bool) – Return an async iterator of response chunks

    • +
    • chunk_size (int) – Number of characters to read per stream chunk

    Returns:
    @@ -336,14 +357,14 @@

    2.1.
    -chat_session(conversation: AsyncConversation, delete: bool = False)
    +chat_session(conversation: AsyncConversation, delete: bool = False)

    Starts a new chat session for context-aware conversations

    Parameters:
    • conversation (AsyncConversation) – Conversation object to use for this chat session

    • -
    • delete (bool) – Delete conversation after session ends

    • +
    • delete (bool) – Delete conversation after session ends

    @@ -351,11 +372,11 @@

    2.1.
    -async create(replace: int | None = False) → None
    +async create(replace: int | None = False) → None

    Asynchronously create an AI Profile in the Database

    Parameters:
    -

    replace (bool) – Set True to replace else False

    +

    replace (bool) – Set True to replace else False

    Returns:

    None

    @@ -368,11 +389,11 @@

    2.1.
    -async delete(force=False) → None
    +async delete(force=False) → None

    Asynchronously deletes an AI profile from the database

    Parameters:
    -

    force (bool) – Ignores errors if AI profile does not exist.

    +

    force (bool) – Ignores errors if AI profile does not exist.

    Returns:

    None

    @@ -385,14 +406,13 @@

    2.1.
    -async delete_feedback(prompt_spec: Tuple[str, Action] = None, sql_id: str | None = None)
    +async delete_feedback(prompt_spec: Tuple[str, Action] = None, sql_id: str | None = None)

    Delete feedback from the database

    Parameters:
      -
    • prompt_spec (Tuple[str, Action]) – First element is the prompt and -second is the corresponding action

    • -
    • sql_id (str) – SQL identifier from V$MAPPED_SQL view

    • +
    • prompt_spec (Tuple[str, select_ai.action.Action]) – First element is the prompt; second is the corresponding action

    • +
    • sql_id (str) – SQL identifier from V$MAPPED_SQL view

    @@ -400,13 +420,13 @@

    2.1.
    -async classmethod delete_profile(profile_name: str, force: bool = False)
    +async classmethod delete_profile(profile_name: str, force: bool = False)

    Asynchronously deletes an AI profile from the database

    Parameters:
      -
    • profile_name (str) – Name of the AI profile

    • -
    • force (bool) – Ignores errors if AI profile does not exist.

    • +
    • profile_name (str) – Name of the AI profile

    • +
    • force (bool) – Ignores errors if AI profile does not exist.

    Returns:
    @@ -418,17 +438,47 @@

    2.1. +
    +async disable() → None
    +

    Asynchronously disable this AI profile in the database.

    +
    +
    Returns:
    +

    None

    +
    +
    Raises:
    +

    oracledb.DatabaseError

    +
    +
    +

    + +
    +
    +async enable() → None
    +

    Asynchronously enable this AI profile in the database.

    +
    +
    Returns:
    +

    None

    +
    +
    Raises:
    +

    oracledb.DatabaseError

    +
    +
    +
    +
    -async explain_sql(prompt: str, params: Mapping = None, stream: bool = False, chunk_size: int = 8192)
    +async explain_sql(prompt: str, params: Mapping = None, stream: bool = False, chunk_size: int = 8192, *, attributes: Mapping = None)

    Explain the generated SQL

    Parameters:
      -
    • prompt (str) – Natural language prompt

    • +
    • prompt (str) – Natural language prompt

    • params – Parameters to include in the LLM request

    • -
    • stream (bool) – Return an async iterator of response chunks

    • -
    • chunk_size (int) – Number of characters to read per stream chunk

    • +
    • attributes (Mapping) – Profile attributes to override for this +request

    • +
    • stream (bool) – Return an async iterator of response chunks

    • +
    • chunk_size (int) – Number of characters to read per stream chunk

    Returns:
    @@ -439,12 +489,15 @@

    2.1.
    -async classmethod fetch(profile_name: str) → AsyncProfile
    +async classmethod fetch(profile_name: str, owner: str | None = None) → AsyncProfile

    Asynchronously create an AI Profile object from attributes saved in the database

    Parameters:
    -

    profile_name (str)

    +
      +
    • profile_name (str) – Name of the AI profile.

    • +
    • owner (str) – Owner of a shared profile. Defaults to current schema.

    • +
    Returns:

    select_ai.Profile

    @@ -457,17 +510,19 @@

    2.1.
    -async generate(prompt: str, action=Action.SHOWSQL, params: Mapping = None, stream: bool = False, chunk_size: int = 8192) → DataFrame | str | AsyncGenerator[str, None] | None
    +async generate(prompt: str, action=Action.SHOWSQL, params: Mapping = None, stream: bool = False, chunk_size: int = 8192, *, attributes: Mapping = None) → DataFrame | str | AsyncGenerator[str, None] | None

    Asynchronously perform AI translation using this profile

    Parameters:
      -
    • prompt (str) – Natural language prompt to translate

    • -
    • action (select_ai.profile.Action)

    • +
    • prompt (str) – Natural language prompt to translate

    • +
    • action (select_ai.action.Action)

    • params – Parameters to include in the LLM request. For e.g. conversation_id for context-aware chats

    • -
    • stream (bool) – Return an async iterator of response chunks

    • -
    • chunk_size (int) – Number of characters to read per stream chunk

    • +
    • attributes (Mapping) – Profile attributes to override for this +request

    • +
    • stream (bool) – Return an async iterator of response chunks

    • +
    • chunk_size (int) – Number of characters to read per stream chunk

    Returns:
    @@ -478,7 +533,7 @@

    2.1.
    -async generate_synthetic_data(synthetic_data_attributes: SyntheticDataAttributes) → None
    +async generate_synthetic_data(synthetic_data_attributes: SyntheticDataAttributes) → None

    Generate synthetic data for a single table, multiple tables or a full schema.

    @@ -496,11 +551,11 @@

    2.1.
    -async get_attributes() → ProfileAttributes
    +async get_attributes() → ProfileAttributes

    Asynchronously gets AI profile attributes from the Database

    Returns:
    -

    select_ai.provider.ProviderAttributes

    +

    select_ai.ProfileAttributes

    Raises:

    ProfileNotFoundError

    @@ -508,15 +563,24 @@

    2.1. +
    +async grant_access(user_or_role_name: str) → None
    +

    Asynchronously grant a user or role access to this AI profile.

    +

    +
    -classmethod list(profile_name_pattern: str = '.*') → AsyncGenerator[AsyncProfile, None]
    +classmethod list(profile_name_pattern: str = '.*', owner: str | None = None) → AsyncGenerator[AsyncProfile, None]

    Asynchronously list AI Profiles saved in the database.

    Parameters:
    -

    profile_name_pattern (str) – Regular expressions can be used +

      +
    • profile_name_pattern (str) – Regular expressions can be used to specify a pattern. Function REGEXP_LIKE is used to perform the -match. Default value is “.*” i.e. match all AI profiles.

      +match. Default value is “.*” i.e. match all AI profiles.

    • +
    • owner (str) – Owner of shared profiles. Defaults to current schema.

    • +
    Returns:

    Iterator[Profile]

    @@ -526,15 +590,17 @@

    2.1.
    -async narrate(prompt, params: Mapping = None, stream: bool = False, chunk_size: int = 8192) → str | AsyncGenerator[str, None]
    +async narrate(prompt, params: Mapping = None, stream: bool = False, chunk_size: int = 8192, *, attributes: Mapping = None) → str | AsyncGenerator[str, None]

    Narrate the result of the SQL

    Parameters:
      -
    • prompt (str) – Natural language prompt

    • +
    • prompt (str) – Natural language prompt

    • params – Parameters to include in the LLM request

    • -
    • stream (bool) – Return an async iterator of response chunks

    • -
    • chunk_size (int) – Number of characters to read per stream chunk

    • +
    • attributes (Mapping) – Profile attributes to override for this +request

    • +
    • stream (bool) – Return an async iterator of response chunks

    • +
    • chunk_size (int) – Number of characters to read per stream chunk

    Returns:
    @@ -543,34 +609,54 @@

    2.1. +
    +property qualified_name: str | None
    +

    Return the owner-qualified profile name when owner is known.

    +

    + +
    +
    +async revoke_access(user_or_role_name: str) → None
    +

    Asynchronously revoke a user or role’s profile access.

    +
    +
    -async run_pipeline(prompt_specifications: List[Tuple[str, Action]], continue_on_error: bool = False) → List[str | DataFrame]
    -

    Send Multiple prompts in a single roundtrip to the Database

    +async run_pipeline(prompt_specifications: List[Tuple[str, Action]], continue_on_error: bool = False, *, attributes: Mapping = None) → List[object] +

    Submit prompt/action pairs as a database pipeline

    Parameters:
      -
    • prompt_specifications (List[Tuple[str, Action]]) – List of -2-element tuples. First element is the prompt and second is the +

    • prompt_specifications (List[Tuple[str, select_ai.action.Action]]) – List of 2-element tuples. First element is the prompt and second is the corresponding action

    • -
    • continue_on_error (bool) – True to continue on error else False

    • +
    • continue_on_error (bool) – True to continue on error else False

    • +
    • attributes (Mapping) – Profile attributes to override for every +request in the pipeline

    Returns:
    -

    List[Union[str, pandas.DataFrame]]

    +

    Ordered list of strings for successful operations and driver +error objects for failures when continue_on_error=True. RUNSQL +results remain JSON strings; they are not converted to DataFrames.

    +

    Operations execute sequentially. Round-trip reduction requires a +supporting database version; reading returned CLOBs adds calls. +With continue_on_error=False, an operation failure raises an exception.

    -async run_sql(prompt, params: Mapping = None) → DataFrame
    +async run_sql(prompt, params: Mapping = None, *, attributes: Mapping = None) → DataFrame

    Explain the generated SQL

    Parameters:
      -
    • prompt (str) – Natural language prompt

    • +
    • prompt (str) – Natural language prompt

    • params – Parameters to include in the LLM request

    • +
    • attributes (Mapping) – Profile attributes to override for this +request

    Returns:
    @@ -581,14 +667,14 @@

    2.1.
    -async set_attribute(attribute_name: str, attribute_value: bool | str | int | float | Provider)
    +async set_attribute(attribute_name: str, attribute_value: bool | str | int | float | Provider)

    Updates AI profile attribute on the Python object and also saves it in the database

    Parameters:
      -
    • attribute_name (str) – Name of the AI profile attribute

    • -
    • attribute_value (Union[bool, str, int, float]) – Value of the +

    • attribute_name (str) – Name of the AI profile attribute

    • +
    • attribute_value (Union[bool, str, int, float]) – Value of the profile attribute

    @@ -600,7 +686,7 @@

    2.1.
    -async set_attributes(attributes: ProfileAttributes)
    +async set_attributes(attributes: ProfileAttributes)

    Updates AI profile attributes on the Python object and also saves it in the database

    @@ -616,15 +702,17 @@

    2.1.
    -async show_prompt(prompt: str, params: Mapping = None, stream: bool = False, chunk_size: int = 8192)
    +async show_prompt(prompt: str, params: Mapping = None, stream: bool = False, chunk_size: int = 8192, *, attributes: Mapping = None)

    Show the prompt sent to LLM

    Parameters:
      -
    • prompt (str) – Natural language prompt

    • +
    • prompt (str) – Natural language prompt

    • params – Parameters to include in the LLM request

    • -
    • stream (bool) – Return an async iterator of response chunks

    • -
    • chunk_size (int) – Number of characters to read per stream chunk

    • +
    • attributes (Mapping) – Profile attributes to override for this +request

    • +
    • stream (bool) – Return an async iterator of response chunks

    • +
    • chunk_size (int) – Number of characters to read per stream chunk

    Returns:
    @@ -635,15 +723,17 @@

    2.1.
    -async show_sql(prompt, params: Mapping = None, stream: bool = False, chunk_size: int = 8192)
    +async show_sql(prompt, params: Mapping = None, stream: bool = False, chunk_size: int = 8192, *, attributes: Mapping = None)

    Show the generated SQL

    Parameters:
      -
    • prompt (str) – Natural language prompt

    • +
    • prompt (str) – Natural language prompt

    • params – Parameters to include in the LLM request

    • -
    • stream (bool) – Return an async iterator of response chunks

    • -
    • chunk_size (int) – Number of characters to read per stream chunk

    • +
    • attributes (Mapping) – Profile attributes to override for this +request

    • +
    • stream (bool) – Return an async iterator of response chunks

    • +
    • chunk_size (int) – Number of characters to read per stream chunk

    Returns:
    @@ -654,17 +744,17 @@

    2.1.
    -async summarize(content: str = None, prompt: str = None, location_uri: str = None, credential_name: str = None, params: SummaryParams = None) → str
    +async summarize(content: str = None, prompt: str = None, location_uri: str = None, credential_name: str = None, params: SummaryParams = None) → str

    Generate summary

    Parameters:

    @@ -773,28 +866,67 @@

    2.3. Reuse Async ProfileAsyncProfile with only the profile name to reuse the database profile:

    async_profile = await select_ai.AsyncProfile(
    -    profile_name="async_oci_ai_profile",
    +    profile_name="async_oci_ai_profile",
     )
    -sql = await async_profile.show_sql(prompt="How many promotions?")
    +sql = await async_profile.show_sql(prompt="How many promotions?")
     

    Use AsyncProfile.fetch(...) when you want to create a proxy object from a saved database profile and raise an error if the profile does not exist:

    async_profile = await select_ai.AsyncProfile.fetch(
    -    "async_oci_ai_profile"
    +    "async_oci_ai_profile"
     )
     

    +
    +

    2.4. 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.

    +
    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())
    +
    +
    +

    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
    +
    +
    +
    -

    2.4. Update Async Profile

    +

    2.5. Update Async Profile

    Use set_attribute(...) to update one profile attribute or set_attributes(...) to update several attributes. Updates are saved to the database profile.

    async_profile = await select_ai.AsyncProfile(
    -    profile_name="async_oci_ai_profile",
    +    profile_name="async_oci_ai_profile",
     )
    -await async_profile.set_attribute("temperature", 0.1)
    +await async_profile.set_attribute("temperature", 0.1)
     
     await async_profile.set_attributes(
         select_ai.ProfileAttributes(
    @@ -806,40 +938,40 @@ 

    2.4. Update Async Profile -

    2.5. Delete Async Profile

    +

    2.6. Delete Async Profile

    Use delete(...) or AsyncProfile.delete_profile(...) to remove a profile from the database. Pass force=True when cleanup should succeed even if the profile does not exist.

    async_profile = await select_ai.AsyncProfile(
    -    profile_name="async_oci_ai_profile",
    +    profile_name="async_oci_ai_profile",
     )
     await async_profile.delete(force=True)
     
     await select_ai.AsyncProfile.delete_profile(
    -    "async_oci_ai_profile",
    +    "async_oci_ai_profile",
         force=True,
     )
     
    -

    2.6. Async explain SQL

    +

    2.7. Async explain SQL

    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")
    +user = os.getenv("SELECT_AI_USER")
    +password = os.getenv("SELECT_AI_PASSWORD")
    +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING")
     
     
     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="async_oci_ai_profile",
         )
    -    response = await async_profile.explain_sql("How many promotions ?")
    +    response = await async_profile.explain_sql("How many promotions ?")
         print(response)
     
     
    @@ -847,46 +979,46 @@ 

    2.6. Async explain SQL
    To answer the question "How many promotions", we need to write a SQL query that counts the number of rows in the "PROMOTIONS" table. Here is the query:
    +
    To answer the question "How many promotions", we need to write a SQL query that counts the number of rows in the "PROMOTIONS" table. Here is the query:
     
     ```sql
     SELECT
    -  COUNT("p"."PROMO_ID") AS "Number of Promotions"
    +  COUNT("p"."PROMO_ID") AS "Number of Promotions"
     FROM
    -  "SH"."PROMOTIONS" "p";
    +  "SH"."PROMOTIONS" "p";
     ```
     
     Explanation:
     
     * We use the `COUNT` function to count the number of rows in the table.
    -* We use the table alias `"p"` to refer to the `"PROMOTIONS"` table.
    +* We use the table alias `"p"` to refer to the `"PROMOTIONS"` table.
     * We enclose the table name and column name in double quotes to make them case-sensitive.
     * We use the `AS` keyword to give an alias to the count column, making it easier to read.
     
    -This query will return the total number of promotions in the `"PROMOTIONS"` table.
    +This query will return the total number of promotions in the `"PROMOTIONS"` table.
     

    -

    2.7. Async run SQL

    +

    2.8. Async run SQL

    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")
    +user = os.getenv("SELECT_AI_USER")
    +password = os.getenv("SELECT_AI_PASSWORD")
    +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING")
     
     
     # This example shows how to asynchronously run sql
     async def main():
         await select_ai.async_connect(user=user, password=password, dsn=dsn)
         async_profile = await select_ai.AsyncProfile(
    -        profile_name="async_oci_ai_profile",
    +        profile_name="async_oci_ai_profile",
         )
         # run_sql returns a pandas df
    -    df = await async_profile.run_sql("How many promotions?")
    +    df = await async_profile.run_sql("How many promotions?")
         print(df)
     
     
    @@ -900,23 +1032,23 @@ 

    2.7. Async run SQL

    -

    2.8. Async show SQL

    +

    2.9. Async show SQL

    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")
    +user = os.getenv("SELECT_AI_USER")
    +password = os.getenv("SELECT_AI_PASSWORD")
    +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING")
     
     
     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="async_oci_ai_profile",
         )
    -    response = await async_profile.show_sql("How many promotions?")
    +    response = await async_profile.show_sql("How many promotions?")
         print(response)
     
     
    @@ -924,76 +1056,86 @@ 

    2.8. Async show SQL

    output:

    -
    SELECT COUNT("p"."PROMO_ID") AS "PROMOTION_COUNT" FROM "SH"."PROMOTIONS" "p"
    +
    SELECT COUNT("p"."PROMO_ID") AS "PROMOTION_COUNT" FROM "SH"."PROMOTIONS" "p"
     
    -

    2.9. Async show prompt

    +

    2.10. Async show prompt

    Use show_prompt(...) to inspect the prompt that Select AI sends to the model. This is useful when tuning profile attributes, object lists, comments, constraints, and provider settings.

    async_profile = await select_ai.AsyncProfile(
    -    profile_name="async_oci_ai_profile",
    +    profile_name="async_oci_ai_profile",
     )
    -prompt = await async_profile.show_prompt(prompt="How many promotions?")
    +prompt = await async_profile.show_prompt(prompt="How many promotions?")
     print(prompt)
     
    -

    2.10. Async concurrent SQL

    +

    2.11. Async concurrent SQL

    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")
    +user = os.getenv("SELECT_AI_USER")
    +password = os.getenv("SELECT_AI_PASSWORD")
    +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING")
     
     
     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",
    +    select_ai.create_pool_async(
    +        user=user, password=password, dsn=dsn,
    +        min_size=1, max_size=3, increment=1,
         )
    -    sql_tasks = [
    -        async_profile.show_sql(prompt="How many customers?"),
    -        async_profile.run_sql(prompt="How many promotions?"),
    -        async_profile.explain_sql(prompt="How many promotions?"),
    -    ]
    -
    -    # Collect results from multiple asynchronous tasks
    -    for sql_task in asyncio.as_completed(sql_tasks):
    -        result = await sql_task
    -        print(result)
    +    tasks = []
    +    try:
    +        async_profile = await select_ai.AsyncProfile(
    +            profile_name="async_oci_ai_profile",
    +        )
    +        # Each operation acquires its own connection from the pool.
    +        tasks = [
    +            asyncio.create_task(async_profile.show_sql("How many customers?")),
    +            asyncio.create_task(async_profile.run_sql("How many promotions?")),
    +            asyncio.create_task(async_profile.explain_sql("How many promotions?")),
    +        ]
    +        for task in asyncio.as_completed(tasks):
    +            print(await task)
    +    finally:
    +        # Finish cancellation before closing connections, including on errors.
    +        for task in tasks:
    +            if not task.done():
    +                task.cancel()
    +        await asyncio.gather(*tasks, return_exceptions=True)
    +        await select_ai.async_disconnect()
     
     
     asyncio.run(main())
     

    output:

    -
    SELECT COUNT("c"."CUST_ID") AS "customer_count" FROM "SH"."CUSTOMERS" "c"
    +
    SELECT COUNT("c"."CUST_ID") AS "customer_count" FROM "SH"."CUSTOMERS" "c"
     
    -To answer the question "How many promotions", we need to write a SQL query that counts the number of rows in the "PROMOTIONS" table. Here is the query:
    +To answer the question "How many promotions", we need to write a SQL query that counts the number of rows in the "PROMOTIONS" table. Here is the query:
     
     ```sql
     SELECT
    -  COUNT("p"."PROMO_ID") AS "number_of_promotions"
    +  COUNT("p"."PROMO_ID") AS "number_of_promotions"
     FROM
    -  "SH"."PROMOTIONS" "p";
    +  "SH"."PROMOTIONS" "p";
     ```
     
     Explanation:
     
     * We use the `COUNT` function to count the number of rows in the table.
    -* We use the table alias `"p"` to refer to the `"PROMOTIONS"` table.
    -* We specify the schema name `"SH"` to ensure that we are accessing the correct table.
    +* We use the table alias `"p"` to refer to the `"PROMOTIONS"` table.
    +* We specify the schema name `"SH"` to ensure that we are accessing the correct table.
     * We enclose the table name, schema name, and column name in double quotes to make them case-sensitive.
    -* The `AS` keyword is used to give an alias to the calculated column, in this case, `"number_of_promotions"`.
    +* The `AS` keyword is used to give an alias to the calculated column, in this case, `"number_of_promotions"`.
     
    -This query will return the total number of promotions in the `"PROMOTIONS"` table.
    +This query will return the total number of promotions in the `"PROMOTIONS"` table.
     
        PROMOTION_COUNT
     0              503
    @@ -1001,28 +1143,28 @@ 

    2.10. Async concurrent SQL -

    2.11. Async chat

    +

    2.12. Async chat

    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")
    +user = os.getenv("SELECT_AI_USER")
    +password = os.getenv("SELECT_AI_PASSWORD")
    +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING")
     
     
     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="async_oci_ai_profile"
         )
     
         # Asynchronously send multiple chat prompts
         chat_tasks = [
    -        async_profile.chat(prompt="What is OCI ?"),
    -        async_profile.chat(prompt="What is OML4PY?"),
    -        async_profile.chat(prompt="What is Autonomous Database ?"),
    +        async_profile.chat(prompt="What is OCI ?"),
    +        async_profile.chat(prompt="What is OML4PY?"),
    +        async_profile.chat(prompt="What is Autonomous Database ?"),
         ]
         for chat_task in asyncio.as_completed(chat_tasks):
             result = await chat_task
    @@ -1050,28 +1192,28 @@ 

    2.11. Async chat

    -

    2.12. Async streaming chat

    +

    2.13. Async streaming chat

    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")
    +user = os.getenv("SELECT_AI_USER")
    +password = os.getenv("SELECT_AI_PASSWORD")
    +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING")
     
     
     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="async_oci_ai_profile"
         )
     
         chunks = await async_profile.chat(
    -        prompt="What is OCI ?", stream=True, chunk_size=4096
    +        prompt="What is OCI ?", stream=True, chunk_size=4096
         )
         async for chunk in chunks:
    -        print(chunk, end="")
    +        print(chunk, end="")
         print()
     
     
    @@ -1087,34 +1229,145 @@ 

    2.12. Async streaming chatgenerate(), chat(), narrate(), explain_sql(), show_sql(), and show_prompt(). It is not supported for run_sql(), which returns a pandas.DataFrame.

    +

    The SDK awaits the database function before reading its returned CLOB in +chunks. The first chunk is available after that function returns; this does +not guarantee token delivery while the model is generating. A partially +consumed iterator retains its cursor and connection. Use aclosing() when +iteration can stop early, and consume session streams before leaving the +chat_session() context:

    +
    from contextlib import aclosing
    +
    +chunks = await async_profile.chat("Explain connection pooling.", stream=True)
    +async with aclosing(chunks):
    +    async for chunk in chunks:
    +        print(chunk, end="")
    +
    +
    +

    +
    +

    2.14. 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.

    +
    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:

    +
    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:

    +
    
    +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())
    +
    +
    +

    output:

    +
    Request attributes applied: True
    +
    +
    -

    2.13. Summarize

    +

    2.15. Summarize

    Summarize inline content

    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")
    +user = os.getenv("SELECT_AI_USER")
    +password = os.getenv("SELECT_AI_PASSWORD")
    +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING")
     
    -content = """
    +content = """
     A gas cloud in our galaxy, Sagittarius B2, contains enough alcohol to brew 400
     trillion pints of beer, and some stars are so cool that you could touch them
     without being burned. Meanwhile, on the exoplanet 55 Cancri e, a form of
    -"hot ice" exists where high pressure prevents water from becoming gas even at
    -high temperatures. Additionally, some ancient stars found in the Milky Way's
    +"hot ice" exists where high pressure prevents water from becoming gas even at
    +high temperatures. Additionally, some ancient stars found in the Milky Way's
     halo are much older than the Sun, providing clues about the early universe and
     its composition
    -"""
    +"""
     
     
     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="async_oci_ai_profile",
         )
         summary = await async_profile.summarize(content=content)
         print(summary)
    @@ -1126,10 +1379,10 @@ 

    2.13. Summarizeoutput:

    A gas cloud in the Sagittarius B2 galaxy contains a large amount of alcohol,
     while some stars are cool enough to touch without being burned. The exoplanet
    -55 Cancri e has a unique form of "hot ice" where water remains solid despite
    -high temperatures due to high pressure. Ancient stars in the Milky Way's halo
    +55 Cancri e has a unique form of "hot ice" where water remains solid despite
    +high temperatures due to high pressure. Ancient stars in the Milky Way's halo
     are older than the Sun, providing insights into the early universe and its composition,
    -offering clues about the universe's formation and evolution.
    +offering clues about the universe's formation and evolution.
     

    Summarize content accessible via a URL

    @@ -1138,18 +1391,18 @@

    2.13. Summarizeimport select_ai -user = os.getenv("SELECT_AI_USER") -password = os.getenv("SELECT_AI_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +user = os.getenv("SELECT_AI_USER") +password = os.getenv("SELECT_AI_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") 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="async_oci_ai_profile", ) summary = await async_profile.summarize( - location_uri="https://en.wikipedia.org/wiki/Astronomy" + location_uri="https://en.wikipedia.org/wiki/Astronomy" ) print(summary) @@ -1177,26 +1430,49 @@

    2.13. Summarize

    -

    2.14. Translate

    +

    2.16. 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:

    +
    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:

    +
    print(await async_profile.translate(text="Thank you", target_language="de"))
    +
    +
    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")
    +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",
    +        text="Thank you",
    +        target_language="de",
         )
         print(response)
     
    @@ -1210,44 +1486,81 @@ 

    2.14. Translate

    -

    2.15. Async pipeline

    +

    2.17. Async pipeline

    +
    +

    2.17.1. Pipeline compatibility and results

    +

    run_pipeline() requires python-oracledb>=2.4.0, which provides +create_pipeline() and AsyncConnection.run_pipeline(). If an older +driver lacks these APIs, upgrade it with python -m pip install --upgrade +oracledb. Async operations use Thin mode.

    +

    The driver documents true pipelining for Oracle AI Database 26ai and later. +On older databases it executes operations individually, without reducing +round trips. Pipeline operations execute sequentially, even when true +pipelining is available. Select AI returns CLOBs, and reading these adds +database calls; do not assume the complete SDK operation uses one round trip. +See python-oracledb pipelining.

    +

    Results preserve input order. Successful results are strings read from CLOBs. +For Action.RUNSQL, this is the database’s JSON response, rather than the +pandas.DataFrame returned by AsyncProfile.run_sql(). With +continue_on_error=False (the default), an operation error raises an +exception. With continue_on_error=True, failed entries contain driver +error objects; check their type before using them as text:

    +
    results = await async_profile.run_pipeline(
    +    [("How many promotions?", select_ai.Action.SHOWSQL)],
    +    continue_on_error=True,
    +)
    +for result in results:
    +    if isinstance(result, str):
    +        print(result)
    +    else:
    +        print(f"Pipeline operation failed: {result}")
    +
    +
    +

    AsyncProfile.run_pipeline() also accepts attributes=.... The mapping +is applied to every prompt/action request in the pipeline, while the saved +profile remains unchanged.

    +
    results = await async_profile.run_pipeline(
    +    prompt_specifications,
    +    attributes={
    +        "additional_instructions": "Keep each answer concise.",
    +        "seed": 42,
    +    },
    +)
    +
    +
    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")
    +user = os.getenv("SELECT_AI_USER")
    +password = os.getenv("SELECT_AI_PASSWORD")
    +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING")
     
     
     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="async_oci_ai_profile"
         )
         prompt_specifications = [
    -        ("What is Oracle Autonomous Database?", select_ai.Action.CHAT),
    -        ("Generate SQL to list all customers?", select_ai.Action.SHOWSQL),
    +        ("What is Oracle Autonomous Database?", select_ai.Action.CHAT),
    +        ("Generate SQL to list all customers?", select_ai.Action.SHOWSQL),
             (
    -            "Explain the query: SELECT * FROM sh.products",
    +            "Explain the query: SELECT * FROM sh.products",
                 select_ai.Action.EXPLAINSQL,
             ),
    -        ("Explain the query: SELECT * FROM sh.products", "INVALID ACTION"),
    +        ("Explain the query: SELECT * FROM sh.products", "INVALID ACTION"),
         ]
     
    -    # 1. Multiple prompts are sent in a single roundtrip to the Database
    -    # 2. Results are returned as soon as Database has executed all prompts
    -    # 3. Application doesn't have to wait on one response before sending
    -    #    the next prompts
    -    # 4. Fewer round trips and database is kept busy
    -    # 5. Efficient network usage
    +    # Operations execute sequentially; round-trip savings depend on the
    +    # database version. Successful results are strings, including RUNSQL JSON.
         results = await async_profile.run_pipeline(
             prompt_specifications, continue_on_error=True
         )
         for i, result in enumerate(results):
             print(
    -            f"Result {i} for prompt '{prompt_specifications[i][0]}' is: {result}"
    +            f"Result {i} for prompt '{prompt_specifications[i][0]}' is: {result}"
             )
     
     
    @@ -1255,61 +1568,142 @@ 

    2.15. Async pipeline
    Result 0 for prompt 'What is Oracle Autonomous Database?' is: Oracle Autonomous Database is a cloud-based database service that uses artificial intelligence (AI) and machine learning (ML) to automate many of the tasks associated with managing a database. It is a self-driving, self-securing, and self-repairing database that eliminates the need for manual database administration, allowing users to focus on higher-level tasks.
    +
    Result 0 for prompt 'What is Oracle Autonomous Database?' is: Oracle Autonomous Database is a cloud-based database service that uses artificial intelligence (AI) and machine learning (ML) to automate many of the tasks associated with managing a database. It is a self-driving, self-securing, and self-repairing database that eliminates the need for manual database administration, allowing users to focus on higher-level tasks.
     
     
    -Result 1 for prompt 'Generate SQL to list all customers?' is: SELECT "c"."CUST_ID" AS "Customer ID", "c"."CUST_FIRST_NAME" AS "First Name", "c"."CUST_LAST_NAME" AS "Last Name", "c"."CUST_EMAIL" AS "Email" FROM "SH"."CUSTOMERS" "c"
    +Result 1 for prompt 'Generate SQL to list all customers?' is: SELECT "c"."CUST_ID" AS "Customer ID", "c"."CUST_FIRST_NAME" AS "First Name", "c"."CUST_LAST_NAME" AS "Last Name", "c"."CUST_EMAIL" AS "Email" FROM "SH"."CUSTOMERS" "c"
     
    -Result 2 for prompt 'Explain the query: SELECT * FROM sh.products' is: ```sql
    +Result 2 for prompt 'Explain the query: SELECT * FROM sh.products' is: ```sql
     SELECT
       p.*
     FROM
    -  "SH"."PRODUCTS" p;
    +  "SH"."PRODUCTS" p;
     ```
     
     **Explanation:**
     
    -This query is designed to retrieve all columns (`*`) from the `"SH"."PRODUCTS"` table.
    +This query is designed to retrieve all columns (`*`) from the `"SH"."PRODUCTS"` table.
     
    -Here's a breakdown of the query components:
    +Here's a breakdown of the query components:
     
     
    -Result 3 for prompt 'Explain the query: SELECT * FROM sh.products' is: ORA-20000: Invalid action - INVALID ACTION
    +Result 3 for prompt 'Explain the query: SELECT * FROM sh.products' is: ORA-20000: Invalid action - INVALID ACTION
     

    +
    +
    +

    2.18. Share a profile asynchronously

    +

    While connected as the profile owner, grant access to an existing profile:

    +
    profile = await select_ai.AsyncProfile.fetch("OCI_AI_PROFILE")
    +await profile.grant_access("APP_USER")
    +
    +
    +

    The recipient can fetch the profile with owner="APP_OWNER" in a separate +connection. When access should end, run +await profile.revoke_access("APP_USER") as the owner. See +Sharing and ownership for the complete owner and recipient +sequence.

    +
    +
    +

    2.19. Give async feedback on generated SQL

    +

    Use feedback after reviewing the SQL produced for a prompt. Identify the +request with a (prompt, Action) tuple or a sql_id from +V$MAPPED_SQL. The example below assumes an open async connection and an +existing profile over the SH schema:

    +
    profile = await select_ai.AsyncProfile.fetch("OCI_AI_PROFILE")
    +prompt = "How many promotions?"
    +sql = await profile.show_sql(prompt=prompt)
    +print(sql)
    +
    +
    +

    After inspecting the SQL, record positive feedback if it answers the question:

    +
    await profile.add_positive_feedback(
    +    prompt_spec=(prompt, select_ai.Action.SHOWSQL)
    +)
    +
    +
    +

    For an incorrect answer, use add_negative_feedback() with the same request +identifier, response containing the expected SQL, and feedback_content +explaining the correction. Remove obsolete feedback with +delete_feedback() and the same request identifier. +Await these methods when using AsyncProfile.

    +
    -

    2.16. List profiles asynchronously

    -

    Profile listing returns profiles visible to the connected database user. The -async list API returns an async iterator.

    +

    2.20. List profiles asynchronously

    +

    Use AsyncProfile.list() to discover saved profiles in the current schema. +Call it on the class; you do not need to create an empty profile first. +Each returned object includes profile_name, owner, and the +owner-qualified qualified_name.

    +
    +

    2.20.1. Connect and list your profiles

    +

    Set SELECT_AI_USER, SELECT_AI_PASSWORD, and +SELECT_AI_DB_CONNECT_STRING before running this complete script. It prints +one qualified name per profile and closes the connection afterward:

    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")
    -
     
     async def main():
    -    await select_ai.async_connect(user=user, password=password, dsn=dsn)
    -    async_profile = await select_ai.AsyncProfile()
    -    # matches the start of string
    -    async for fetched_profile in async_profile.list(
    -        profile_name_pattern="^oci"
    -    ):
    -        print(fetched_profile.profile_name)
    +    await select_ai.async_connect(
    +        user=os.environ["SELECT_AI_USER"],
    +        password=os.environ["SELECT_AI_PASSWORD"],
    +        dsn=os.environ["SELECT_AI_DB_CONNECT_STRING"],
    +    )
    +    try:
    +        async for profile in select_ai.AsyncProfile.list():
    +            print(profile.qualified_name)
    +    finally:
    +        await select_ai.async_disconnect()
     
     
     asyncio.run(main())
     
    -

    output:

    -
    OCI_VECTOR_AI_PROFILE
    -OCI_AI_PROFILE
    +

    For a schema named APP_OWNER containing two profiles, the output could be:

    +
    APP_OWNER.OCI_AI_PROFILE
    +APP_OWNER.OCI_VECTOR_AI_PROFILE
    +
    +
    +

    Your results depend on the profiles visible in your schema; result order is +not guaranteed. An empty result means no profiles match the requested scope.

    +
    +
    +

    2.20.2. Filter by name

    +

    With a connection open, use profile_name_pattern to match names with a +regular expression. This example lists names starting with OCI_:

    +
    async for profile in select_ai.AsyncProfile.list(profile_name_pattern="^OCI_"):
    +    print(profile.qualified_name)
    +
    +
    +

    AsyncProfile.list() returns an async iterator. Use async for +directly; do not await the iterator. Put these snippets inside your async +function before disconnecting.

    +
    +
    +

    2.20.3. Find and reuse a shared profile

    +

    Pass owner to list profiles in another schema. The connected user must +have access to those profiles; specifying an owner does not grant access. +While connected as the recipient, list the profiles shared by APP_OWNER:

    +
    async for profile in select_ai.AsyncProfile.list(owner="APP_OWNER"):
    +    print(profile.qualified_name)
    +
    +
    +

    Once you know the profile name, fetch that profile with the same owner and +use the returned object directly:

    +
    profile = await select_ai.AsyncProfile.fetch(
    +    "OCI_AI_PROFILE", owner="APP_OWNER"
    +)
    +print(profile.qualified_name)  # APP_OWNER.OCI_AI_PROFILE
    +response = await profile.chat(prompt="What is retrieval augmented generation?")
    +print(response)
     
    +

    See Sharing and ownership for the owner’s grant and revoke +steps.

    +
    @@ -1335,13 +1729,18 @@

    Table of Contents

  • AsyncProfile.delete()
  • AsyncProfile.delete_feedback()
  • AsyncProfile.delete_profile()
  • +
  • AsyncProfile.disable()
  • +
  • AsyncProfile.enable()
  • AsyncProfile.explain_sql()
  • AsyncProfile.fetch()
  • AsyncProfile.generate()
  • AsyncProfile.generate_synthetic_data()
  • AsyncProfile.get_attributes()
  • +
  • AsyncProfile.grant_access()
  • AsyncProfile.list()
  • AsyncProfile.narrate()
  • +
  • AsyncProfile.qualified_name
  • +
  • AsyncProfile.revoke_access()
  • AsyncProfile.run_pipeline()
  • AsyncProfile.run_sql()
  • AsyncProfile.set_attribute()
  • @@ -1356,19 +1755,31 @@

    Table of Contents

  • 2.2. Async Profile creation
  • 2.3. Reuse Async Profile
  • -
  • 2.4. Update Async Profile
  • -
  • 2.5. Delete Async Profile
  • -
  • 2.6. Async explain SQL
  • -
  • 2.7. Async run SQL
  • -
  • 2.8. Async show SQL
  • -
  • 2.9. Async show prompt
  • -
  • 2.10. Async concurrent SQL
  • -
  • 2.11. Async chat
  • -
  • 2.12. Async streaming chat
  • -
  • 2.13. Summarize
  • -
  • 2.14. Translate
  • -
  • 2.15. Async pipeline
  • -
  • 2.16. List profiles asynchronously
  • +
  • 2.4. Enable and disable AsyncProfile
  • +
  • 2.5. Update Async Profile
  • +
  • 2.6. Delete Async Profile
  • +
  • 2.7. Async explain SQL
  • +
  • 2.8. Async run SQL
  • +
  • 2.9. Async show SQL
  • +
  • 2.10. Async show prompt
  • +
  • 2.11. Async concurrent SQL
  • +
  • 2.12. Async chat
  • +
  • 2.13. Async streaming chat
  • +
  • 2.14. Async request-level profile attribute overrides
  • +
  • 2.15. Summarize
  • +
  • 2.16. Translate
  • +
  • 2.17. Async pipeline +
  • +
  • 2.18. Share a profile asynchronously
  • +
  • 2.19. Give async feedback on generated SQL
  • +
  • 2.20. List profiles asynchronously +
  • @@ -1411,13 +1822,16 @@

    Navigation

  • index
  • +
  • + modules |
  • next |
  • previous |
  • -
  • Python logo
  • +
  • Python logo
  • Python »
  • @@ -1425,7 +1839,7 @@

    Navigation

  • - Select AI for Python 1.4.0 documentation » + Select AI for Python 1.5.0 documentation »
  • @@ -1470,7 +1884,7 @@

    Navigation


    - Created using Sphinx 8.2.3. + Created using Sphinx 9.1.0. diff --git a/docs/user_guide/cli.html b/docs/user_guide/cli.html index 9a6d8d5..2269781 100644 --- a/docs/user_guide/cli.html +++ b/docs/user_guide/cli.html @@ -5,21 +5,21 @@ - 1. Command line interface — Select AI for Python 1.4.0 documentation + 1. Command line interface — Select AI for Python 1.5.0 documentation + - - + - - - + + + - + @@ -68,7 +68,25 @@

    Table of Contents

  • 1.2. Interactive chat
  • 1.3. SQL commands
  • 1.4. Profile commands
  • -
  • 1.5. Command summary
  • +
  • 1.5. A2A commands +
  • +
  • 1.6. Command summary
  • @@ -76,8 +94,8 @@

    Table of Contents

    Previous topic

    -

    1. AsyncTool

    +

    1. Agent2Agent (A2A)

    Next topic

    @@ -111,13 +129,16 @@

    Navigation

  • index
  • +
  • + modules |
  • next |
  • - previous |
  • -
  • Python logo
  • +
  • Python logo
  • Python »
  • @@ -125,7 +146,7 @@

    Navigation

  • - Select AI for Python 1.4.0 documentation » + Select AI for Python 1.5.0 documentation »
  • @@ -169,28 +190,39 @@

    Navigation

    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 +server, clustered gateway and worker, and Agent Card generator. RAG is +supported when the selected profile is already configured with a vector +index. The A2A guide explains the difference between fixed and +dynamic connections and standalone and clustered topologies; this page gives +the corresponding CLI commands.

    Select AI CLI demo

    The package provides an optional select-ai command line tool. Install the -CLI extra to use it:

    -
    pip install 'select_ai[cli]'
    +CLI extra to use profile, SQL, and chat commands. Install the separate a2a
    +extra for the A2A server and worker commands. If A2A dependencies are missing,
    +invoking the A2A command group or a server or worker command reports how to
    +install them; the other CLI commands remain available:

    +
    pip install 'select_ai[cli]'
    +# Install A2A support:
    +pip install 'select_ai[a2a]'
     

    Use select-ai --help to view the available command groups and select-ai <command> --help to view options for a specific command.

    -

    Set the database connection details as environment variables, or pass them as -command line options:

    -
    export SELECT_AI_USER=<db_user>
    -export SELECT_AI_PASSWORD=<db_password>
    -export SELECT_AI_DB_CONNECT_STRING=<db_connect_string>
    -
    -
    +

    The A2A examples below pass database credentials with CLI options so the +commands are easy to copy and adapt. Environment-variable equivalents are +listed in the connection-options table.

    1.1. Connection options

    -

    All CLI commands accept the same database connection options:

    +

    Database-backed commands accept the following connection options. The +select-ai a2a serve command accepts these options directly. In standalone +mode, providing all four connection values at startup selects the fixed +connection path; omitting values opens a dynamic A2UI form for them. In +clustered mode, the gateway can preconfigure any values and the form collects +the rest. The worker receives the resolved values when it opens a session. +For a private database CA, use --database-tls-ca-file; the CLI reads PEM +from the specified file.

    Select AI Async Agent Tools
    @@ -218,11 +250,19 @@

    1.1. Connection options

    + + +
    Connection options

    --wallet-password

    SELECT_AI_WALLET_PASSWORD

    --database-tls-ca-file

    SELECT_AI_DATABASE_TLS_CA_FILE

    -

    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.

    +

    Profile, SQL, and chat commands prompt for the database password when neither +--password nor SELECT_AI_PASSWORD is set. For a2a serve, omitting +any connection value selects dynamic mode, and the A2A client supplies the +missing values through an A2UI form. Supply --dsn, --user, +--password, and --team for fixed standalone mode. Wallet options apply +to fixed standalone A2A connections; dynamic sessions do not currently +support Oracle wallet authentication. The A2A CLI examples below include a +host, port, service name, credentials, and optional database CA file.

    1.2. Interactive chat

    @@ -230,7 +270,7 @@

    1.2. Interactive chat

    1.3. SQL commands

    SQL operations are one-shot subcommands instead of a REPL:

    -
    select-ai sql show --profile OCI_AI_PROFILE "count movies by genre"
    -select-ai sql run --profile OCI_AI_PROFILE "count movies by genre"
    -select-ai sql explain --profile OCI_AI_PROFILE "count movies by genre"
    -select-ai sql narrate --profile OCI_AI_PROFILE "count movies by genre"
    +
    select-ai sql show --profile OCI_AI_PROFILE "count movies by genre"
    +select-ai sql run --profile OCI_AI_PROFILE "count movies by genre"
    +select-ai sql explain --profile OCI_AI_PROFILE "count movies by genre"
    +select-ai sql narrate --profile OCI_AI_PROFILE "count movies by genre"
     

    show, explain, and narrate stream text output by default. Use @@ -298,24 +338,24 @@

    1.3. SQL commands

    1.4. Profile commands

    Summarize and translate are available under the profile command group:

    -
    select-ai profile list
    -select-ai profile list --pattern "OCI.*"
    +
    select-ai profile list
    +select-ai profile list --pattern "OCI.*"
     
    -select-ai profile summarize --profile OCI_AI_PROFILE "Text to summarize"
    -select-ai profile summarize --profile OCI_AI_PROFILE --file notes.txt
    -select-ai profile summarize \
    +select-ai profile summarize --profile OCI_AI_PROFILE "Text to summarize"
    +select-ai profile summarize --profile OCI_AI_PROFILE --file notes.txt
    +select-ai profile summarize \
         --profile OCI_AI_PROFILE \
         --location-uri https://example.com/article.txt
    -select-ai profile summarize \
    +select-ai profile summarize \
         --profile OCI_AI_PROFILE \
         --location-uri https://objectstorage.example.com/n/namespace/b/bucket/o/file.txt \
         --credential-name OBJECT_STORE_CRED
     
    -select-ai profile translate \
    +select-ai profile translate \
         --profile OCI_AI_PROFILE \
         --source-language English \
         --target-language German \
    -    "Thank you"
    +    "Thank you"
     

    profile list prints profile names visible to the connected database user. @@ -325,10 +365,262 @@

    1.4. Profile commands--credential-name when the location URI requires an object storage credential.

    -
    -

    1.5. Command summary

    +
    +

    1.5. 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:

    +
    select-ai a2a --help
    +select-ai a2a serve --help
    +select-ai a2a worker --help
    +select-ai a2a agent-card --help
    +
    +
    - + ++++ + + + + + + + + + + + + + + + + +
    CLI command summaryA2A CLI commands

    Command

    Purpose

    select-ai a2a serve

    Start the public A2A HTTP server. The default standalone topology needs +no worker or Consul. Add --deployment clustered to use a gateway, +workers, and Consul.

    select-ai a2a worker

    Start an internal database worker for clustered topology. Standalone +topology does not use this command.

    select-ai a2a agent-card

    Print a Gemini Enterprise-compatible A2A v0.3 Agent Card without +starting a server.

    +
    +

    1.5.1. A2A deployment examples

    +

    The A2A guide explains how fixed and dynamic connections differ +from standalone and clustered topologies. These examples use CLI options for +the database settings. Replace the placeholder credentials and connection +string with your values.

    +
    +

    1.5.1.1. Fixed standalone connection

    +

    Pass all four connection values at startup to use the standalone server’s +shared connection pool. This needs no Consul or worker and supports streaming:

    +
    select-ai a2a serve \
    +    --deployment standalone \
    +    --team ORACLE_AI_DATABASE_AGENT \
    +    --user DB_USERNAME \
    +    --password 'DB_PASSWORD' \
    +    --dsn 'dbhost.example.com:1521/db_service' \
    +    --database-tls-ca-file /run/secrets/database-ca.pem \
    +    --database-call-timeout-seconds 120 \
    +    --host 0.0.0.0 \
    +    --port 8000 \
    +    --public-url https://a2a.example.com
    +
    +
    +

    The database CA file is optional when the server certificate is already +trusted by the operating system. --wallet-location and +--wallet-password are also available for fixed standalone connections +that use an Oracle wallet.

    +Select AI A2A server CLI demo + +
    +
    +

    1.5.1.2. Dynamic standalone connection

    +

    Omit one or more of --dsn, --user, --password, and --team to +have the A2A client supply those values through an A2UI form. The server runs +the session child processes itself, so this mode needs no Consul or separate +worker. Run a single Uvicorn process. Dynamic standalone supports task polling, +not streaming:

    +
    select-ai a2a serve \
    +    --deployment standalone \
    +    --database-tls-ca-file /run/secrets/database-ca.pem \
    +    --host 0.0.0.0 \
    +    --port 8000 \
    +    --public-url https://a2a.example.com
    +
    +
    +

    Remove --database-tls-ca-file when the database server certificate is +already trusted by the operating system. Any connection values provided on +the command line are fixed; the form asks only for the values that are +missing.

    +
    +
    +

    1.5.1.3. Clustered gateway and worker

    +

    Clustered topology requires Consul and at least one worker. Consul is the +worker directory and session-routing service: it helps the gateway find a +healthy worker for a new session and send later requests back to that worker. +See Why clustered deployments use Consul for a worked +example. Point the gateway and all workers at the same Consul service with +--consul-url.

    +

    The gateway can run without database credentials; by default, the A2A client supplies them +through the connection form. For local development, start Consul and then run +the worker and gateway in separate terminals:

    +
    # Terminal 1: local development Consul agent
    +consul agent -dev -bind=127.0.0.1 -client=127.0.0.1
    +
    +# Terminal 2: database worker
    +select-ai a2a worker \
    +    --host 127.0.0.1 \
    +    --port 8081 \
    +    --consul-url http://127.0.0.1:8500 \
    +    --worker-id local-worker \
    +    --worker-endpoint http://127.0.0.1:8081 \
    +    --database-call-timeout-seconds 120
    +
    +# Terminal 3: public gateway
    +select-ai a2a serve \
    +    --deployment clustered \
    +    --host 127.0.0.1 \
    +    --port 8000 \
    +    --public-url http://127.0.0.1:8000 \
    +    --consul-url http://127.0.0.1:8500 \
    +    --database-tls-ca-file /run/secrets/database-ca.pem \
    +    --database-call-timeout-seconds 120
    +
    +
    +

    The local Consul command is for development only. In a deployment, point the +gateway and workers at the existing Consul service and use network-reachable +worker addresses. The database CA option is needed only for a private database +CA; the gateway passes its PEM contents to the worker when opening a session. +Set the same --database-call-timeout-seconds value on the gateway and +worker. See the A2A guide for Consul setup and clustered deployment details.

    +

    Important serve options include --team, --host, --port, +--public-url, --description, --pool-max-size, and +--database-call-timeout-seconds. The default database call timeout is 120 +seconds. On the clustered gateway, this value sets the request deadline basis; +the worker applies it to Oracle calls and derives its child-process deadline.

    +

    By default, a2a serve does not require an application OAuth token. A2A +task and context records use a shared owner scope, and each A2A context maps +to its own Select AI conversation. Add --require-oauth to require +Authorization: Bearer ... and derive an owner scope as well as context. +The SDK does not validate token signatures or claims. A trusted upstream +service must validate the end-user token and prevent direct access that +bypasses validation. See the A2A authentication contract. +The Agent Card advertises bearer security only in that mode.

    +
    +
    +

    1.5.1.4. Clustered option reference

    +

    Configure discovery first, then connection values and session limits. Explicit +CLI values take precedence over environment fallbacks. For a fixed clustered +connection, see the complete gateway command.

    +
    +
    1.5.1.4.1. Gateway discovery
    +
    +
    --public-url (PUBLIC_URL)

    Client-visible base URL advertised in the Agent Card.

    +
    +
    --consul-url (CONSUL_HTTP_URL)

    Consul HTTP API URL. Use the same Consul service as the workers.

    +
    +
    --worker-service (WORKER_SERVICE)

    Consul service to discover. Default: select-ai-a2a-worker.

    +
    +
    +
    +
    +
    1.5.1.4.2. Worker registration
    +
    +
    --host and --port

    Worker listen address and port. The port is also registered with Consul.

    +
    +
    --worker-id (WORKER_ID)

    Unique worker identifier. Defaults to the host name.

    +
    +
    --consul-url (CONSUL_HTTP_URL)

    Consul HTTP API URL used to register the worker and send heartbeats.

    +
    +
    --worker-endpoint (WORKER_ENDPOINT)

    Worker URL reachable by the gateway. In cluster manifests, +WORKER_ADDRESS can supply the pod IP when no endpoint is given.

    +
    +
    +
    +
    +
    1.5.1.4.3. Connection values and forms
    +
    +
    --dsn, --user, --password, and --team (gateway)

    Fix the supplied values at startup. Set all four for a fixed connection; +otherwise the A2UI form asks only for the missing values. The environment +fallbacks are SELECT_AI_DB_CONNECT_STRING, SELECT_AI_USER, +SELECT_AI_PASSWORD, and SELECT_AI_A2A_TEAM.

    +
    +
    --a2ui-form (gateway)

    Path to a custom A2UI JSON form. It must submit exactly the missing +connection values. See A2A Integration for the form contract.

    +
    +
    +
    +
    +
    1.5.1.4.4. Session lifetime and timeouts
    +
    +
    --session-ttl-seconds (gateway and worker)

    Lifetime of a temporary database session; default 900 seconds. +The gateway also reads SESSION_TTL_SECONDS. Configure the same lifetime +on both processes.

    +
    +
    --conversation-retention-days (worker)

    Days to retain Oracle conversations; default 7. Environment fallback: +CONVERSATION_RETENTION_DAYS. Use 0 to retain until manually deleted.

    +
    +
    --session-start-timeout-seconds (worker)

    Maximum wait for a child process to initialize its database session.

    +
    +
    --database-call-timeout-seconds (gateway and worker)

    Database call timeout; default 120 seconds. Set the same value on both +processes. The worker applies it to Oracle calls; both processes derive +their request deadlines from it.

    +
    +
    +
    +
    +
    1.5.1.4.5. Authentication and TLS
    +
    +
    --require-oauth (gateway)

    Require a bearer token and derive an owner scope for task and context +records. Token validation must occur upstream. Uses the same ownership +behavior as standalone mode.

    +
    +
    --tls-cert-file and --tls-key-file (gateway)

    Enable HTTPS on the public A2A listener. Supply both together; without +them, the listener uses HTTP.

    +
    +
    --tls-ca-file (gateway)

    Optional CA that makes the public listener require and verify client +certificates (mTLS). Requires --tls-cert-file and --tls-key-file.

    +
    +
    --worker-tls-ca-file, --worker-tls-cert-file, --worker-tls-key-file

    Gateway TLS options for worker connections. The CA verifies the worker’s +server certificate; it can be supplied alone for HTTPS without mTLS. If +omitted, Requests uses its default CA bundle. The client certificate and +key are optional, but must be supplied together; the worker must require +and trust them for mTLS. When any --worker-tls-* option is used, set +--worker-endpoint to an HTTPS URL. +Environment fallbacks are WORKER_TLS_CA_FILE, +WORKER_TLS_CERT_FILE, and WORKER_TLS_KEY_FILE.

    +
    +
    --tls-ca-file, --tls-cert-file, --tls-key-file (worker)

    The certificate and key enable HTTPS on the worker and must be supplied +together. --tls-ca-file is optional; when supplied, the worker +requires and verifies client certificates for mTLS. Set +--worker-endpoint to an HTTPS URL so the gateway connects over TLS.

    +
    +
    --database-tls-ca-file (gateway)

    CA for verifying the Oracle database certificate. The gateway passes the +PEM contents to the worker session. This is separate from the certificates +for the gateway-to-worker HTTP connection and from Oracle wallet mTLS.

    +
    +
    +
    +
    +
    +
    +

    1.5.2. Agent Card generation

    +

    Print the portable A2A v0.3 card for a standalone public service:

    +
    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 A2A Integration for protocol +compatibility, persistent state, gateway session behavior, wallet support, and +Google Cloud deployment details.

    +
    +
    +
    +

    1.6. Command summary

    + +@@ -363,6 +655,18 @@

    1.5. Command summary

    + + + + + + + + +
    CLI command summary

    select-ai profile translate

    Translate text with a saved profile.

    select-ai a2a serve

    Start the public A2A server. The default is standalone; use +--deployment clustered for a gateway and workers. Omitted +connection values are collected through A2UI.

    select-ai a2a worker

    Start a database worker for clustered topology. Standalone deployments +do not use a worker.

    select-ai a2a agent-card

    Print an A2A v0.3 Agent Card.

    @@ -383,7 +687,25 @@

    Table of Contents

  • 1.2. Interactive chat
  • 1.3. SQL commands
  • 1.4. Profile commands
  • -
  • 1.5. Command summary
  • +
  • 1.5. A2A commands +
  • +
  • 1.6. Command summary
  • @@ -391,8 +713,8 @@

    Table of Contents

    Previous topic

    -

    1. AsyncTool

    +

    1. Agent2Agent (A2A)

    Next topic

    @@ -426,13 +748,16 @@

    Navigation

  • index
  • +
  • + modules |
  • next |
  • - previous |
  • -
  • Python logo
  • +
  • Python logo
  • Python »
  • @@ -440,7 +765,7 @@

    Navigation

  • - Select AI for Python 1.4.0 documentation » + Select AI for Python 1.5.0 documentation »
  • @@ -485,7 +810,7 @@

    Navigation


    - Created using Sphinx 8.2.3. + Created using Sphinx 9.1.0.
    diff --git a/docs/user_guide/compatibility.html b/docs/user_guide/compatibility.html new file mode 100644 index 0000000..fb4ff37 --- /dev/null +++ b/docs/user_guide/compatibility.html @@ -0,0 +1,344 @@ + + + + + + + + 5. Compatibility and requirements — Select AI for Python 1.5.0 documentation + + + + + + + + + + + + + + + + + + + + + +
    + + + +
    + + + +
    +
    +
    +
    + +
    +

    5. Compatibility and requirements

    +

    Select AI for Python 1.5.0 supports Python 3.11, 3.12, 3.13, and 3.14. +Installation requires oracledb>=2.0.0 and pandas==3.0.3. Some optional +features require driver APIs introduced after the minimum installable driver +version; upgrade oracledb if those APIs are unavailable.

    +

    Installing the SDK does not install database PL/SQL packages or enable their +features. Availability depends on the Oracle Database service, version, and +package updates. Consult Oracle’s Select AI and Select AI Agent capability +matrix +for your environment before choosing a workflow.

    + + +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Feature requirements

    Feature

    Python installation

    Database and deployment requirements

    Profiles, SQL generation, and chat

    select_ai

    DBMS_CLOUD_AI and DBMS_CLOUD; provider credential and outbound +database network access. SQL execution also requires object privileges.

    Async APIs

    select_ai with an async-capable oracledb driver

    Thin mode; use an async connection or async pool. Use a pool for +independent concurrent database operations.

    Pipelines

    oracledb>=2.4.0 with create_pipeline() and +AsyncConnection.run_pipeline()

    True pipelining requires Oracle AI Database 26ai or later. Older +databases execute individually. See Pipeline compatibility and results.

    RAG vector indexes

    select_ai

    Vector-index support in DBMS_CLOUD_AI, DBMS_CLOUD_PIPELINE, an +embedding model, source access, and database storage capacity.

    Agent teams and history

    select_ai

    DBMS_CLOUD_AI_AGENT and the corresponding agent history views.

    Profile, index, team, and credential sharing

    select_ai

    Database packages providing the relevant grant/revoke APIs. The +recipient still needs privileges on workflow dependencies.

    CLI profile, SQL, and chat commands

    select_ai[cli]

    Same requirements as the corresponding SDK operation.

    A2A fixed standalone

    select_ai[a2a]

    Existing agent team; A2A storage privileges and quota. Supports +streaming and wallet authentication.

    A2A dynamic standalone

    select_ai[a2a]

    One Uvicorn process; A2UI-capable client. Polling supported; streaming +and Oracle wallet authentication are unavailable.

    A2A clustered

    select_ai[a2a] on gateway and workers

    Consul and at least one worker. Polling supported; streaming and +Oracle wallet authentication are unavailable.

    +

    The OCI credential-loading examples also require oci and a configured +OCI API-signing key. This is a sample dependency, rather than an SDK runtime +requirement. See Credential.

    +

    Provider models, regions, and account permissions are independent of Python +package compatibility. Replace example model identifiers and storage URIs +with values supported by your provider account and database integration.

    +
    + + +
    +
    +
    +
    + +
    +
    + + + + + diff --git a/docs/user_guide/concurrent_prompt_processing.html b/docs/user_guide/concurrent_prompt_processing.html index ff80ec6..d9d04cd 100644 --- a/docs/user_guide/concurrent_prompt_processing.html +++ b/docs/user_guide/concurrent_prompt_processing.html @@ -5,19 +5,20 @@ - 1. Concurrent prompt processing — Select AI for Python 1.4.0 documentation + 1. Concurrent prompt processing — Select AI for Python 1.5.0 documentation + - - + - - - + + + + @@ -83,6 +84,11 @@

    Previous topic

    1. Using select_ai with Python web frameworks

    +

    This Page

    @@ -144,7 +163,7 @@

    Navigation

  • previous |
  • -
  • Python logo
  • +
  • Python logo
  • Python »
  • @@ -152,7 +171,7 @@

    Navigation

  • - Select AI for Python 1.4.0 documentation » + Select AI for Python 1.5.0 documentation »
  • @@ -209,7 +228,8 @@

    Navigation

    1. Conversation Object model

    -Select AI Conversation +Select AI Conversation object model +
    @@ -246,8 +266,8 @@

    2.
    import datetime
     
     attributes = select_ai.ConversationAttributes(
    -    title="Sales analysis",
    -    description="Follow-up questions about quarterly sales",
    +    title="Sales analysis",
    +    description="Follow-up questions about quarterly sales",
         retention_days=datetime.timedelta(days=14),
         conversation_length=20,
     )
    @@ -255,7 +275,7 @@ 

    2.
    -class select_ai.ConversationAttributes(title: str | None = 'New Conversation', description: str | None = None, retention_days: timedelta | None = datetime.timedelta(days=7), conversation_length: int | None = 10)
    +class select_ai.ConversationAttributes(title: str | None = 'New Conversation', description: str | None = None, retention_days: timedelta | None = datetime.timedelta(days=7), conversation_length: int | None = 10)

    Conversation Attributes

    Parameters:
    @@ -278,7 +298,7 @@

    2. 3. Conversation API

    -class select_ai.Conversation(conversation_id: str | None = None, attributes: ConversationAttributes | None = None)
    +class select_ai.Conversation(conversation_id: str | None = None, attributes: ConversationAttributes | None = None)

    Conversation class can be used to create, update and delete conversations in the database

    Typical usage is to combine this conversation object with an AI @@ -292,6 +312,20 @@

    3. +
    +add_tag(tag_key: str, tag_value: str) → None
    +

    Add or update a tag on this conversation.

    +
    +
    Parameters:
    +
      +
    • tag_key (str) – Tag name.

    • +
    • tag_value (str) – Value associated with the tag name.

    • +
    +
    +
    +

    +
    create() → str
    @@ -310,9 +344,23 @@

    3. +
    +delete_prompt(conversation_prompt_id: str, force: bool = False) → None
    +

    Delete a stored prompt from this conversation.

    +
    +
    Parameters:
    +
      +
    • conversation_prompt_id (str) – Identifier of the prompt to delete.

    • +
    • force (bool) – Ignore a missing prompt when True.

    • +
    +
    +
    +

    +
    -classmethod fetch(conversation_id: str) → Conversation
    +classmethod fetch(conversation_id: str) → Conversation

    Fetch conversation attributes from the database and build a proxy object

    @@ -330,7 +378,7 @@

    3.
    -classmethod list() → Iterator[Conversation]
    +classmethod list() → Iterator[Conversation]

    List all conversations

    Returns:
    @@ -339,6 +387,31 @@

    3. +
    +list_prompts() → Iterator[ConversationPrompt]
    +

    List the prompts and responses stored for this conversation.

    +
    +
    Returns:
    +

    Prompts in creation order.

    +
    +
    +

    + +
    +
    +remove_tag(tag_key: str, force: bool = False) → None
    +

    Remove a tag from this conversation.

    +
    +
    Parameters:
    +
      +
    • tag_key (str) – Tag name to remove.

    • +
    • force (bool) – Ignore a missing tag when True.

    • +
    +
    +
    +
    +
    set_attributes(attributes: ConversationAttributes)
    @@ -372,6 +445,18 @@

    3.

    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.

    @@ -390,19 +475,19 @@

    3.1. Create conversationimport select_ai -user = os.getenv("SELECT_AI_USER") -password = os.getenv("SELECT_AI_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +user = os.getenv("SELECT_AI_USER") +password = os.getenv("SELECT_AI_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") select_ai.connect(user=user, password=password, dsn=dsn) conversation_attributes = select_ai.ConversationAttributes( - title="History of Science", - description="LLM's understanding of history of science", + title="History of Science", + description="LLM's understanding of history of science", ) conversation = select_ai.Conversation(attributes=conversation_attributes) conversation_id = conversation.create() -print("Created conversation with conversation id: ", conversation_id) +print("Created conversation with conversation id: ", conversation_id)

    output:

    @@ -419,28 +504,28 @@

    3.2. Chat sessionimport select_ai -user = os.getenv("SELECT_AI_USER") -password = os.getenv("SELECT_AI_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +user = os.getenv("SELECT_AI_USER") +password = os.getenv("SELECT_AI_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") select_ai.connect(user=user, password=password, dsn=dsn) -profile = select_ai.Profile(profile_name="oci_ai_profile") +profile = select_ai.Profile(profile_name="oci_ai_profile") conversation_attributes = select_ai.ConversationAttributes( - title="History of Science", - description="LLM's understanding of history of science", + title="History of Science", + description="LLM's understanding of history of science", ) conversation = select_ai.Conversation(attributes=conversation_attributes) with profile.chat_session(conversation=conversation, delete=True) as session: print( - "Conversation ID for this session is:", + "Conversation ID for this session is:", conversation.conversation_id, ) response = session.chat( - prompt="What is importance of history of science ?" + prompt="What is importance of history of science ?" ) print(response) response = session.chat( - prompt="Elaborate more on 'Learning from past mistakes'" + prompt="Elaborate more on 'Learning from past mistakes'" ) print(response) @@ -461,17 +546,89 @@

    3.2. Chat session

    +
    +

    3.3. 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.

    +
    +
    +class select_ai.ConversationPrompt(conversation_prompt_id: str, conversation_id: str, conversation_title: str, profile_name: str | None, prompt_action: str | None, prompt: str | None, prompt_response: str | None, created: datetime | None, modified: datetime | None, client_identifier: str | None, client_ip: str | None, sid: int | None, serial: int | None)
    +

    A prompt and response stored in a conversation’s history.

    +
    + +
    conversation.add_tag("PROJECT", "SELECT_AI")
    +prompts = list(conversation.list_prompts())
    +for prompt in prompts:
    +    print(prompt.conversation_prompt_id, prompt.prompt)
    +if prompts:
    +    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:

    +
    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)
    +
    +conversation = select_ai.Conversation(
    +    attributes=select_ai.ConversationAttributes(title="Prompt and tag sample")
    +)
    +conversation.create()
    +profile = select_ai.Profile(profile_name=profile_name)
    +
    +try:
    +    conversation.add_tag("PROJECT", "SELECT_AI")
    +    conversation.add_tag("PROJECT", "SELECT_AI_SAMPLES")  # Updates the tag.
    +    with profile.chat_session(conversation=conversation) as session:
    +        print(session.chat("Reply with exactly: conversation history works."))
    +
    +    prompts = list(conversation.list_prompts())
    +    for prompt in prompts:
    +        print(f"Prompt {prompt.conversation_prompt_id}: {prompt.prompt}")
    +
    +    if prompts:
    +        conversation.delete_prompt(prompts[-1].conversation_prompt_id)
    +    print("Prompts after deletion:", list(conversation.list_prompts()))
    +    conversation.remove_tag("PROJECT")
    +finally:
    +    conversation.delete(force=True)
    +
    +
    +

    The conversation ID, prompt ID, and model response vary between runs. +Example output (generated IDs and model responses vary):

    +

    output:

    +
    conversation history works.
    +Prompt <generated prompt id>: Reply with exactly: conversation history works.
    +Prompts after deletion: []
    +
    +
    +
    -

    3.3. List conversations

    +

    3.4. List conversations

    Listing returns Conversation objects with their conversation_id and metadata. It does not replay the conversation transcript.

    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")
    +user = os.getenv("SELECT_AI_USER")
    +password = os.getenv("SELECT_AI_PASSWORD")
    +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING")
     
     select_ai.connect(user=user, password=password, dsn=dsn)
     for conversation in select_ai.Conversation().list():
    @@ -481,15 +638,15 @@ 

    3.3. List conversations
    5275A80-A290-DA17-E063-151B000AD3B4
    -ConversationAttributes(title='History of Science', description="LLM's understanding of history of science", retention_days=7)
    +ConversationAttributes(title='History of Science', description="LLM's understanding of history of science", retention_days=7)
     
     37DF777F-F3DA-F084-E063-D81A000A53BE
    -ConversationAttributes(title='History of Science', description="LLM's understanding of history of science", retention_days=7)
    +ConversationAttributes(title='History of Science', description="LLM's understanding of history of science", retention_days=7)
     

    -

    3.4. Delete conversation

    +

    3.5. Delete conversation

    Delete conversations that are no longer needed, especially when retention_days is set to 0 or when the content should not remain in the database after a session ends. For temporary sessions, prefer @@ -498,17 +655,17 @@

    3.4. Delete conversationimport select_ai -user = os.getenv("SELECT_AI_USER") -password = os.getenv("SELECT_AI_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +user = os.getenv("SELECT_AI_USER") +password = os.getenv("SELECT_AI_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") select_ai.connect(user=user, password=password, dsn=dsn) conversation = select_ai.Conversation( - conversation_id="37DDC22E-11C8-3D49-E063-D81A000A85FE" + conversation_id="37DDC22E-11C8-3D49-E063-D81A000A85FE" ) conversation.delete(force=True) print( - "Deleted conversation with conversation id: ", + "Deleted conversation with conversation id: ", conversation.conversation_id, ) @@ -523,7 +680,7 @@

    3.4. Delete conversation4. AsyncConversation API

    -class select_ai.AsyncConversation(conversation_id: str | None = None, attributes: ConversationAttributes | None = None)
    +class select_ai.AsyncConversation(conversation_id: str | None = None, attributes: ConversationAttributes | None = None)

    AsyncConversation class can be used to create, update and delete conversations in the database in an async manner

    Typical usage is to combine this conversation object with an @@ -536,9 +693,23 @@

    4. +
    +async add_tag(tag_key: str, tag_value: str) → None
    +

    Add or update a tag on this conversation.

    +
    +
    Parameters:
    +
      +
    • tag_key (str) – Tag name.

    • +
    • tag_value (str) – Value associated with the tag name.

    • +
    +
    +
    +

    +
    -async create() → str
    +async create() → str

    Creates a new conversation and returns the conversation_id to be used in context-aware conversations with LLMs

    @@ -550,25 +721,39 @@

    4.
    -async delete(force: bool = False)
    +async delete(force: bool = False)

    Delete the conversation

    +
    +
    +async delete_prompt(conversation_prompt_id: str, force: bool = False) → None
    +

    Delete a stored prompt from this conversation.

    +
    +
    Parameters:
    +
      +
    • conversation_prompt_id (str) – Identifier of the prompt to delete.

    • +
    • force (bool) – Ignore a missing prompt when True.

    • +
    +
    +
    +
    +
    -async classmethod fetch(conversation_id: str) → AsyncConversation
    +async classmethod fetch(conversation_id: str) → AsyncConversation

    Fetch conversation attributes from the database

    -async get_attributes() → ConversationAttributes
    +async get_attributes() → ConversationAttributes

    Get attributes of the conversation from the database

    -classmethod list() → AsyncGenerator[AsyncConversation, None]
    +classmethod list() → AsyncGenerator[AsyncConversation, None]

    List all conversations

    Returns:
    @@ -577,9 +762,34 @@

    4. +
    +async list_prompts() → AsyncGenerator[ConversationPrompt, None]
    +

    List the prompts and responses stored for this conversation.

    +
    +
    Returns:
    +

    Prompts in creation order.

    +
    +
    +

    + +
    +
    +async remove_tag(tag_key: str, force: bool = False) → None
    +

    Remove a tag from this conversation.

    +
    +
    Parameters:
    +
      +
    • tag_key (str) – Tag name to remove.

    • +
    • force (bool) – Ignore a missing tag when True.

    • +
    +
    +
    +
    +
    -async set_attributes(attributes: ConversationAttributes)
    +async set_attributes(attributes: ConversationAttributes)

    Updates the attributes of the conversation

    @@ -612,6 +822,18 @@

    4.

    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(...)

    @@ -626,19 +848,19 @@

    4.1. Async chat sessionimport select_ai -user = os.getenv("SELECT_AI_USER") -password = os.getenv("SELECT_AI_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +user = os.getenv("SELECT_AI_USER") +password = os.getenv("SELECT_AI_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") 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="async_oci_ai_profile" ) conversation_attributes = select_ai.ConversationAttributes( - title="History of Science", - description="LLM's understanding of history of science", + title="History of Science", + description="LLM's understanding of history of science", ) async_conversation = select_ai.AsyncConversation( attributes=conversation_attributes @@ -648,11 +870,11 @@

    4.1. Async chat sessionconversation=async_conversation, delete=True ) as async_session: response = await async_session.chat( - prompt="What is importance of history of science ?" + prompt="What is importance of history of science ?" ) print(response) response = await async_session.chat( - prompt="Elaborate more on 'Learning from past mistakes'" + prompt="Elaborate more on 'Learning from past mistakes'" ) print(response) @@ -676,17 +898,80 @@

    4.1. Async chat session +

    4.2. Async prompt history and tags

    +

    Use prompt history to inspect or remove stored prompts, and tags to attach +application metadata to a conversation. This example first creates a +conversation and records a chat prompt so there is history to inspect.

    +
    +

    4.2.1. Create a conversation and save a prompt

    +

    With an async connection open, run the following inside an async function. +The saved profile oci_ai_profile must already exist:

    +
    conversation = select_ai.AsyncConversation(
    +    attributes=select_ai.ConversationAttributes(title="Prompt history demo")
    +)
    +await conversation.create()
    +profile = await select_ai.AsyncProfile(profile_name="oci_ai_profile")
    +async with profile.chat_session(conversation=conversation) as session:
    +    response = await session.chat("Explain retrieval augmented generation.")
    +    print(response)
    +
    +
    +
    +
    +

    4.2.2. Tag and inspect the conversation

    +

    A second add_tag() call with the same key updates its value. +list_prompts() returns an async iterator of ConversationPrompt objects:

    +
    await conversation.add_tag("PROJECT", "SELECT_AI")
    +await conversation.add_tag("PROJECT", "SELECT_AI_SAMPLES")
    +prompts = [prompt async for prompt in conversation.list_prompts()]
    +for prompt in prompts:
    +    print(prompt.conversation_prompt_id, prompt.prompt)
    +
    +
    +
    +
    +

    4.2.3. Remove a stored prompt and its tag

    +

    Delete by prompt ID, then remove the tag by key. Check for an empty history +before choosing a prompt to delete:

    +
    if prompts:
    +    await conversation.delete_prompt(prompts[-1].conversation_prompt_id)
    +await conversation.remove_tag("PROJECT")
    +
    +
    +

    Use force=True on delete_prompt() or remove_tag() when a missing +prompt or tag should be ignored. When finished with this temporary conversation, +call await conversation.delete(force=True).

    +
    +
    +

    4.2.4. Run the complete example

    +

    The complete async prompt-history script +adds connection setup and a finally block that deletes the temporary +conversation. Set the common database environment variables and optionally +set SELECT_AI_PROFILE_NAME (default oci_ai_profile), then run from the +repository root:

    +
    python samples/async/conversation_prompts_tags.py
    +
    +
    +

    Example output (prompt IDs and model responses vary):

    +
    conversation history works.
    +Prompt <generated prompt id>: Reply with exactly: conversation history works.
    +Prompts after deletion: []
    +
    +
    +
    +

    -

    4.2. Async list conversations

    +

    4.3. Async list conversations

    AsyncConversation.list() is an async iterator.

    @@ -727,32 +1012,51 @@

    Table of Contents

  • 3. Conversation API
  • 4. AsyncConversation API
  • @@ -801,7 +1105,7 @@

    Navigation

  • previous |
  • -
  • Python logo
  • +
  • Python logo
  • Python »
  • @@ -809,7 +1113,7 @@

    Navigation

  • - Select AI for Python 1.4.0 documentation » + Select AI for Python 1.5.0 documentation »
  • @@ -854,7 +1158,7 @@

    Navigation


    - Created using Sphinx 8.2.3. + Created using Sphinx 9.1.0. diff --git a/docs/user_guide/credential.html b/docs/user_guide/credential.html index 89f64a1..96f51a3 100644 --- a/docs/user_guide/credential.html +++ b/docs/user_guide/credential.html @@ -5,20 +5,20 @@ - 1. Create credential — Select AI for Python 1.4.0 documentation + 1. Create credential — Select AI for Python 1.5.0 documentation + - - + - - - + + + - + @@ -64,15 +64,16 @@

    Table of Contents

    @@ -83,8 +84,8 @@

    Previous topic

    This Page

    @@ -114,12 +115,15 @@

    Navigation

    index
  • - modules |
  • +
  • + next |
  • previous |
  • -
  • Python logo
  • +
  • Python logo
  • Python »
  • @@ -127,7 +131,7 @@

    Navigation

  • - Select AI for Python 1.4.0 documentation » + Select AI for Python 1.5.0 documentation »
  • @@ -177,8 +181,8 @@

    Navigation

    tenancy_ocid, private_key, fingerprint, and comments.

    The following table shows AI providers and corresponding credential object formats.

    - - +
    AI provider and expected credential format
    +@@ -191,9 +195,9 @@

    Navigation

    - + + + + + - + - + - + - + + + + +
    AI provider and expected credential format

    Anthropic

    {
    -    "credential_name": "ANTHROPIC_CRED",
    -    "username": "anthropic",
    -    "password": "sk-ant-xxx",
    +    "credential_name": "ANTHROPIC_CRED",
    +    "username": "anthropic",
    +    "password": "sk-ant-xxx",
     }
     
    @@ -201,9 +205,9 @@

    Navigation

    AWS Bedrock

    {
    -    "credential_name": "AWS_BEDROCK_CRED",
    -    "username": "<aws_access_key_id>",
    -    "password": "<aws_secret_access_key>",
    +    "credential_name": "AWS_BEDROCK_CRED",
    +    "username": "<aws_access_key_id>",
    +    "password": "<aws_secret_access_key>",
     }
     
    @@ -211,9 +215,9 @@

    Navigation

    Azure OpenAI

    {
    -    "credential_name": "AZURE_OPENAI_CRED",
    -    "username": "azure",
    -    "password": "<azure_openai_api_key>",
    +    "credential_name": "AZURE_OPENAI_CRED",
    +    "username": "azure",
    +    "password": "<azure_openai_api_key>",
     }
     
    @@ -221,9 +225,9 @@

    Navigation

    Cohere

    {
    -    "credential_name": "COHERE_CRED",
    -    "username": "cohere",
    -    "password": "<cohere_api_key>",
    +    "credential_name": "COHERE_CRED",
    +    "username": "cohere",
    +    "password": "<cohere_api_key>",
     }
     
    @@ -231,9 +235,9 @@

    Navigation

    Google

    {
    -    "credential_name": "GOOGLE_CRED",
    -    "username": "google",
    -    "password": "<google_api_key>",
    +    "credential_name": "GOOGLE_CRED",
    +    "username": "google",
    +    "password": "<google_api_key>",
     }
     
    @@ -241,9 +245,9 @@

    Navigation

    HuggingFace

    {
    -    "credential_name": "HUGGINGFACE_CRED",
    -    "username": "hf",
    -    "password": "hf_xxx",
    +    "credential_name": "HUGGINGFACE_CRED",
    +    "username": "hf",
    +    "password": "hf_xxx",
     }
     
    @@ -251,11 +255,11 @@

    Navigation

    OCI Gen AI

    {
    -    "credential_name": "OCI_GENAI_CRED",
    -    "user_ocid": "<user_ocid>",
    -    "tenancy_ocid": "<tenancy_ocid>",
    -    "private_key": "<private_key_contents>",
    -    "fingerprint": "<fingerprint>",
    +    "credential_name": "OCI_GENAI_CRED",
    +    "user_ocid": "<user_ocid>",
    +    "tenancy_ocid": "<tenancy_ocid>",
    +    "private_key": "<private_key_contents>",
    +    "fingerprint": "<fingerprint>",
     }
     
    @@ -263,9 +267,9 @@

    Navigation

    OpenAI

    {
    -    "credential_name": "OPENAI_CRED",
    -    "username": "openai",
    -    "password": "sk-xxx",
    +    "credential_name": "OPENAI_CRED",
    +    "username": "openai",
    +    "password": "sk-xxx",
     }
     
    @@ -277,36 +281,59 @@

    Navigation

    1. Create credential

    In this example, we create a credential object to authenticate to OCI Gen AI service provider:

    +

    The synchronous and asynchronous samples below use the OCI Python SDK to read +an existing API-signing configuration. Install this sample-specific dependency:

    +
    python -m pip install oci
    +
    +
    +

    Configure the DEFAULT profile in ~/.oci/config with user, +tenancy, fingerprint, region, and key_file. The key_file +must point to a readable private key whose public key is registered with the +OCI user. See OCI SDK configuration. +The samples call oci.config.from_file() with its default path and profile; +edit that call if you use another configuration. Keep the configuration and +private key outside your source repository.

    +

    The oci package is needed only to load this configuration in the samples. +The Select AI credential API itself accepts the credential dictionary shown +above without requiring the OCI SDK.

    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.

    -
    -

    1.1. 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.

    +
    +

    1.1. Create Sync API

    import os
     
     import oci
     import select_ai
     
    -user = os.getenv("SELECT_AI_USER")
    -password = os.getenv("SELECT_AI_PASSWORD")
    -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING")
    +user = os.getenv("SELECT_AI_USER")
    +password = os.getenv("SELECT_AI_PASSWORD")
    +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING")
     
     select_ai.connect(user=user, password=password, dsn=dsn)
     
     # Default config file and profile
     default_config = oci.config.from_file()
     oci.config.validate_config(default_config)
    -with open(default_config["key_file"]) as fp:
    +with open(default_config["key_file"]) as fp:
         key_contents = fp.read()
     credential = {
    -    "credential_name": "my_oci_ai_profile_key",
    -    "user_ocid": default_config["user"],
    -    "tenancy_ocid": default_config["tenancy"],
    -    "private_key": key_contents,
    -    "fingerprint": default_config["fingerprint"],
    +    "credential_name": "my_oci_ai_profile_key",
    +    "user_ocid": default_config["user"],
    +    "tenancy_ocid": default_config["tenancy"],
    +    "private_key": key_contents,
    +    "fingerprint": default_config["fingerprint"],
     }
    -select_ai.create_credential(credential=credential, replace=True)
    -print("Created credential: ", credential["credential_name"])
    +select_ai.create_credential(
    +    credential=credential,
    +    replace=True,
    +    public_synonym=True,
    +)
    +print("Created credential: ", credential["credential_name"])
     

    output:

    @@ -314,36 +341,38 @@

    1.1. Sync API

    -
    -

    1.2. Async API

    +
    +

    1.2. Create Async API

    import asyncio
     import os
     
     import oci
     import select_ai
     
    -user = os.getenv("SELECT_AI_USER")
    -password = os.getenv("SELECT_AI_PASSWORD")
    -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING")
    +user = os.getenv("SELECT_AI_USER")
    +password = os.getenv("SELECT_AI_PASSWORD")
    +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING")
     
     
     async def main():
         await select_ai.async_connect(user=user, password=password, dsn=dsn)
         default_config = oci.config.from_file()
         oci.config.validate_config(default_config)
    -    with open(default_config["key_file"]) as fp:
    +    with open(default_config["key_file"]) as fp:
             key_contents = fp.read()
         credential = {
    -        "credential_name": "my_oci_ai_profile_key",
    -        "user_ocid": default_config["user"],
    -        "tenancy_ocid": default_config["tenancy"],
    -        "private_key": key_contents,
    -        "fingerprint": default_config["fingerprint"],
    +        "credential_name": "my_oci_ai_profile_key",
    +        "user_ocid": default_config["user"],
    +        "tenancy_ocid": default_config["tenancy"],
    +        "private_key": key_contents,
    +        "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"])
    +    print("Created credential: ", credential["credential_name"])
     
     
     asyncio.run(main())
    @@ -359,21 +388,24 @@ 

    1.2. Async API2. 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.

    -
    -

    2.1. Sync API

    +does not exist. Pass public_synonym=True to drop the credential’s public +synonym as well; this requires the DROP PUBLIC SYNONYM system privilege.

    +
    +

    2.1. Delete Sync API

    
     import select_ai
     
    -user = os.getenv("SELECT_AI_USER")
    -password = os.getenv("SELECT_AI_PASSWORD")
    -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING")
    +user = os.getenv("SELECT_AI_USER")
    +password = os.getenv("SELECT_AI_PASSWORD")
    +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING")
     
     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")
    +print("Deleted credential: my_oci_ai_profile_key")
     

    output:

    @@ -381,24 +413,26 @@

    2.1. Sync API

    -
    -

    2.2. Async API

    +
    +

    2.2. Delete Async API

    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")
    +user = os.getenv("SELECT_AI_USER")
    +password = os.getenv("SELECT_AI_PASSWORD")
    +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING")
     
     
     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")
    +    print("Deleted credential: my_oci_ai_profile_key")
     
     
     asyncio.run(main())
    @@ -409,6 +443,35 @@ 

    2.2. Async API

    +
    +
    +

    3. 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(...).

    +
    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:

    +
    await select_ai.async_grant_credential_access(
    +    "MY_PROVIDER_CREDENTIAL",
    +    "APP_USER",
    +)
    +await select_ai.async_revoke_credential_access(
    +    "MY_PROVIDER_CREDENTIAL",
    +    "APP_USER",
    +)
    +
    +
    @@ -422,15 +485,16 @@

    2.2. Async APITable of Contents

    @@ -441,8 +505,8 @@

    Previous topic

    This Page

    @@ -472,12 +536,15 @@

    Navigation

    index
  • - modules |
  • +
  • + next |
  • previous |
  • -
  • Python logo
  • +
  • Python logo
  • Python »
  • @@ -485,7 +552,7 @@

    Navigation

  • - Select AI for Python 1.4.0 documentation » + Select AI for Python 1.5.0 documentation »
  • @@ -530,7 +597,7 @@

    Navigation


    - Created using Sphinx 8.2.3. + Created using Sphinx 9.1.0.
    diff --git a/docs/user_guide/installation.html b/docs/user_guide/installation.html index f9c6edb..cbb9e3b 100644 --- a/docs/user_guide/installation.html +++ b/docs/user_guide/installation.html @@ -5,16 +5,16 @@ - 2. Installing select_ai — Select AI for Python 1.4.0 documentation + 2. Installing select_ai — Select AI for Python 1.5.0 documentation + - - + - - - + + + @@ -110,13 +110,16 @@

    Navigation

  • index
  • +
  • + modules |
  • next |
  • previous |
  • -
  • Python logo
  • +
  • Python logo
  • Python »
  • @@ -124,7 +127,7 @@

    Navigation

  • - Select AI for Python 1.4.0 documentation » + Select AI for Python 1.5.0 documentation »
  • @@ -205,10 +208,16 @@

    Navigation

  • If you want the optional command line interface, install the cli extra:

    -
    python -m pip install --upgrade "select_ai[cli]"
    +
    python -m pip install --upgrade "select_ai[cli]"
    +
    +
    +

    This installs the select-ai command with profile, SQL, and chat +commands. To use the A2A server and worker commands, install the separate +a2a extra:

    +
    python -m pip install --upgrade "select_ai[a2a]"
     
    -

    This installs the select-ai command. See Command Line Interface.

    +

    See Command Line Interface and A2A Integration.

  • If you are behind a proxy, use the --proxy option. For example:

  • @@ -253,6 +262,8 @@

    2.3. Connection smoke test to create a credential and profile and query sample data.

    2.4. Install documentation dependencies

    @@ -322,13 +333,16 @@

    Navigation

  • index
  • +
  • + modules |
  • next |
  • previous |
  • -
  • Python logo
  • +
  • Python logo
  • Python »
  • @@ -336,7 +350,7 @@

    Navigation

  • - Select AI for Python 1.4.0 documentation » + Select AI for Python 1.5.0 documentation »
  • @@ -381,7 +395,7 @@

    Navigation


    - Created using Sphinx 8.2.3. + Created using Sphinx 9.1.0. diff --git a/docs/user_guide/introduction.html b/docs/user_guide/introduction.html index b6786af..035fa03 100644 --- a/docs/user_guide/introduction.html +++ b/docs/user_guide/introduction.html @@ -5,16 +5,16 @@ - 1. Introduction to Select AI for Python — Select AI for Python 1.4.0 documentation + 1. Introduction to Select AI for Python — Select AI for Python 1.5.0 documentation + - - + - - - + + + @@ -110,13 +110,16 @@

    Navigation

  • index
  • +
  • + modules |
  • next |
  • previous |
  • -
  • Python logo
  • +
  • Python logo
  • Python »
  • @@ -124,7 +127,7 @@

    Navigation

  • - Select AI for Python 1.4.0 documentation » + Select AI for Python 1.5.0 documentation »
  • @@ -167,25 +170,38 @@

    Navigation

    Python ecosystem. It gives Python applications a higher-level API for working with AI providers, credentials, profiles, natural language prompts, vector indexes, conversations, summarization, synthetic data, and AI agent workflows.

    +
    +Select AI A2A server demo from the project README + +
    +

    Connect an A2A client to a Select AI agent team. Follow the +A2A guide to run the server.

    +
    +
    +

    For a complete setup-to-query example, start with Run your first SQL +query. Existing users can review What’s new in 1.5.0 and Compatibility.

    1.1. What you can build

    -

    Use select_ai to:

    +

    Start with the workflow you want to build:

      -
    • Ask questions about database objects in natural language and generate SQL.

    • -
    • Run generated SQL and return results as Python objects such as pandas data -frames.

    • -
    • Generate narrative answers, explanations, prompt previews, translations, and -summaries.

    • -
    • Use Retrieval Augmented Generation (RAG) with vector indexes over documents -and object storage content.

    • -
    • Create synthetic data for database tables.

    • -
    • Build context-aware chat sessions with database-backed conversations.

    • -
    • Register tools, tasks, agents, and teams for database-backed AI agent -workflows.

    • -
    • Use the optional select-ai command line interface for interactive chat -and SQL workflows.

    • -
    • Use synchronous APIs, asynchronous APIs, and connection pools in scripts, -services, and web applications.

    • +
    • Natural language data apps: generate SQL, return query results as pandas +DataFrames, or explain results in plain language with Profiles +and Async Profiles.

    • +
    • RAG and chat assistants: ground answers in your documents with +Vector Indexes and keep chat context with +Conversations.

    • +
    • AI agent workflows: compose tools, tasks, and agents into +Teams, then expose a team to other AI applications through +an Agent2Agent (A2A) server.

    • +
    • Python web services: add Select AI endpoints to FastAPI, Flask, or Django +using the pool lifecycle patterns in Web Frameworks.

    • +
    • Concurrent prompt processing: use threads, async tasks, queues, or a +database pipeline with the Concurrency Guides.

    • +
    • Content and test-data tools: summarize content, +translate text, or generate +Synthetic Data for database tables.

    • +
    • Terminal workflows: chat, generate SQL, and run an A2A server with the +optional Command Line Interface.

    @@ -216,28 +232,38 @@

    1.2. Core concepts

    Store provider and service secrets securely in Oracle Database.

    Credential

    Profile

    Sharing and ownership

    Share profiles, vector indexes, teams, and credentials with users or +roles and address objects owned by another schema.

    Sharing and ownership

    Profile

    Combine provider, credential, database object scope, and generation options into a reusable Select AI profile.

    Profile

    Actions

    Actions

    Choose what Select AI should do with a prompt, such as show SQL, run SQL, chat, narrate, summarize, or translate.

    Actions

    Conversation

    Conversation

    Keep prompt history for context-aware chat sessions.

    Conversation

    Vector index

    Vector index

    Index document content for RAG over trusted source material.

    Vector Index

    Agent

    Agent

    Define tools, tasks, agents, and teams for multi-step AI workflows.

    Agent

    Agent2Agent (A2A)

    Expose an agent team as a discoverable service for A2A clients. Choose +standalone or clustered deployment and fixed or dynamic connections.

    A2A server

    @@ -316,13 +342,16 @@

    Navigation

  • index
  • +
  • + modules |
  • next |
  • previous |
  • -
  • Python logo
  • +
  • Python logo
  • Python »
  • @@ -330,7 +359,7 @@

    Navigation

  • - Select AI for Python 1.4.0 documentation » + Select AI for Python 1.5.0 documentation »
  • @@ -375,7 +404,7 @@

    Navigation


    - Created using Sphinx 8.2.3. + Created using Sphinx 9.1.0.
    diff --git a/docs/user_guide/privileges.html b/docs/user_guide/privileges.html index 6155f2b..bf41b72 100644 --- a/docs/user_guide/privileges.html +++ b/docs/user_guide/privileges.html @@ -5,16 +5,16 @@ - 1. Grant privilege — Select AI for Python 1.4.0 documentation + 1. Grant privilege — Select AI for Python 1.5.0 documentation + - - + - - - + + + @@ -113,7 +113,7 @@

    Navigation

  • previous |
  • -
  • Python logo
  • +
  • Python logo
  • Python »
  • @@ -121,7 +121,7 @@

    Navigation

  • - Select AI for Python 1.4.0 documentation » + Select AI for Python 1.5.0 documentation »
  • @@ -196,15 +196,15 @@

    1. Grant privilegeimport select_ai -admin_user = os.getenv("SELECT_AI_ADMIN_USER") -password = os.getenv("SELECT_AI_ADMIN_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") -select_ai_user = os.getenv("SELECT_AI_USER") +admin_user = os.getenv("SELECT_AI_ADMIN_USER") +password = os.getenv("SELECT_AI_ADMIN_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +select_ai_user = os.getenv("SELECT_AI_USER") select_ai.connect(user=admin_user, password=password, dsn=dsn) select_ai.grant_privileges(users=select_ai_user) -print("Granted privileges to: ", select_ai_user) +print("Granted privileges to: ", select_ai_user)

    output:

    @@ -220,15 +220,15 @@

    2. Revoke privilegeimport select_ai -admin_user = os.getenv("SELECT_AI_ADMIN_USER") -password = os.getenv("SELECT_AI_ADMIN_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") -select_ai_user = os.getenv("SELECT_AI_USER") +admin_user = os.getenv("SELECT_AI_ADMIN_USER") +password = os.getenv("SELECT_AI_ADMIN_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +select_ai_user = os.getenv("SELECT_AI_USER") select_ai.connect(user=admin_user, password=password, dsn=dsn) select_ai.revoke_privileges(users=select_ai_user) -print("Revoked privileges from: ", select_ai_user) +print("Revoked privileges from: ", select_ai_user)

    output:

    @@ -254,20 +254,20 @@

    3. Grant network accessimport select_ai -admin_user = os.getenv("SELECT_AI_ADMIN_USER") -password = os.getenv("SELECT_AI_ADMIN_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") -select_ai_user = os.getenv("SELECT_AI_USER") +admin_user = os.getenv("SELECT_AI_ADMIN_USER") +password = os.getenv("SELECT_AI_ADMIN_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +select_ai_user = os.getenv("SELECT_AI_USER") select_ai.connect(user=admin_user, password=password, dsn=dsn) select_ai.grant_network_access( users=select_ai_user, - host="smtp.example.com", - privileges=["connect", "smtp"], + host="smtp.example.com", + privileges=["connect", "smtp"], lower_port=587, upper_port=587, ) -print("Granted network access to: ", select_ai_user) +print("Granted network access to: ", select_ai_user)

    output:

    @@ -280,22 +280,22 @@

    3. Grant network accessimport select_ai -admin_user = os.getenv("SELECT_AI_ADMIN_USER") -password = os.getenv("SELECT_AI_ADMIN_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") -select_ai_user = os.getenv("SELECT_AI_USER") +admin_user = os.getenv("SELECT_AI_ADMIN_USER") +password = os.getenv("SELECT_AI_ADMIN_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +select_ai_user = os.getenv("SELECT_AI_USER") async def main(): await select_ai.async_connect(user=admin_user, password=password, dsn=dsn) await select_ai.async_grant_network_access( users=select_ai_user, - host="smtp.example.com", - privileges=["connect", "smtp"], + host="smtp.example.com", + privileges=["connect", "smtp"], lower_port=587, upper_port=587, ) - print("Granted network access to: ", select_ai_user) + print("Granted network access to: ", select_ai_user) asyncio.run(main()) @@ -312,20 +312,20 @@

    4. Revoke network accessimport select_ai -admin_user = os.getenv("SELECT_AI_ADMIN_USER") -password = os.getenv("SELECT_AI_ADMIN_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") -select_ai_user = os.getenv("SELECT_AI_USER") +admin_user = os.getenv("SELECT_AI_ADMIN_USER") +password = os.getenv("SELECT_AI_ADMIN_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +select_ai_user = os.getenv("SELECT_AI_USER") select_ai.connect(user=admin_user, password=password, dsn=dsn) select_ai.revoke_network_access( users=select_ai_user, - host="smtp.example.com", - privileges=["connect", "smtp"], + host="smtp.example.com", + privileges=["connect", "smtp"], lower_port=587, upper_port=587, ) -print("Revoked network access from: ", select_ai_user) +print("Revoked network access from: ", select_ai_user)

    output:

    @@ -338,22 +338,22 @@

    4. Revoke network accessimport select_ai -admin_user = os.getenv("SELECT_AI_ADMIN_USER") -password = os.getenv("SELECT_AI_ADMIN_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") -select_ai_user = os.getenv("SELECT_AI_USER") +admin_user = os.getenv("SELECT_AI_ADMIN_USER") +password = os.getenv("SELECT_AI_ADMIN_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +select_ai_user = os.getenv("SELECT_AI_USER") async def main(): await select_ai.async_connect(user=admin_user, password=password, dsn=dsn) await select_ai.async_revoke_network_access( users=select_ai_user, - host="smtp.example.com", - privileges=["connect", "smtp"], + host="smtp.example.com", + privileges=["connect", "smtp"], lower_port=587, upper_port=587, ) - print("Revoked network access from: ", select_ai_user) + print("Revoked network access from: ", select_ai_user) asyncio.run(main()) @@ -421,7 +421,7 @@

    Navigation

  • previous |
  • -
  • Python logo
  • +
  • Python logo
  • Python »
  • @@ -429,7 +429,7 @@

    Navigation

  • - Select AI for Python 1.4.0 documentation » + Select AI for Python 1.5.0 documentation »
  • @@ -474,7 +474,7 @@

    Navigation


    - Created using Sphinx 8.2.3. + Created using Sphinx 9.1.0. diff --git a/docs/user_guide/profile.html b/docs/user_guide/profile.html index 4855ecd..553aaee 100644 --- a/docs/user_guide/profile.html +++ b/docs/user_guide/profile.html @@ -5,16 +5,16 @@ - 1. Profile lifecycle — Select AI for Python 1.4.0 documentation + 1. Profile lifecycle — Select AI for Python 1.5.0 documentation + - - + - - - + + + @@ -67,7 +67,10 @@

    Table of Contents

  • 2. Profile actions @@ -154,13 +171,16 @@

    Navigation

  • index
  • +
  • + modules |
  • next |
  • previous |
  • -
  • Python logo
  • +
  • Python logo
  • Python »
  • @@ -168,7 +188,7 @@

    Navigation

  • - Select AI for Python 1.4.0 documentation » + Select AI for Python 1.5.0 documentation »
  • @@ -223,6 +243,7 @@

    1. Profile lifecycleselect_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.

  • @@ -273,14 +294,15 @@

    2. Profile actions

    2.1. Profile Object Model

    -Select AI Profile and Providers +Select AI Profile object model +

    2.2. Base Profile API

    -class select_ai.BaseProfile(profile_name: str | None = None, attributes: ProfileAttributes | None = None, description: str | None = None, merge: bool | None = False, replace: bool | None = False, raise_error_if_exists: bool | None = True, raise_error_on_empty_attributes: bool | None = False)
    +class select_ai.BaseProfile(profile_name: str | None = None, attributes: ProfileAttributes | None = None, description: str | None = None, merge: bool | None = False, replace: bool | None = False, raise_error_if_exists: bool | None = True, raise_error_on_empty_attributes: bool | None = False, owner: str | None = None)

    BaseProfile is an abstract base class representing a Profile for Select AI’s interactions with AI service providers (LLMs). Use either select_ai.Profile or select_ai.AsyncProfile to @@ -290,21 +312,28 @@

    2.2. Base Parameters:
    • attributes (select_ai.ProfileAttributes) – Object specifying AI profile attributes

    • -
    • description (str) – Description of the profile

    • -
    • merge (bool) – Fetches the profile +

    • description (str) – Description of the profile

    • +
    • owner (str) – Database user that owns the profile

    • +
    • merge (bool) – Fetches the profile from database, merges the non-null attributes and saves it back in the database. Default value is False

    • -
    • replace (bool) – Replaces the profile and attributes +

    • replace (bool) – Replaces the profile and attributes in the database. Default value is False

    • -
    • raise_error_if_exists (bool) – Raise ProfileExistsError +

    • raise_error_if_exists (bool) – Raise ProfileExistsError if profile exists in the database and replace = False and merge = False. Default value is True

    • -
    • raise_error_on_empty_attributes (bool) – Raise +

    • raise_error_on_empty_attributes (bool) – Raise ProfileEmptyAttributesError, if profile attributes are empty in database. Default value is False.

    +
    +
    +property qualified_name: str | None
    +

    Return the owner-qualified profile name when owner is known.

    +
    +
    @@ -312,23 +341,22 @@

    2.2. Base 2.3. Profile API

    -class select_ai.Profile(*args, **kwargs)
    +class select_ai.Profile(*args, **kwargs)

    Profile class represents an AI Profile. It defines attributes and methods to interact with the underlying AI Provider. All methods in this class are synchronous or blocking

    -add_negative_feedback(prompt_spec: Tuple[str, Action] | None = None, sql_id: str | None = None, response: str | None = None, feedback_content: str | None = None)
    +add_negative_feedback(prompt_spec: Tuple[str, Action] | None = None, sql_id: str | None = None, response: str | None = None, feedback_content: str | None = None)

    Give negative feedback to the LLM

    Parameters:
      -
    • prompt_spec (Tuple[str, Action]) – First element is the prompt and -second is the corresponding action

    • -
    • sql_id (str) – SQL identifier from V$MAPPED_SQL view

    • -
    • response (str) – Expected SQL from LLM

    • -
    • feedback_content (str) – Actual feedback in natural language

    • +
    • prompt_spec (Tuple[str, select_ai.action.Action]) – First element is the prompt; second is the corresponding action

    • +
    • sql_id (str) – SQL identifier from V$MAPPED_SQL view

    • +
    • response (str) – Expected SQL from LLM

    • +
    • feedback_content (str) – Actual feedback in natural language

    @@ -336,14 +364,13 @@

    2.3.
    -add_positive_feedback(prompt_spec: Tuple[str, Action] | None = None, sql_id: str | None = None)
    +add_positive_feedback(prompt_spec: Tuple[str, Action] | None = None, sql_id: str | None = None)

    Give positive feedback to the LLM

    Parameters:
      -
    • prompt_spec (Tuple[str, Action]) – First element is the prompt and -second is the corresponding action

    • -
    • sql_id (str) – SQL identifier from V$MAPPED_SQL view

    • +
    • prompt_spec (Tuple[str, select_ai.action.Action]) – First element is the prompt; second is the corresponding action

    • +
    • sql_id (str) – SQL identifier from V$MAPPED_SQL view

    @@ -351,15 +378,17 @@

    2.3.
    -chat(prompt: str, params: Mapping = None, stream: bool = False, chunk_size: int = 8192) → str | Generator[str, None, None]
    +chat(prompt: str, params: Mapping = None, stream: bool = False, chunk_size: int = 8192, *, attributes: Mapping = None) → str | Generator[str, None, None]

    Chat with the LLM

    Parameters:
      -
    • prompt (str) – Natural language prompt

    • +
    • prompt (str) – Natural language prompt

    • params – Parameters to include in the LLM request

    • -
    • stream (bool) – Return an iterator of response chunks

    • -
    • chunk_size (int) – Number of characters to read per stream chunk

    • +
    • attributes (Mapping) – Profile attributes to override for this +request

    • +
    • stream (bool) – Return an iterator of response chunks

    • +
    • chunk_size (int) – Number of characters to read per stream chunk

    Returns:
    @@ -370,14 +399,14 @@

    2.3.
    -chat_session(conversation: Conversation, delete: bool = False)
    +chat_session(conversation: Conversation, delete: bool = False)

    Starts a new chat session for context-aware conversations

    Parameters:
    • conversation (Conversation) – Conversation object to use for this chat session

    • -
    • delete (bool) – Delete conversation after session ends

    • +
    • delete (bool) – Delete conversation after session ends

    Returns:
    @@ -388,11 +417,11 @@

    2.3.
    -create(replace: int | None = False) → None
    +create(replace: int | None = False) → None

    Create an AI Profile in the Database

    Parameters:
    -

    replace (bool) – Set True to replace else False

    +

    replace (bool) – Set True to replace else False

    Returns:

    None

    @@ -405,11 +434,11 @@

    2.3.
    -delete(force=False) → None
    +delete(force=False) → None

    Deletes an AI profile from the database

    Parameters:
    -

    force (bool) – Ignores errors if AI profile does not exist.

    +

    force (bool) – Ignores errors if AI profile does not exist.

    Returns:

    None

    @@ -422,14 +451,13 @@

    2.3.
    -delete_feedback(prompt_spec: Tuple[str, Action] = None, sql_id: str | None = None)
    +delete_feedback(prompt_spec: Tuple[str, Action] = None, sql_id: str | None = None)

    Delete feedback from the database

    Parameters:
      -
    • prompt_spec (Tuple[str, Action]) – First element is the prompt and -second is the corresponding action

    • -
    • sql_id (str) – SQL identifier from V$MAPPED_SQL view

    • +
    • prompt_spec (Tuple[str, select_ai.action.Action]) – First element is the prompt; second is the corresponding action

    • +
    • sql_id (str) – SQL identifier from V$MAPPED_SQL view

    @@ -437,13 +465,13 @@

    2.3.
    -classmethod delete_profile(profile_name: str, force: bool = False)
    +classmethod delete_profile(profile_name: str, force: bool = False)

    Class method to delete an AI profile from the database

    Parameters:
      -
    • profile_name (str) – Name of the AI profile

    • -
    • force (bool) – Ignores errors if AI profile does not exist.

    • +
    • profile_name (str) – Name of the AI profile

    • +
    • force (bool) – Ignores errors if AI profile does not exist.

    Returns:
    @@ -455,17 +483,47 @@

    2.3. +
    +disable() → None
    +

    Disable this AI profile in the database.

    +
    +
    Returns:
    +

    None

    +
    +
    Raises:
    +

    oracledb.DatabaseError

    +
    +
    +

    + +
    +
    +enable() → None
    +

    Enable this AI profile in the database.

    +
    +
    Returns:
    +

    None

    +
    +
    Raises:
    +

    oracledb.DatabaseError

    +
    +
    +
    +
    -explain_sql(prompt: str, params: Mapping = None, stream: bool = False, chunk_size: int = 8192) → str | Generator[str, None, None]
    +explain_sql(prompt: str, params: Mapping = None, stream: bool = False, chunk_size: int = 8192, *, attributes: Mapping = None) → str | Generator[str, None, None]

    Explain the generated SQL

    Parameters:
      -
    • prompt (str) – Natural language prompt

    • +
    • prompt (str) – Natural language prompt

    • params – Parameters to include in the LLM request

    • -
    • stream (bool) – Return an iterator of response chunks

    • -
    • chunk_size (int) – Number of characters to read per stream chunk

    • +
    • attributes (Mapping) – Profile attributes to override for this +request

    • +
    • stream (bool) – Return an iterator of response chunks

    • +
    • chunk_size (int) – Number of characters to read per stream chunk

    Returns:
    @@ -476,12 +534,15 @@

    2.3.
    -classmethod fetch(profile_name: str) → Profile
    +classmethod fetch(profile_name: str, owner: str | None = None) → Profile

    Create a proxy Profile object from fetched attributes saved in the database

    Parameters:
    -

    profile_name (str) – The name of the AI profile

    +
      +
    • profile_name (str) – The name of the AI profile

    • +
    • owner (str) – Owner of a shared profile. Defaults to current schema.

    • +
    Returns:

    select_ai.Profile

    @@ -494,17 +555,19 @@

    2.3.
    -generate(prompt: str, action: Action | None = Action.RUNSQL, params: Mapping = None, stream: bool = False, chunk_size: int = 8192) → DataFrame | str | Generator[str, None, None] | None
    +generate(prompt: str, action: Action | None = Action.RUNSQL, params: Mapping = None, stream: bool = False, chunk_size: int = 8192, *, attributes: Mapping = None) → DataFrame | str | Generator[str, None, None] | None

    Perform AI translation using this profile

    Parameters:
      -
    • prompt (str) – Natural language prompt to translate

    • -
    • action (select_ai.profile.Action)

    • +
    • prompt (str) – Natural language prompt to translate

    • +
    • action (select_ai.action.Action)

    • params – Parameters to include in the LLM request. For e.g. conversation_id for context-aware chats

    • -
    • stream (bool) – Return an iterator of response chunks

    • -
    • chunk_size (int) – Number of characters to read per stream chunk

    • +
    • attributes (Mapping) – Profile attributes to override for this +request

    • +
    • stream (bool) – Return an iterator of response chunks

    • +
    • chunk_size (int) – Number of characters to read per stream chunk

    Returns:
    @@ -515,7 +578,7 @@

    2.3.
    -generate_synthetic_data(synthetic_data_attributes: SyntheticDataAttributes) → None
    +generate_synthetic_data(synthetic_data_attributes: SyntheticDataAttributes) → None

    Generate synthetic data for a single table, multiple tables or a full schema.

    @@ -542,15 +605,35 @@

    2.3. +
    +grant_access(user_or_role_name: str) → None
    +

    Grant a database user or role access to this AI profile.

    +
    +
    Parameters:
    +

    user_or_role_name (str) – Database user or role receiving access.

    +
    +
    Returns:
    +

    None

    +
    +
    Raises:
    +

    oracledb.DatabaseError

    +
    +
    +

    +
    -classmethod list(profile_name_pattern: str = '.*') → Generator[Profile, None, None]
    +classmethod list(profile_name_pattern: str = '.*', owner: str | None = None) → Generator[Profile, None, None]

    List AI Profiles saved in the database.

    Parameters:
    -

    profile_name_pattern (str) – Regular expressions can be used +

      +
    • profile_name_pattern (str) – Regular expressions can be used to specify a pattern. Function REGEXP_LIKE is used to perform the -match. Default value is “.*” i.e. match all AI profiles.

      +match. Default value is “.*” i.e. match all AI profiles.

    • +
    • owner (str) – Owner of shared profiles. Defaults to current schema.

    • +
    Returns:

    Iterator[Profile]

    @@ -560,15 +643,17 @@

    2.3.
    -narrate(prompt: str, params: Mapping = None, stream: bool = False, chunk_size: int = 8192) → str | Generator[str, None, None]
    +narrate(prompt: str, params: Mapping = None, stream: bool = False, chunk_size: int = 8192, *, attributes: Mapping = None) → str | Generator[str, None, None]

    Narrate the result of the SQL

    Parameters:
      -
    • prompt (str) – Natural language prompt

    • +
    • prompt (str) – Natural language prompt

    • params – Parameters to include in the LLM request

    • -
    • stream (bool) – Return an iterator of response chunks

    • -
    • chunk_size (int) – Number of characters to read per stream chunk

    • +
    • attributes (Mapping) – Profile attributes to override for this +request

    • +
    • stream (bool) – Return an iterator of response chunks

    • +
    • chunk_size (int) – Number of characters to read per stream chunk

    Returns:
    @@ -577,16 +662,41 @@

    2.3. +
    +property qualified_name: str | None
    +

    Return the owner-qualified profile name when owner is known.

    +

    + +
    +
    +revoke_access(user_or_role_name: str) → None
    +

    Revoke a database user or role’s access to this AI profile.

    +
    +
    Parameters:
    +

    user_or_role_name (str) – Database user or role losing access.

    +
    +
    Returns:
    +

    None

    +
    +
    Raises:
    +

    oracledb.DatabaseError

    +
    +
    +
    +
    -run_sql(prompt: str, params: Mapping = None) → DataFrame
    +run_sql(prompt: str, params: Mapping = None, *, attributes: Mapping = None) → DataFrame

    Run the generate SQL statement and return a pandas Dataframe built using the result set

    Parameters:
      -
    • prompt (str) – Natural language prompt

    • +
    • prompt (str) – Natural language prompt

    • params – Parameters to include in the LLM request

    • +
    • attributes (Mapping) – Profile attributes to override for this +request

    Returns:
    @@ -597,14 +707,14 @@

    2.3.
    -set_attribute(attribute_name: str, attribute_value: bool | str | int | float | Provider)
    +set_attribute(attribute_name: str, attribute_value: bool | str | int | float | Provider)

    Updates AI profile attribute on the Python object and also saves it in the database

    Parameters:
      -
    • attribute_name (str) – Name of the AI profile attribute

    • -
    • attribute_value (Union[bool, str, int, float, Provider]) – Value of +

    • attribute_name (str) – Name of the AI profile attribute

    • +
    • attribute_value (Union[bool, str, int, float, Provider]) – Value of the profile attribute

    @@ -621,7 +731,7 @@

    2.3.
    Parameters:
    -

    attributes (ProviderAttributes) – Object specifying AI profile +

    attributes (select_ai.ProfileAttributes) – Object specifying AI profile attributes

    Returns:
    @@ -632,15 +742,17 @@

    2.3.
    -show_prompt(prompt: str, params: Mapping = None, stream: bool = False, chunk_size: int = 8192) → str | Generator[str, None, None]
    +show_prompt(prompt: str, params: Mapping = None, stream: bool = False, chunk_size: int = 8192, *, attributes: Mapping = None) → str | Generator[str, None, None]

    Show the prompt sent to LLM

    Parameters:
      -
    • prompt (str) – Natural language prompt

    • +
    • prompt (str) – Natural language prompt

    • params – Parameters to include in the LLM request

    • -
    • stream (bool) – Return an iterator of response chunks

    • -
    • chunk_size (int) – Number of characters to read per stream chunk

    • +
    • attributes (Mapping) – Profile attributes to override for this +request

    • +
    • stream (bool) – Return an iterator of response chunks

    • +
    • chunk_size (int) – Number of characters to read per stream chunk

    Returns:
    @@ -651,15 +763,17 @@

    2.3.
    -show_sql(prompt: str, params: Mapping = None, stream: bool = False, chunk_size: int = 8192) → str | Generator[str, None, None]
    +show_sql(prompt: str, params: Mapping = None, stream: bool = False, chunk_size: int = 8192, *, attributes: Mapping = None) → str | Generator[str, None, None]

    Show the generated SQL

    Parameters:
      -
    • prompt (str) – Natural language prompt

    • +
    • prompt (str) – Natural language prompt

    • params – Parameters to include in the LLM request

    • -
    • stream (bool) – Return an iterator of response chunks

    • -
    • chunk_size (int) – Number of characters to read per stream chunk

    • +
    • attributes (Mapping) – Profile attributes to override for this +request

    • +
    • stream (bool) – Return an iterator of response chunks

    • +
    • chunk_size (int) – Number of characters to read per stream chunk

    Returns:
    @@ -670,17 +784,17 @@

    2.3.
    -summarize(content: str = None, prompt: str = None, location_uri: str = None, credential_name: str = None, params: SummaryParams = None) → str
    +summarize(content: str = None, prompt: str = None, location_uri: str = None, credential_name: str = None, params: SummaryParams = None) → str

    Generate summary

    Parameters:
    @@ -108,7 +155,10 @@

    Table of Contents

  • AsyncVectorIndex.get_attributes()
  • AsyncVectorIndex.get_next_refresh_timestamp()
  • AsyncVectorIndex.get_profile()
  • +
  • AsyncVectorIndex.grant_access()
  • AsyncVectorIndex.list()
  • +
  • AsyncVectorIndex.qualified_name
  • +
  • AsyncVectorIndex.revoke_access()
  • AsyncVectorIndex.set_attribute()
  • AsyncVectorIndex.set_attributes()
  • @@ -117,7 +167,10 @@

    Table of Contents

  • 5.2. Async list vector index
  • 5.3. Async fetch vector index
  • 5.4. Async update vector index attributes
  • -
  • 5.5. Async RAG using vector index
  • +
  • 5.5. Async RAG using vector index +
  • @@ -160,13 +213,16 @@

    Navigation

  • index
  • +
  • + modules |
  • next |
  • previous |
  • -
  • Python logo
  • +
  • Python logo
  • Python »
  • @@ -174,7 +230,7 @@

    Navigation

  • - Select AI for Python 1.4.0 documentation » + Select AI for Python 1.5.0 documentation »
  • @@ -242,7 +298,8 @@

    Navigation

    2. VectorIndex Object Model

    -Select AI Vector Index +Select AI Vector Index object model +
    @@ -305,8 +362,8 @@

    3.
    attributes = select_ai.OracleVectorIndexAttributes(
    -    location="https://objectstorage.us-ashburn-1.oraclecloud.com/n/example/b/docs/o/product-guides",
    -    object_storage_credential_name="object_store_credential",
    +    location="https://objectstorage.us-ashburn-1.oraclecloud.com/n/example/b/docs/o/product-guides",
    +    object_storage_credential_name="object_store_credential",
         chunk_size=1024,
         chunk_overlap=128,
         match_limit=5,
    @@ -323,50 +380,250 @@ 

    3.
    -class select_ai.VectorIndexAttributes(chunk_size: int | None = None, chunk_overlap: int | None = None, enable_sources: bool | None = None, location: str | None = None, match_limit: int | None = None, object_storage_credential_name: str | None = None, profile_name: str | None = None, refresh_rate: int | None = None, similarity_threshold: float | None = None, vector_distance_metric: VectorDistanceMetric | None = None, vector_db_endpoint: str | None = None, vector_db_credential_name: str | None = None, vector_db_provider: VectorDBProvider | None = None, vector_dimension: int | None = None, vector_table_name: str | None = None, pipeline_name: str | None = None)
    +class select_ai.VectorIndexAttributes(chunk_size: int | None = None, chunk_overlap: int | None = None, enable_sources: bool | None = None, location: str | None = None, match_limit: int | None = None, object_storage_credential_name: str | None = None, profile_name: str | None = None, refresh_rate: int | None = None, similarity_threshold: float | None = None, vector_distance_metric: VectorDistanceMetric | None = None, vector_db_endpoint: str | None = None, vector_db_credential_name: str | None = None, vector_db_provider: VectorDBProvider | None = None, vector_dimension: int | None = None, vector_table_name: str | None = None, pipeline_name: str | None = None)

    Attributes of a vector index help to manage and configure the behavior of the vector index.

    Parameters:
      -
    • chunk_size (int) – Text size of chunking the input data.

    • -
    • chunk_overlap (int) – Specifies the amount of overlapping +

    • chunk_size (int) – Text size of chunking the input data.

    • +
    • chunk_overlap (int) – Specifies the amount of overlapping characters between adjacent chunks of text.

    • enable_sources – Provides document source links and filenames in RAG output

    • -
    • location (str) – Location of the object store.

    • -
    • match_limit (int) – Specifies the maximum number of results to return +

    • location (str) – Location of the object store.

    • +
    • match_limit (int) – Specifies the maximum number of results to return in a vector search query

    • -
    • object_storage_credential_name (str) – Name of the credentials for +

    • object_storage_credential_name (str) – Name of the credentials for accessing object storage.

    • -
    • profile_name (str) – Name of the AI profile which is used for +

    • profile_name (str) – Name of the AI profile which is used for embedding source data and user prompts.

    • -
    • refresh_rate (int) – Interval of updating data in the vector store. +

    • refresh_rate (int) – Interval of updating data in the vector store. The unit is minutes.

    • -
    • similarity_threshold (float) – Defines the minimum level of similarity +

    • similarity_threshold (float) – Defines the minimum level of similarity required for two items to be considered a match

    • -
    • vector_distance_metric (VectorDistanceMetric) – Specifies the type of +

    • vector_distance_metric (select_ai.vector_index.VectorDistanceMetric) – Specifies the type of distance calculation used to compare vectors in a database

    • -
    • vector_db_provider (VectorDBProvider) – Name of the Vector database +

    • vector_db_provider (select_ai.vector_index.VectorDBProvider) – Name of the Vector database provider. Default value is “oracle”

    • -
    • vector_db_endpoint (str) – Endpoint to access the Vector database

    • -
    • vector_db_credential_name (str) – Name of the credentials for accessing +

    • vector_db_endpoint (str) – Endpoint to access the Vector database

    • +
    • vector_db_credential_name (str) – Name of the credentials for accessing Vector database

    • -
    • vector_dimension (int) – Specifies the number of elements in each +

    • vector_dimension (int) – Specifies the number of elements in each vector within the vector store

    • -
    • vector_table_name (str) – Specifies the name of the table or collection +

    • vector_table_name (str) – Specifies the name of the table or collection to store vector embeddings and chunked data

    +
    +
    +chunk_overlap: int | None = None
    +
    + +
    +
    +chunk_size: int | None = None
    +
    + +
    +
    +classmethod create(*, vector_db_provider: str | None = None, **kwargs)
    +
    + +
    +
    +dict(exclude_null=True)
    +
    + +
    +
    +enable_sources: bool | None = None
    +
    + +
    +
    +json(exclude_null=True)
    +
    + +
    +
    +classmethod keys()
    +
    + +
    +
    +location: str | None = None
    +
    + +
    +
    +match_limit: int | None = None
    +
    + +
    +
    +object_storage_credential_name: str | None = None
    +
    + +
    +
    +pipeline_name: str | None = None
    +
    + +
    +
    +profile_name: str | None = None
    +
    + +
    +
    +refresh_rate: int | None = None
    +
    + +
    +
    +similarity_threshold: float | None = None
    +
    + +
    +
    +vector_db_credential_name: str | None = None
    +
    + +
    +
    +vector_db_endpoint: str | None = None
    +
    + +
    +
    +vector_db_provider: VectorDBProvider | None = None
    +
    + +
    +
    +vector_dimension: int | None = None
    +
    + +
    +
    +vector_distance_metric: VectorDistanceMetric | None = None
    +
    + +
    +
    +vector_table_name: str | None = None
    +
    +

    3.1. OracleVectorIndexAttributes

    -class select_ai.OracleVectorIndexAttributes(chunk_size: int | None = None, chunk_overlap: int | None = None, enable_sources: bool | None = None, location: str | None = None, match_limit: int | None = None, object_storage_credential_name: str | None = None, profile_name: str | None = None, refresh_rate: int | None = None, similarity_threshold: float | None = None, vector_distance_metric: VectorDistanceMetric | None = None, vector_db_endpoint: str | None = None, vector_db_credential_name: str | None = None, vector_db_provider: VectorDBProvider | None = VectorDBProvider.ORACLE, vector_dimension: int | None = None, vector_table_name: str | None = None, pipeline_name: str | None = None)
    +class select_ai.OracleVectorIndexAttributes(chunk_size: int | None = None, chunk_overlap: int | None = None, enable_sources: bool | None = None, location: str | None = None, match_limit: int | None = None, object_storage_credential_name: str | None = None, profile_name: str | None = None, refresh_rate: int | None = None, similarity_threshold: float | None = None, vector_distance_metric: VectorDistanceMetric | None = None, vector_db_endpoint: str | None = None, vector_db_credential_name: str | None = None, vector_db_provider: VectorDBProvider | None = VectorDBProvider.ORACLE, vector_dimension: int | None = None, vector_table_name: str | None = None, pipeline_name: str | None = None)

    Oracle specific vector index attributes

    +
    +
    +chunk_overlap: int | None = None
    +
    + +
    +
    +chunk_size: int | None = None
    +
    + +
    +
    +classmethod create(*, vector_db_provider: str | None = None, **kwargs)
    +
    + +
    +
    +dict(exclude_null=True)
    +
    + +
    +
    +enable_sources: bool | None = None
    +
    + +
    +
    +json(exclude_null=True)
    +
    + +
    +
    +classmethod keys()
    +
    + +
    +
    +location: str | None = None
    +
    + +
    +
    +match_limit: int | None = None
    +
    + +
    +
    +object_storage_credential_name: str | None = None
    +
    + +
    +
    +pipeline_name: str | None = None
    +
    + +
    +
    +profile_name: str | None = None
    +
    + +
    +
    +refresh_rate: int | None = None
    +
    + +
    +
    +similarity_threshold: float | None = None
    +
    + +
    +
    +vector_db_credential_name: str | None = None
    +
    + +
    +
    +vector_db_endpoint: str | None = None
    +
    + +
    +
    +vector_db_provider: VectorDBProvider | None = 'oracle'
    +
    + +
    +
    +vector_dimension: int | None = None
    +
    + +
    +
    +vector_distance_metric: VectorDistanceMetric | None = None
    +
    + +
    +
    +vector_table_name: str | None = None
    +
    +
    @@ -376,20 +633,20 @@

    4. VectorIndex object can be created with select_ai.VectorIndex()

    -class select_ai.VectorIndex(profile: BaseProfile | None = None, index_name: str | None = None, description: str | None = None, attributes: VectorIndexAttributes | None = None)
    +class select_ai.VectorIndex(profile: BaseProfile | None = None, index_name: str | None = None, description: str | None = None, attributes: VectorIndexAttributes | None = None, owner: str | None = None)

    VectorIndex objects let you manage vector indexes

    Parameters:
      -
    • index_name (str) – The name of the vector index

    • -
    • description (str) – The description of the vector index

    • +
    • index_name (str) – The name of the vector index

    • +
    • description (str) – The description of the vector index

    • attributes (select_ai.VectorIndexAttributes) – The attributes of the vector index

    -create(replace: bool | None = False, wait_for_completion: bool = False)
    +create(replace: bool | None = False, wait_for_completion: bool = False)
    Create a vector index in the database and populates the index

    with data from an object store bucket using an async scheduler job

    @@ -397,8 +654,8 @@

    4.
    Parameters:
      -
    • replace (bool) – Replace vector index if it exists

    • -
    • wait_for_completion (bool) – True to wait for index creation

    • +
    • replace (bool) – Replace vector index if it exists

    • +
    • wait_for_completion (bool) – True to wait for index creation

    Returns:
    @@ -409,15 +666,15 @@

    4.
    -delete(include_data: bool | None = True, force: bool | None = False)
    +delete(include_data: bool | None = True, force: bool | None = False)

    This procedure removes a vector store index

    Parameters:
      -
    • include_data (bool) – Indicates whether to delete +

    • include_data (bool) – Indicates whether to delete both the customer’s vector store and vector index along with the vector index object

    • -
    • force (bool) – Indicates whether to ignore errors +

    • force (bool) – Indicates whether to ignore errors that occur if the vector index does not exist

    @@ -432,16 +689,16 @@

    4.
    -classmethod delete_index(index_name: str, include_data: bool = True, force: bool = False)
    +classmethod delete_index(index_name: str, include_data: bool = True, force: bool = False)

    Class method to remove a vector store index

    Parameters:
      -
    • index_name (str) – The name of the vector index

    • -
    • include_data (bool) – Indicates whether to delete +

    • index_name (str) – The name of the vector index

    • +
    • include_data (bool) – Indicates whether to delete both the customer’s vector store and vector index along with the vector index object

    • -
    • force (bool) – Indicates whether to ignore errors +

    • force (bool) – Indicates whether to ignore errors that occur if the vector index does not exist

    @@ -491,13 +748,16 @@

    4.
    -classmethod fetch(index_name: str) → VectorIndex
    +classmethod fetch(index_name: str, owner: str | None = None) → VectorIndex

    Fetches vector index attributes from the database and builds a proxy object for the passed index_name

    Parameters:
    -

    index_name (str) – The name of the vector index

    +
      +
    • index_name (str) – The name of the vector index

    • +
    • owner (str) – Owner of a shared index. Defaults to current schema.

    • +

    @@ -518,7 +778,7 @@

    4.
    -get_next_refresh_timestamp() → datetime | None
    +get_next_refresh_timestamp() → datetime | None

    Returns the UTC timestamp of the next scheduled refresh

    @@ -536,15 +796,24 @@

    4. +
    +grant_access(user_or_role_name: str) → None
    +

    Grant a database user or role access to this vector index.

    +

    +
    -classmethod list(index_name_pattern: str = '.*') → Iterator[VectorIndex]
    +classmethod list(index_name_pattern: str = '.*', owner: str | None = None) → Iterator[VectorIndex]

    List Vector Indexes

    Parameters:
    -

    index_name_pattern (str) – Regular expressions can be used +

      +
    • index_name_pattern (str) – Regular expressions can be used to specify a pattern. Function REGEXP_LIKE is used to perform the -match. Default value is “.*” i.e. match all vector indexes.

      +match. Default value is “.*” i.e. match all vector indexes.

    • +
    • owner (str) – Owner of shared indexes. Defaults to current schema.

    • +
    Returns:

    Iterator[VectorIndex]

    @@ -552,16 +821,28 @@

    4. +
    +property qualified_name: str | None
    +

    Return the owner-qualified index name when owner is known.

    +

    + +
    +
    +revoke_access(user_or_role_name: str) → None
    +

    Revoke a database user or role’s vector index access.

    +
    +
    -set_attribute(attribute_name: str, attribute_value: str | int | float)
    +set_attribute(attribute_name: str, attribute_value: str | int | float)

    This procedure updates an existing vector store index with a specified value of the vector index attribute.

    Parameters:
      -
    • attribute_name (str) – Custom attribute name

    • -
    • attribute_value (Union[str, int, float]) – Attribute Value

    • +
    • attribute_name (str) – Custom attribute name

    • +
    • attribute_value (Union[str, int, float]) – Attribute Value

    @@ -606,13 +887,14 @@

    4. 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.

    @@ -638,53 +920,75 @@

    4.1. Create vector indexcreate().

    +

    Upload samples/data/refund-policy.txt from the repository to a bucket you +can read. The document says customers have 30 calendar days to request a +refund. Configure the sample with the URI of that uploaded file and the name +of an existing database credential authorized to read it:

    +
    export SELECT_AI_DOCUMENT_URI='https://objectstorage.REGION.oraclecloud.com/n/NAMESPACE/b/BUCKET/o/refund-policy.txt'
    +export SELECT_AI_OBJECT_STORAGE_CREDENTIAL='OBJECT_STORE_CRED'
    +export SELECT_AI_PROVIDER_CREDENTIAL='my_oci_ai_profile_key'
    +
    +
    +

    Replace REGION, NAMESPACE, and BUCKET with your storage values. +Use the common database connection variables from Connection. +The provider credential and storage credential can be the same credential +when it has both permissions. Optionally set SELECT_AI_OCI_REGION and +SELECT_AI_EMBEDDING_MODEL for your OCI configuration. The default region +is us-chicago-1 and embedding model is cohere.embed-v4.0; choose a model +available in your account and region.

    +

    The creation samples replace test_vector_index and their named RAG +profile. Use a development schema, or edit these names before running if you +need to preserve existing definitions. The samples close their database +connections but leave the profile and index available for the next step.

    +

    From the repository root, run python samples/vector_index_create.py. +For async, use python samples/async/vector_index_create.py. Both examples +wait for the initial load to complete. Run either workflow in its own schema, +or delete the existing sample index before switching between them.

    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")
    -
    -
    -select_ai.connect(user=user, password=password, dsn=dsn)
    -# Configure an AI provider with an embedding model
    -# of your choice
    -provider = select_ai.OCIGenAIProvider(
    -    region="us-chicago-1",
    -    oci_apiformat="GENERIC",
    -    embedding_model="cohere.embed-english-v3.0",
    +user = os.environ["SELECT_AI_USER"]
    +password = os.environ["SELECT_AI_PASSWORD"]
    +dsn = os.environ["SELECT_AI_DB_CONNECT_STRING"]
    +# Upload samples/data/refund-policy.txt to your bucket, then set its URI.
    +source_uri = os.environ["SELECT_AI_DOCUMENT_URI"]
    +provider_credential = os.getenv(
    +    "SELECT_AI_PROVIDER_CREDENTIAL", "my_oci_ai_profile_key"
     )
    +storage_credential = os.environ["SELECT_AI_OBJECT_STORAGE_CREDENTIAL"]
     
    -# Create an AI profile to use the Vector index with
    -profile_attributes = select_ai.ProfileAttributes(
    -    credential_name="my_oci_ai_profile_key",
    -    provider=provider,
    -)
    -profile = select_ai.Profile(
    -    profile_name="oci_vector_ai_profile",
    -    attributes=profile_attributes,
    -    description="MY OCI AI Profile",
    -    replace=True,
    -)
    -
    -# Specify objects to create an embedding for. In this example,
    -# the objects reside in ObjectStore and the vector database is
    -# Oracle
    -vector_index_attributes = select_ai.OracleVectorIndexAttributes(
    -    location="https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph",
    -    object_storage_credential_name="my_oci_ai_profile_key",
    -)
    -
    -# Create a Vector index object
    -vector_index = select_ai.VectorIndex(
    -    index_name="test_vector_index",
    -    attributes=vector_index_attributes,
    -    description="Test vector index",
    -    profile=profile,
    -)
    -vector_index.create(replace=True, wait_for_completion=True)
    -print("Created vector index: test_vector_index")
    +select_ai.connect(user=user, password=password, dsn=dsn)
    +try:
    +    provider = select_ai.OCIGenAIProvider(
    +        region=os.getenv("SELECT_AI_OCI_REGION", "us-chicago-1"),
    +        oci_apiformat="GENERIC",
    +        embedding_model=os.getenv(
    +            "SELECT_AI_EMBEDDING_MODEL", "cohere.embed-v4.0"
    +        ),
    +    )
    +    profile = select_ai.Profile(
    +        profile_name="oci_vector_ai_profile",
    +        attributes=select_ai.ProfileAttributes(
    +            credential_name=provider_credential,
    +            provider=provider,
    +        ),
    +        description="Profile for the sample refund policy",
    +        replace=True,
    +    )
    +    vector_index = select_ai.VectorIndex(
    +        index_name="test_vector_index",
    +        attributes=select_ai.OracleVectorIndexAttributes(
    +            location=source_uri,
    +            object_storage_credential_name=storage_credential,
    +        ),
    +        description="Sample refund policy",
    +        profile=profile,
    +    )
    +    vector_index.create(replace=True, wait_for_completion=True)
    +    print("Created vector index: test_vector_index")
    +finally:
    +    select_ai.disconnect()
     

    output:

    @@ -698,51 +1002,54 @@

    4.2. List vector indeximport select_ai -user = os.getenv("SELECT_AI_USER") -password = os.getenv("SELECT_AI_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +user = os.getenv("SELECT_AI_USER") +password = os.getenv("SELECT_AI_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") select_ai.connect(user=user, password=password, dsn=dsn) vector_index = select_ai.VectorIndex() -for index in vector_index.list(index_name_pattern="^test"): - print("Vector index", index.index_name) - print("Vector index profile", index.profile) - - -

    output:

    -
    Vector index TEST_VECTOR_INDEX
    -Vector index profile Profile(profile_name=oci_vector_ai_profile, attributes=ProfileAttributes(annotations=None, case_sensitive_values=None, comments=None, constraints=None, conversation=None, credential_name='my_oci_ai_profile_key', enable_sources=None, enable_source_offsets=None, enforce_object_list=None, max_tokens=1024, object_list=None, object_list_mode=None, provider=OCIGenAIProvider(embedding_model=None, model=None, provider_name='oci', provider_endpoint=None, region='us-chicago-1', oci_apiformat='GENERIC', oci_compartment_id=None, oci_endpoint_id=None, oci_runtimetype=None), seed=None, stop_tokens=None, streaming=None, temperature=None, vector_index_name='test_vector_index'), description=None)
    +for index in vector_index.list(index_name_pattern="^test"):
    +    print("Vector index", index.index_name)
    +    print("Vector index profile", index.profile)
     
    +

    The script prints each matching index name and its linked profile. Results +depend on the indexes visible to the connected user.

    4.3. 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.

    +
    vector_index = select_ai.VectorIndex.fetch(
    +    "PRODUCT_DOCS",
    +    owner="APP_OWNER",
    +)
    +print(vector_index.qualified_name)
    +
    +
    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")
    +user = os.getenv("SELECT_AI_USER")
    +password = os.getenv("SELECT_AI_PASSWORD")
    +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING")
     
     select_ai.connect(user=user, password=password, dsn=dsn)
     
    -vector_index = select_ai.VectorIndex.fetch(index_name="test_vector_index")
    +vector_index = select_ai.VectorIndex.fetch(index_name="test_vector_index")
     print(vector_index.attributes)
     print(vector_index.profile)
     print(vector_index.get_next_refresh_timestamp())
     
    -

    output:

    -
    OracleVectorIndexAttributes(chunk_size=1024, chunk_overlap=128, location='https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph', match_limit=5, object_storage_credential_name='my_oci_ai_profile_key', profile_name='oci_vector_ai_profile', refresh_rate=1450, similarity_threshold=0.5, vector_distance_metric='COSINE', vector_db_endpoint=None, vector_db_credential_name=None, vector_db_provider=<VectorDBProvider.ORACLE: 'oracle'>, vector_dimension=None, vector_table_name=None, pipeline_name='TEST_VECTOR_INDEX$VECPIPELINE')
    -
    -Profile(profile_name=oci_vector_ai_profile, attributes=ProfileAttributes(annotations=None, case_sensitive_values=None, comments=None, constraints=None, conversation=None, credential_name='my_oci_ai_profile_key', enable_custom_source_uri=None, enable_sources=None, enable_source_offsets=None, enforce_object_list=None, max_tokens=1024, object_list=None, object_list_mode=None, provider=OCIGenAIProvider(embedding_model='cohere.embed-english-v3.0', model=None, provider_name='oci', provider_endpoint=None, region='us-chicago-1', oci_apiformat='GENERIC', oci_compartment_id=None, oci_endpoint_id=None, oci_runtimetype=None), seed=None, stop_tokens=None, streaming=None, temperature=None, vector_index_name='test_vector_index'), description=MY OCI AI Profile)
    -
    -
    +

    The script prints the saved index attributes, its linked profile, and the next +refresh timestamp. The URI and credentials reflect your configuration; the +next refresh can be None when no refresh execution has been recorded.

    4.4. Update vector index attributes

    @@ -753,12 +1060,12 @@

    4.4. Update vector index attributesimport select_ai -user = os.getenv("SELECT_AI_USER") -password = os.getenv("SELECT_AI_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +user = os.getenv("SELECT_AI_USER") +password = os.getenv("SELECT_AI_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") select_ai.connect(user=user, password=password, dsn=dsn) vector_index = select_ai.VectorIndex( - index_name="test_vector_index", + index_name="test_vector_index", ) # Use vector_index.set_attributes to update a multiple attributes @@ -767,15 +1074,13 @@

    4.4. Update vector index attributes# Use vector_index.set_attribute to update a single attribute vector_index.set_attribute( - attribute_name="similarity_threshold", attribute_value=0.5 + attribute_name="similarity_threshold", attribute_value=0.5 ) print(vector_index.attributes) -

    output:

    -
    OracleVectorIndexAttributes(chunk_size=1024, chunk_overlap=128, location='https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph', match_limit=5, object_storage_credential_name='my_oci_ai_profile_key', profile_name='oci_vector_ai_profile', refresh_rate=1450, similarity_threshold=0.5, vector_distance_metric='COSINE', vector_db_endpoint=None, vector_db_credential_name=None, vector_db_provider=<VectorDBProvider.ORACLE: 'oracle'>, vector_dimension=None, vector_table_name=None, pipeline_name='TEST_VECTOR_INDEX$VECPIPELINE')
    -
    -
    +

    The script sets refresh_rate to 1450 minutes and similarity_threshold +to 0.5, then prints the attributes held by the Python object.

    4.5. RAG using vector index

    @@ -787,32 +1092,35 @@

    4.5. RAG using vector indeximport select_ai -user = os.getenv("SELECT_AI_USER") -password = os.getenv("SELECT_AI_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +user = os.environ["SELECT_AI_USER"] +password = os.environ["SELECT_AI_PASSWORD"] +dsn = os.environ["SELECT_AI_DB_CONNECT_STRING"] select_ai.connect(user=user, password=password, dsn=dsn) -profile = select_ai.Profile(profile_name="oci_vector_ai_profile") -r = profile.narrate("list the conda environments in my object store") -print(r) +try: + profile = select_ai.Profile( + profile_name="oci_vector_ai_profile" + ) + response = profile.narrate( + "How many days do customers have to request a refund?" + ) + print(response) +finally: + select_ai.disconnect() +

    The answer should identify the 30 calendar day refund window. Exact +wording and source formatting depend on the provider and profile settings. +For example:

    output:

    -

    4.6. Delete vector index

    @@ -824,14 +1132,14 @@

    4.6. Delete vector indeximport select_ai -user = os.getenv("SELECT_AI_USER") -password = os.getenv("SELECT_AI_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +user = os.getenv("SELECT_AI_USER") +password = os.getenv("SELECT_AI_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") select_ai.connect(user=user, password=password, dsn=dsn) -vector_index = select_ai.VectorIndex(index_name="test_vector_index") +vector_index = select_ai.VectorIndex(index_name="test_vector_index") vector_index.delete(force=True) -print("Deleted vector index: test_vector_index") +print("Deleted vector index: test_vector_index")

    output:

    @@ -846,29 +1154,29 @@

    5. select_ai.AsyncVectorIndex()

    -class select_ai.AsyncVectorIndex(profile: BaseProfile | None = None, index_name: str | None = None, description: str | None = None, attributes: VectorIndexAttributes | None = None)
    +class select_ai.AsyncVectorIndex(profile: BaseProfile | None = None, index_name: str | None = None, description: str | None = None, attributes: VectorIndexAttributes | None = None, owner: str | None = None)

    AsyncVectorIndex objects let you manage vector indexes using async APIs. Use this for non-blocking concurrent requests

    Parameters:
      -
    • index_name (str) – The name of the vector index

    • -
    • description (str) – The description of the vector index

    • +
    • index_name (str) – The name of the vector index

    • +
    • description (str) – The description of the vector index

    • attributes (VectorIndexAttributes) – The attributes of the vector index

    -async create(replace: bool | None = False, wait_for_completion: bool | None = False) → None
    +async create(replace: bool | None = False, wait_for_completion: bool | None = False) → None

    Create a vector index in the database and populates it with data from an object store bucket using an async scheduler job

    Parameters:
      -
    • replace (bool) – True to replace existing vector index

    • -
    • wait_for_completion (bool) – True to wait for index creation

    • +
    • replace (bool) – True to replace existing vector index

    • +
    • wait_for_completion (bool) – True to wait for index creation

    @@ -876,15 +1184,15 @@

    5.
    -async delete(include_data: bool | None = True, force: bool | None = False) → None
    +async delete(include_data: bool | None = True, force: bool | None = False) → None

    This procedure removes a vector store index.

    Parameters:
      -
    • include_data (bool) – Indicates whether to delete +

    • include_data (bool) – Indicates whether to delete both the customer’s vector store and vector index along with the vector index object.

    • -
    • force (bool) – Indicates whether to ignore errors +

    • force (bool) – Indicates whether to ignore errors that occur if the vector index does not exist.

    @@ -899,16 +1207,16 @@

    5.
    -async classmethod delete_index(index_name: str, include_data: bool = True, force: bool = False)
    +async classmethod delete_index(index_name: str, include_data: bool = True, force: bool = False)

    Class method to remove a vector store index

    Parameters:
      -
    • index_name (str) – The name of the vector index

    • -
    • include_data (bool) – Indicates whether to delete +

    • index_name (str) – The name of the vector index

    • +
    • include_data (bool) – Indicates whether to delete both the customer’s vector store and vector index along with the vector index object

    • -
    • force (bool) – Indicates whether to ignore errors +

    • force (bool) – Indicates whether to ignore errors that occur if the vector index does not exist

    @@ -923,7 +1231,7 @@

    5.
    -async disable() → None
    +async disable() → None

    This procedure disables a vector index object in the current database. When disabled, an AI profile cannot use the vector index, and the system does not load data into the vector store as new data @@ -941,7 +1249,7 @@

    5.
    -async enable() → None
    +async enable() → None

    This procedure enables or activates a previously disabled vector index object. Generally, when you create a vector index, by default it is enabled such that the AI profile can use it to perform indexing @@ -958,20 +1266,23 @@

    5.
    -async classmethod fetch(index_name: str) → AsyncVectorIndex
    +async classmethod fetch(index_name: str, owner: str | None = None) → AsyncVectorIndex

    Fetches vector index attributes from the database and builds a proxy object for the passed index_name

    Parameters:
    -

    index_name (str) – The name of the vector index

    +
      +
    • index_name (str) – The name of the vector index

    • +
    • owner (str) – Owner of a shared index. Defaults to current schema.

    • +

    -async get_attributes() → VectorIndexAttributes
    +async get_attributes() → VectorIndexAttributes

    Get attributes of a vector index

    Returns:
    @@ -985,13 +1296,13 @@

    5.
    -async get_next_refresh_timestamp() → datetime | None
    +async get_next_refresh_timestamp() → datetime | None

    Return the UTC timestamp for the next scheduled refresh.

    -async get_profile() → AsyncProfile
    +async get_profile() → AsyncProfile

    Get AsyncProfile object linked to this vector index

    Returns:
    @@ -1003,15 +1314,24 @@

    5. +
    +async grant_access(user_or_role_name: str) → None
    +

    Asynchronously grant a user or role access to this vector index.

    +

    +
    -classmethod list(index_name_pattern: str = '.*') → AsyncGenerator[AsyncVectorIndex, None]
    +classmethod list(index_name_pattern: str = '.*', owner: str | None = None) → AsyncGenerator[AsyncVectorIndex, None]

    List Vector Indexes.

    Parameters:
    -

    index_name_pattern (str) – Regular expressions can be used +

      +
    • index_name_pattern (str) – Regular expressions can be used to specify a pattern. Function REGEXP_LIKE is used to perform the -match. Default value is “.*” i.e. match all vector indexes.

      +match. Default value is “.*” i.e. match all vector indexes.

    • +
    • owner (str) – Owner of shared indexes. Defaults to current schema.

    • +
    Returns:

    AsyncGenerator[VectorIndex]

    @@ -1019,16 +1339,28 @@

    5. +
    +property qualified_name: str | None
    +

    Return the owner-qualified index name when owner is known.

    +

    + +
    +
    +async revoke_access(user_or_role_name: str) → None
    +

    Asynchronously revoke a user or role’s vector index access.

    +
    +
    -async set_attribute(attribute_name: str, attribute_value: str | int | float) → None
    +async set_attribute(attribute_name: str, attribute_value: str | int | float) → None

    This procedure updates an existing vector store index with a specified value of the vector index attribute.

    Parameters:
      -
    • attribute_name (str) – Custom attribute name

    • -
    • attribute_value (Union[str, int, float]) – Attribute Value

    • +
    • attribute_name (str) – Custom attribute name

    • +
    • attribute_value (Union[str, int, float]) – Attribute Value

    @@ -1036,7 +1368,7 @@

    5.
    -async set_attributes(attributes: VectorIndexAttributes) → None
    +async set_attributes(attributes: VectorIndexAttributes) → None

    This procedure updates an existing vector store index with a specified value of the vector index attribute. multiple attributes by passing an object of type :class VectorIndexAttributes

    @@ -1093,50 +1425,56 @@

    5.1. Async create vector indeximport select_ai -user = os.getenv("SELECT_AI_USER") -password = os.getenv("SELECT_AI_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +user = os.environ["SELECT_AI_USER"] +password = os.environ["SELECT_AI_PASSWORD"] +dsn = os.environ["SELECT_AI_DB_CONNECT_STRING"] +# Upload samples/data/refund-policy.txt to your bucket, then set its URI. +source_uri = os.environ["SELECT_AI_DOCUMENT_URI"] +provider_credential = os.getenv( + "SELECT_AI_PROVIDER_CREDENTIAL", "my_oci_ai_profile_key" +) +storage_credential = os.environ["SELECT_AI_OBJECT_STORAGE_CREDENTIAL"] async def main(): await select_ai.async_connect(user=user, password=password, dsn=dsn) - - provider = select_ai.OCIGenAIProvider( - region="us-chicago-1", - oci_apiformat="GENERIC", - embedding_model="cohere.embed-english-v3.0", - ) - profile_attributes = select_ai.ProfileAttributes( - credential_name="my_oci_ai_profile_key", - provider=provider, - ) - async_profile = await select_ai.AsyncProfile( - profile_name="async_oci_vector_ai_profile", - attributes=profile_attributes, - description="MY OCI AI Profile", - replace=True, - ) - - vector_index_attributes = select_ai.OracleVectorIndexAttributes( - location="https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph", - object_storage_credential_name="my_oci_ai_profile_key", - ) - - async_vector_index = select_ai.AsyncVectorIndex( - index_name="test_vector_index", - attributes=vector_index_attributes, - description="Vector index for conda environments", - profile=async_profile, - ) - await async_vector_index.create(replace=True, wait_for_completion=True) - print("Created vector index: test_vector_index") + try: + provider = select_ai.OCIGenAIProvider( + region=os.getenv("SELECT_AI_OCI_REGION", "us-chicago-1"), + oci_apiformat="GENERIC", + embedding_model=os.getenv( + "SELECT_AI_EMBEDDING_MODEL", "cohere.embed-v4.0" + ), + ) + profile = await select_ai.AsyncProfile( + profile_name="async_oci_vector_ai_profile", + attributes=select_ai.ProfileAttributes( + credential_name=provider_credential, + provider=provider, + ), + description="Profile for the sample refund policy", + replace=True, + ) + vector_index = select_ai.AsyncVectorIndex( + index_name="test_vector_index", + attributes=select_ai.OracleVectorIndexAttributes( + location=source_uri, + object_storage_credential_name=storage_credential, + ), + description="Sample refund policy", + profile=profile, + ) + await vector_index.create(replace=True, wait_for_completion=True) + print("Created vector index: test_vector_index") + finally: + await select_ai.async_disconnect() asyncio.run(main())

    output:

    -
    created vector index: test_vector_index
    +
    Created vector index: test_vector_index
     

    @@ -1147,27 +1485,24 @@

    5.2. Async list vector indeximport select_ai -user = os.getenv("SELECT_AI_USER") -password = os.getenv("SELECT_AI_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +user = os.getenv("SELECT_AI_USER") +password = os.getenv("SELECT_AI_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") async def main(): await select_ai.async_connect(user=user, password=password, dsn=dsn) vector_index = select_ai.AsyncVectorIndex() - async for index in vector_index.list(index_name_pattern="^test"): - print("Vector index", index.index_name) - print("Vector index profile", index.profile) + async for index in vector_index.list(index_name_pattern="^test"): + print("Vector index", index.index_name) + print("Vector index profile", index.profile) asyncio.run(main()) -

    output:

    -
    Vector index TEST_VECTOR_INDEX
    -Vector index profile AsyncProfile(profile_name=oci_vector_ai_profile, attributes=ProfileAttributes(annotations=None, case_sensitive_values=None, comments=None, constraints=None, conversation=None, credential_name='my_oci_ai_profile_key', enable_sources=None, enable_source_offsets=None, enforce_object_list=None, max_tokens=1024, object_list=None, object_list_mode=None, provider=OCIGenAIProvider(embedding_model=None, model=None, provider_name='oci', provider_endpoint=None, region='us-chicago-1', oci_apiformat='GENERIC', oci_compartment_id=None, oci_endpoint_id=None, oci_runtimetype=None), seed=None, stop_tokens=None, streaming=None, temperature=None, vector_index_name='test_vector_index'), description=None)
    -
    -
    +

    The script prints matching index names and their linked profiles. Results +depend on your schema and configuration.

    5.3. Async fetch vector index

    @@ -1178,15 +1513,15 @@

    5.3. Async fetch vector indeximport select_ai -user = os.getenv("SELECT_AI_USER") -password = os.getenv("SELECT_AI_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +user = os.getenv("SELECT_AI_USER") +password = os.getenv("SELECT_AI_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") async def main(): await select_ai.async_connect(user=user, password=password, dsn=dsn) async_vector_index = await select_ai.AsyncVectorIndex.fetch( - index_name="test_vector_index" + index_name="test_vector_index" ) print(async_vector_index.attributes) print(async_vector_index.profile) @@ -1196,12 +1531,8 @@

    5.3. Async fetch vector indexasyncio.run(main()) -

    output:

    -
    OracleVectorIndexAttributes(chunk_size=1024, chunk_overlap=128, location='https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph', match_limit=5, object_storage_credential_name='my_oci_ai_profile_key', profile_name='oci_vector_ai_profile', refresh_rate=1450, similarity_threshold=0.5, vector_distance_metric='COSINE', vector_db_endpoint=None, vector_db_credential_name=None, vector_db_provider=<VectorDBProvider.ORACLE: 'oracle'>, vector_dimension=None, vector_table_name=None, pipeline_name='TEST_VECTOR_INDEX$VECPIPELINE')
    -
    -AsyncProfile(profile_name=oci_vector_ai_profile, attributes=ProfileAttributes(annotations=None, case_sensitive_values=None, comments=None, constraints=None, conversation=None, credential_name='my_oci_ai_profile_key', enable_custom_source_uri=None, enable_sources=None, enable_source_offsets=None, enforce_object_list=None, max_tokens=1024, object_list=None, object_list_mode=None, provider=OCIGenAIProvider(embedding_model='cohere.embed-english-v3.0', model=None, provider_name='oci', provider_endpoint=None, region='us-chicago-1', oci_apiformat='GENERIC', oci_compartment_id=None, oci_endpoint_id=None, oci_runtimetype=None), seed=None, stop_tokens=None, streaming=None, temperature=None, vector_index_name='test_vector_index'), description=MY OCI AI Profile)
    -
    -
    +

    The script prints the saved attributes and linked profile. Source URIs, +credentials, and other values depend on the index you created.

    5.4. Async update vector index attributes

    @@ -1212,15 +1543,15 @@

    5.4. Async update vector index attribute import select_ai -user = os.getenv("SELECT_AI_USER") -password = os.getenv("SELECT_AI_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +user = os.getenv("SELECT_AI_USER") +password = os.getenv("SELECT_AI_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") async def main(): await select_ai.async_connect(user=user, password=password, dsn=dsn) async_vector_index = select_ai.AsyncVectorIndex( - index_name="test_vector_index", + index_name="test_vector_index", ) # Use vector_index.set_attributes to update a multiple attributes @@ -1231,7 +1562,7 @@

    5.4. Async update vector index attribute # Use vector_index.set_attribute to update a single attribute await async_vector_index.set_attribute( - attribute_name="similarity_threshold", attribute_value=0.5 + attribute_name="similarity_threshold", attribute_value=0.5 ) print(async_vector_index.attributes) @@ -1239,10 +1570,8 @@

    5.4. Async update vector index attribute asyncio.run(main()) -

    output:

    -
    OracleVectorIndexAttributes(chunk_size=1024, chunk_overlap=128, location='https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph', match_limit=5, object_storage_credential_name='my_oci_ai_profile_key', profile_name='oci_vector_ai_profile', refresh_rate=1450, similarity_threshold=0.5, vector_distance_metric='COSINE', vector_db_endpoint=None, vector_db_credential_name=None, vector_db_provider=<VectorDBProvider.ORACLE: 'oracle'>, vector_dimension=None, vector_table_name=None, pipeline_name='TEST_VECTOR_INDEX$VECPIPELINE')
    -
    -
    +

    The script updates the refresh rate and similarity threshold, then prints +the attributes held by the Python object.

    5.5. Async RAG using vector index

    @@ -1250,41 +1579,63 @@

    5.5. Async RAG using vector indeximport select_ai -user = os.getenv("SELECT_AI_USER") -password = os.getenv("SELECT_AI_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +user = os.environ["SELECT_AI_USER"] +password = os.environ["SELECT_AI_PASSWORD"] +dsn = os.environ["SELECT_AI_DB_CONNECT_STRING"] async def main(): await select_ai.async_connect(user=user, password=password, dsn=dsn) - async_profile = await select_ai.AsyncProfile( - profile_name="async_oci_vector_ai_profile" - ) - r = await async_profile.narrate( - "list the conda environments in my object store" - ) - print(r) + try: + profile = await select_ai.AsyncProfile( + profile_name="async_oci_vector_ai_profile" + ) + response = await profile.narrate( + "How many days do customers have to request a refund?" + ) + print(response) + finally: + await select_ai.async_disconnect() asyncio.run(main()) +

    The answer should identify the 30 calendar day refund window. Exact +wording and source formatting depend on the provider and profile settings. +For example:

    output:

    -
    The conda environments in your object store are:
    -1. fccenv
    -2. myrenv
    -3. fully-loaded-mlenv
    -4. graphenv
    -
    -These environments are listed in the provided data as separate JSON documents, each containing information about a specific conda environment.
    -
    -Sources:
    -  - fccenv-manifest.json (https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph/fccenv-manifest.json)
    -  - myrenv-manifest.json (https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph/myrenv-manifest.json)
    -  - fully-loaded-mlenv-manifest.json (https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph/fully-loaded-mlenv-manifest.json)
    -  - graphenv-manifest.json (https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph/graphenv-manifest.json)
    +
    Customers can request a refund within 30 calendar days of purchase.
    +
    +
    +

    If no answer is grounded in the policy, verify that the uploaded URI and +storage credential are correct, that the initial load completed, and that +this profile is linked to test_vector_index. Run the corresponding RAG +script after creating the index. See Delete vector index to remove the +sample index and its data; delete the sample profile separately when finished.

    +
    +

    5.5.1. Share a vector index

    +

    While connected as the index owner, grant access to an existing index:

    +
    index = select_ai.VectorIndex.fetch("PRODUCT_DOCS")
    +index.grant_access("APP_USER")
    +
    +
    +

    In a separate connection as the recipient, use +VectorIndex.fetch("PRODUCT_DOCS", owner="APP_OWNER"). It returns an object +whose qualified_name is APP_OWNER.PRODUCT_DOCS. When access should end, +run index.revoke_access("APP_USER") as the owner.

    +

    For an async application, use AsyncVectorIndex and await each operation:

    +
    # Run as the index owner.
    +index = await select_ai.AsyncVectorIndex.fetch("PRODUCT_DOCS")
    +await index.grant_access("APP_USER")
    +
    +# Run later as the index owner, when access should end.
    +await index.revoke_access("APP_USER")
     
    +

    See Sharing and ownership for the recipient’s async fetch +example and connection setup.

    +

    @@ -1301,9 +1652,53 @@

    Table of Contents

  • 1. Vector Index
  • 2. VectorIndex Object Model
  • 3. VectorIndexAttributes @@ -1319,7 +1714,10 @@

    Table of Contents

  • VectorIndex.get_attributes()
  • VectorIndex.get_next_refresh_timestamp()
  • VectorIndex.get_profile()
  • +
  • VectorIndex.grant_access()
  • VectorIndex.list()
  • +
  • VectorIndex.qualified_name
  • +
  • VectorIndex.revoke_access()
  • VectorIndex.set_attribute()
  • VectorIndex.set_attributes()
  • @@ -1343,7 +1741,10 @@

    Table of Contents

  • AsyncVectorIndex.get_attributes()
  • AsyncVectorIndex.get_next_refresh_timestamp()
  • AsyncVectorIndex.get_profile()
  • +
  • AsyncVectorIndex.grant_access()
  • AsyncVectorIndex.list()
  • +
  • AsyncVectorIndex.qualified_name
  • +
  • AsyncVectorIndex.revoke_access()
  • AsyncVectorIndex.set_attribute()
  • AsyncVectorIndex.set_attributes()
  • @@ -1352,7 +1753,10 @@

    Table of Contents

  • 5.2. Async list vector index
  • 5.3. Async fetch vector index
  • 5.4. Async update vector index attributes
  • -
  • 5.5. Async RAG using vector index
  • +
  • 5.5. Async RAG using vector index +
  • @@ -1395,13 +1799,16 @@

    Navigation

  • index
  • +
  • + modules |
  • next |
  • previous |
  • -
  • Python logo
  • +
  • Python logo
  • Python »
  • @@ -1409,7 +1816,7 @@

    Navigation

  • - Select AI for Python 1.4.0 documentation » + Select AI for Python 1.5.0 documentation »
  • @@ -1454,7 +1861,7 @@

    Navigation


    - Created using Sphinx 8.2.3. + Created using Sphinx 9.1.0. diff --git a/docs/user_guide/web_frameworks.html b/docs/user_guide/web_frameworks.html index 4ffa880..5eaf9b3 100644 --- a/docs/user_guide/web_frameworks.html +++ b/docs/user_guide/web_frameworks.html @@ -5,16 +5,16 @@ - 1. Using select_ai with Python web frameworks — Select AI for Python 1.4.0 documentation + 1. Using select_ai with Python web frameworks — Select AI for Python 1.5.0 documentation + - - + - - - + + + @@ -115,13 +115,16 @@

    Navigation

  • index
  • +
  • + modules |
  • next |
  • previous |
  • -
  • Python logo
  • +
  • Python logo
  • Python »
  • @@ -129,7 +132,7 @@

    Navigation

  • - Select AI for Python 1.4.0 documentation » + Select AI for Python 1.5.0 documentation »
  • @@ -249,13 +252,13 @@

    1.3. FastAPI synchronous endpointsimport select_ai -user = os.getenv("SELECT_AI_USER") -password = os.getenv("SELECT_AI_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +user = os.getenv("SELECT_AI_USER") +password = os.getenv("SELECT_AI_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") -pool_min = int(os.getenv("SELECT_AI_POOL_MIN", "5")) -pool_max = int(os.getenv("SELECT_AI_POOL_MAX", "10")) -pool_increment = int(os.getenv("SELECT_AI_POOL_INCREMENT", "5")) +pool_min = int(os.getenv("SELECT_AI_POOL_MIN", "5")) +pool_max = int(os.getenv("SELECT_AI_POOL_MAX", "10")) +pool_increment = int(os.getenv("SELECT_AI_POOL_INCREMENT", "5")) @asynccontextmanager @@ -275,16 +278,16 @@

    1.3. FastAPI synchronous endpointsapp = FastAPI(lifespan=lifespan) -@app.get("/chat") +@app.get("/chat") def chat(prompt: str): - profile = select_ai.Profile(profile_name="oci_ai_profile") - return {"response": profile.chat(prompt=prompt)} + profile = select_ai.Profile(profile_name="oci_ai_profile") + return {"response": profile.chat(prompt=prompt)} -@app.get("/show_sql") +@app.get("/show_sql") def show_sql(prompt: str): - profile = select_ai.Profile(profile_name="oci_ai_profile") - return {"sql": profile.show_sql(prompt=prompt)} + profile = select_ai.Profile(profile_name="oci_ai_profile") + return {"sql": profile.show_sql(prompt=prompt)}

    Start the server:

    @@ -292,15 +295,18 @@

    1.3. FastAPI synchronous endpoints

    Call the service:

    -
    curl "http://localhost:8000/chat?prompt=What%20is%20OCI%3F"
    +
    curl "http://localhost:8000/chat?prompt=What%20is%20OCI%3F"
     

    Stop the server by pressing Ctrl+C in the terminal where uvicorn is running. FastAPI runs the lifespan shutdown hook and select_ai.disconnect() closes the pool.

    -

    This example creates the Profile proxy inside each handler. The proxy is -lightweight; the database connection is acquired from the pool only when the -profile method calls the database.

    +

    This example constructs Profile inside each handler. Construction reads +the saved profile description and attributes from the database, acquiring +connections from the pool before the action itself runs. This keeps metadata +current but adds database calls to each request. Reusing an initialized profile +avoids those reads; refresh it when saved profile settings change, and avoid +mutating a shared profile object while requests are using it.

    1.4. FastAPI asynchronous endpoints

    @@ -314,9 +320,9 @@

    1.4. FastAPI asynchronous endpointsimport select_ai -user = os.getenv("SELECT_AI_USER") -password = os.getenv("SELECT_AI_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +user = os.getenv("SELECT_AI_USER") +password = os.getenv("SELECT_AI_PASSWORD") +dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") @asynccontextmanager @@ -336,12 +342,12 @@

    1.4. FastAPI asynchronous endpointsapp = FastAPI(lifespan=lifespan) -@app.get("/chat") +@app.get("/chat") async def chat(prompt: str): profile = await select_ai.AsyncProfile( - profile_name="async_oci_ai_profile" + profile_name="async_oci_ai_profile" ) - return {"response": await profile.chat(prompt=prompt)} + return {"response": await profile.chat(prompt=prompt)}

    Start and stop the server the same way:

    @@ -366,19 +372,19 @@

    1.5. Flask exampleapp = Flask(__name__) select_ai.create_pool( - user=os.getenv("SELECT_AI_USER"), - password=os.getenv("SELECT_AI_PASSWORD"), - dsn=os.getenv("SELECT_AI_DB_CONNECT_STRING"), - min_size=int(os.getenv("SELECT_AI_POOL_MIN", "1")), - max_size=int(os.getenv("SELECT_AI_POOL_MAX", "4")), - increment=int(os.getenv("SELECT_AI_POOL_INCREMENT", "1")), + user=os.getenv("SELECT_AI_USER"), + password=os.getenv("SELECT_AI_PASSWORD"), + dsn=os.getenv("SELECT_AI_DB_CONNECT_STRING"), + min_size=int(os.getenv("SELECT_AI_POOL_MIN", "1")), + max_size=int(os.getenv("SELECT_AI_POOL_MAX", "4")), + increment=int(os.getenv("SELECT_AI_POOL_INCREMENT", "1")), ) - @app.get("/show_sql") + @app.get("/show_sql") def show_sql(): - prompt = request.args["prompt"] - profile = select_ai.Profile(profile_name="oci_ai_profile") - return jsonify({"sql": profile.show_sql(prompt=prompt)}) + prompt = request.args["prompt"] + profile = select_ai.Profile(profile_name="oci_ai_profile") + return jsonify({"sql": profile.show_sql(prompt=prompt)}) @atexit.register def close_select_ai_pool(): @@ -403,20 +409,21 @@

    1.6. Django exampleclass MyAppConfig(AppConfig): - name = "myapp" + name = "myapp" def ready(self): if not select_ai.is_connected(): select_ai.create_pool( - user=os.getenv("SELECT_AI_USER"), - password=os.getenv("SELECT_AI_PASSWORD"), - dsn=os.getenv("SELECT_AI_DB_CONNECT_STRING"), - min_size=int(os.getenv("SELECT_AI_POOL_MIN", "1")), - max_size=int(os.getenv("SELECT_AI_POOL_MAX", "4")), - increment=int(os.getenv("SELECT_AI_POOL_INCREMENT", "1")), + user=os.getenv("SELECT_AI_USER"), + password=os.getenv("SELECT_AI_PASSWORD"), + dsn=os.getenv("SELECT_AI_DB_CONNECT_STRING"), + min_size=int(os.getenv("SELECT_AI_POOL_MIN", "1")), + max_size=int(os.getenv("SELECT_AI_POOL_MAX", "4")), + increment=int(os.getenv("SELECT_AI_POOL_INCREMENT", "1")), ) +

    Add the view in myapp/views.py:

    When using Django’s development autoreloader, startup hooks may run more than @@ -457,17 +464,25 @@

    1.8. Pool wait behaviorselect_ai.create_pool() and select_ai.create_pool_async() pass pool options through to python-oracledb. Use wait_timeout and getmode to control what happens when all pooled connections are busy.

    -
    diff --git a/gcloud/README.md b/gcloud/README.md index 01ae2bd..9df1176 100644 --- a/gcloud/README.md +++ b/gcloud/README.md @@ -98,10 +98,17 @@ The connection-form client flow is documented in the By default, the private Cloud Run deployments rely on Cloud Run IAM and keep database sessions separate by A2A conversation. Add `--require-oauth` to -either deployment script when verified human-user ownership is required. In +either deployment script to require a bearer token and derive an owner scope. In that mode Gemini Enterprise must be configured with end-user OAuth and sends its user token in `Authorization`; missing tokens receive HTTP 401. `X-Serverless-Authorization` is separate and automatic: Gemini Enterprise uses it to invoke the private Cloud Run service, and Cloud Run consumes it before the request reaches `a2a serve`. + +The SDK does not validate bearer-token signatures, expiration, issuer, or +audience. A trusted upstream service must validate the end-user token and +prevent callers from bypassing validation to reach the A2A listener directly. +`--require-oauth` alone does not verify identity. Cloud Run IAM validation of +the calling service is separate from validation of the end-user token. See the +[A2A authentication contract](../doc/source/user_guide/a2a.rst) for details. diff --git a/gcloud/cluster/README.md b/gcloud/cluster/README.md index 44b1654..de98434 100644 --- a/gcloud/cluster/README.md +++ b/gcloud/cluster/README.md @@ -6,6 +6,10 @@ password through the generated A2UI form. A session remains available for 15 minutes by default. Set a different lifetime in seconds with `--session-ttl-seconds`; for example, `--session-ttl-seconds 1800` keeps sessions for 30 minutes. +Oracle conversation history is retained for 7 days by default. Set +`--conversation-retention-days` to change this, or use 0 to keep +conversations until they are +manually deleted. ## Protocol architecture @@ -134,7 +138,7 @@ agent. By default, Cloud Run IAM authenticates Gemini Enterprise and database sessions are separated by A2A conversation; the server does not verify the human user who owns a conversation. Add `--require-oauth` when the deployment -requires authenticated end-user ownership. Gemini Enterprise must then be +requires an end-user bearer token and an owner scope. Gemini Enterprise must then be configured with end-user OAuth and every request must carry its separate `Authorization: Bearer ...` token. Requests without it receive HTTP 401. @@ -269,3 +273,10 @@ The Cloud Run server certificate files are mounted from Google Secret Manager. The identity that submits Cloud Build needs permission to use GKE, Cloud Run, and Secret Manager. GKE maintains the managed worker DNS records. + +The SDK does not validate bearer-token signatures, expiration, issuer, or +audience. A trusted upstream service must validate the end-user token and +prevent callers from bypassing validation to reach the A2A listener directly. +`--require-oauth` alone does not verify identity. Cloud Run IAM validation of +the calling service is separate from validation of the end-user token. See the +[A2A authentication contract](../../doc/source/user_guide/a2a.rst) for details. diff --git a/gcloud/cluster/cloudbuild.yaml b/gcloud/cluster/cloudbuild.yaml index ad8d9aa..8d0bf5f 100644 --- a/gcloud/cluster/cloudbuild.yaml +++ b/gcloud/cluster/cloudbuild.yaml @@ -131,6 +131,7 @@ steps: sed -e "s|WORKER_IMAGE|$image|" \ -e "s|WORKER_REPLICAS|${_WORKER_REPLICAS}|" \ -e "s|SESSION_TTL_SECONDS|${_SESSION_TTL_SECONDS}|" \ + -e "s|CONVERSATION_RETENTION_DAYS|${_CONVERSATION_RETENTION_DAYS}|" \ -e "s|GKE_DNS_DOMAIN|${_GKE_DNS_DOMAIN}|" \ "$worker_manifest" > /workspace/worker.yaml @@ -222,6 +223,7 @@ substitutions: _SUBNET: default _WORKER_REPLICAS: "2" _SESSION_TTL_SECONDS: "900" + _CONVERSATION_RETENTION_DAYS: "7" _ENABLE_WORKER_MTLS: "false" _ROTATE_WORKER_MTLS: "false" _GKE_DNS_DOMAIN: select-ai-a2a-cluster.internal diff --git a/gcloud/cluster/deploy.sh b/gcloud/cluster/deploy.sh index 1e6c8cc..97dd1ba 100755 --- a/gcloud/cluster/deploy.sh +++ b/gcloud/cluster/deploy.sh @@ -34,6 +34,9 @@ Options: --subnet NAME VPC subnet for Cloud Run direct VPC egress (default: default) --worker-replicas COUNT GKE worker replica count (default: 2) --session-ttl-seconds N Dynamic session lifetime (default: 900) + --conversation-retention-days N + Oracle conversation retention (default: 7; 0 keeps + conversations until manually deleted) --enable-worker-mtls Use ephemeral mTLS certificates for server-to-worker calls --rotate-worker-mtls Replace the existing worker mTLS CA and certificates --mtls-cert-validity-days DAYS @@ -59,6 +62,7 @@ network="default" subnet="default" worker_replicas="2" session_ttl_seconds="900" +conversation_retention_days="7" enable_worker_mtls="false" rotate_worker_mtls="false" mtls_cert_validity_days="365" @@ -84,6 +88,10 @@ while [[ $# -gt 0 ]]; do session_ttl_seconds="${2:?--session-ttl-seconds requires a value}" shift 2 ;; + --conversation-retention-days) + conversation_retention_days="${2:?--conversation-retention-days requires a value}" + shift 2 + ;; --enable-worker-mtls) enable_worker_mtls="true"; shift ;; --rotate-worker-mtls) rotate_worker_mtls="true"; shift ;; --mtls-cert-validity-days) @@ -113,6 +121,10 @@ if ! [[ "$session_ttl_seconds" =~ ^[1-9][0-9]*$ ]]; then echo "--session-ttl-seconds must be a positive integer." >&2 exit 2 fi +if ! [[ "$conversation_retention_days" =~ ^[0-9]+$ ]]; then + echo "--conversation-retention-days must be a non-negative integer." >&2 + exit 2 +fi if [[ -z "$gke_dns_domain" || "$gke_dns_domain" == *.local ]]; then echo "--gke-dns-domain must be non-empty and must not end in .local." >&2 exit 2 @@ -300,6 +312,7 @@ build_substitutions=( "_SUBNET=$subnet" "_WORKER_REPLICAS=$worker_replicas" "_SESSION_TTL_SECONDS=$session_ttl_seconds" + "_CONVERSATION_RETENTION_DAYS=$conversation_retention_days" "_ENABLE_WORKER_MTLS=$enable_worker_mtls" "_ROTATE_WORKER_MTLS=$rotate_worker_mtls" "_GKE_DNS_DOMAIN=$gke_dns_domain" diff --git a/gcloud/cluster/gke/worker-mtls.yaml b/gcloud/cluster/gke/worker-mtls.yaml index 3c4dc93..e1a7fae 100644 --- a/gcloud/cluster/gke/worker-mtls.yaml +++ b/gcloud/cluster/gke/worker-mtls.yaml @@ -34,6 +34,8 @@ spec: - https://$(POD_NAME).select-ai-a2a-worker.select-ai-a2a.svc.GKE_DNS_DOMAIN:8443 - --session-ttl-seconds - "SESSION_TTL_SECONDS" + - --conversation-retention-days + - "CONVERSATION_RETENTION_DAYS" - --tls-cert-file - /var/run/select-ai-mtls/tls.crt - --tls-key-file diff --git a/gcloud/cluster/gke/worker.yaml b/gcloud/cluster/gke/worker.yaml index 8f22b97..ffa8e87 100644 --- a/gcloud/cluster/gke/worker.yaml +++ b/gcloud/cluster/gke/worker.yaml @@ -34,6 +34,8 @@ spec: - http://$(WORKER_ADDRESS):8080 - --session-ttl-seconds - "SESSION_TTL_SECONDS" + - --conversation-retention-days + - "CONVERSATION_RETENTION_DAYS" ports: - name: http containerPort: 8080 diff --git a/gcloud/standalone/README.md b/gcloud/standalone/README.md index ecb685d..96fc862 100644 --- a/gcloud/standalone/README.md +++ b/gcloud/standalone/README.md @@ -118,7 +118,8 @@ IAM still allows only authorized callers such as Gemini Enterprise to invoke the private service. Each A2A conversation gets a separate database session, but the server does not know or verify which human user owns that conversation. -To require verified end-user ownership, add `--require-oauth`: +To require an end-user bearer token and derive an owner scope, add +`--require-oauth` after configuring upstream token validation: ```bash gcloud/standalone/deploy.sh \ @@ -128,7 +129,7 @@ gcloud/standalone/deploy.sh \ ``` In this mode every A2A request must include `Authorization: Bearer ...` and -the session is scoped by both the authenticated owner and the conversation. +the session is scoped by both the derived owner and the conversation. When registering the agent, configure Gemini Enterprise end-user OAuth rather than selecting **Skip & Finish**. Gemini Enterprise automatically sends its Cloud Run invocation identity in `X-Serverless-Authorization`; end-user OAuth @@ -296,3 +297,10 @@ directories against `.gcloudignore` before running `--build` again. After a successful deployment, the script prints the A2A Agent Card JSON. Paste that JSON into Gemini Enterprise to register the private service. The required Gemini Enterprise invocation permission has already been added. + +The SDK does not validate bearer-token signatures, expiration, issuer, or +audience. A trusted upstream service must validate the end-user token and +prevent callers from bypassing validation to reach the A2A listener directly. +`--require-oauth` alone does not verify identity. Cloud Run IAM validation of +the calling service is separate from validation of the end-user token. See the +[A2A authentication contract](../../doc/source/user_guide/a2a.rst) for details. diff --git a/pyproject.toml b/pyproject.toml index 6ffa295..654dd8f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "select_ai" dynamic = ["version"] description = "Select AI for Python" readme = {file = "README.md", content-type = "text/markdown"} -requires-python = ">=3.10" +requires-python = ">=3.11" authors = [ {name="Abhishek Singh", email="abhishek.o.singh@oracle.com"} ] @@ -20,7 +20,7 @@ keywords = [ "adbs", "autonomous database serverless" ] -license = " UPL-1.0" +license = "UPL-1.0" license-files = ["LICENSE.txt"] classifiers = [ "Development Status :: 5 - Production/Stable", @@ -28,7 +28,6 @@ classifiers = [ "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", @@ -39,22 +38,22 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules" ] dependencies = [ - "oracledb", - "pandas==2.2.3" + "oracledb>=2.0.0", + "pandas==3.0.3" ] [project.optional-dependencies] cli = [ "click", +] +a2a = [ + "click", "a2a-sdk[http-server]>=1.0.3", "uvicorn[standard]>=0.30", "fastapi>=0.115,<1", "httpx>=0.27,<1", "requests>=2.32,<3", ] -a2a = [ - "select_ai[cli]", -] test = [ "anyio", "pytest", diff --git a/recipes/concurrent_prompt_processing/async_pipeline.py b/recipes/concurrent_prompt_processing/async_pipeline.py index 87e09af..90c3a86 100644 --- a/recipes/concurrent_prompt_processing/async_pipeline.py +++ b/recipes/concurrent_prompt_processing/async_pipeline.py @@ -8,7 +8,7 @@ # ----------------------------------------------------------------------------- # concurrent_prompt_processing/async_pipeline.py # -# Send multiple prompts in one database round trip using run_pipeline(). +# Batch multiple prompts using run_pipeline(); see the pipeline user guide. # ----------------------------------------------------------------------------- import asyncio diff --git a/samples/README.md b/samples/README.md index 932b4f9..0017370 100644 --- a/samples/README.md +++ b/samples/README.md @@ -277,3 +277,39 @@ Cloud deployment explanation are in the ```bash export SELECT_AI_DB_CONNECT_STRING="tcps://adb..oraclecloud.com:1521/db2025adb_medium.adb.oraclecloud.com?retry_count=2&retry_delay=3" ``` + +## First query and document RAG + +Run `python samples/quickstart.py` after completing the administrator setup in +[Run your first SQL query](../doc/source/user_guide/quickstart.rst). +Set `OPENAI_API_KEY`, `SELECT_AI_MODEL` (for example `gpt-4.1`), and the common +database connection variables. Each run creates a temporary credential from +that environment variable; replace `OPENAI_API_KEY` to try a different key. +Existing database credentials are not changed. + +To grant package privileges and OpenAI HTTP access first, set +`SELECT_AI_ADMIN_USER` and `SELECT_AI_ADMIN_PASSWORD`, then run +`python samples/quickstart.py --setup`. It disconnects the administrator before +running as the application user. Subsequent runs do not need `--setup`. +The script removes its temporary table, profile, and credential, prints progress, +and sets a 120-second timeout per database round trip. + +For RAG, upload [refund-policy.txt](data/refund-policy.txt) to your object store. +Set `SELECT_AI_DOCUMENT_URI` to that object's URI and +`SELECT_AI_OBJECT_STORAGE_CREDENTIAL` to a database credential that can read it. +The provider credential defaults to `my_oci_ai_profile_key`; override it with +`SELECT_AI_PROVIDER_CREDENTIAL`. See the [vector-index guide](../doc/source/user_guide/vector_index.rst) +for region and model settings before running: + +```bash +python samples/vector_index_create.py +python samples/vector_index_rag.py +# Or use the async workflow in its own schema: +python samples/async/vector_index_create.py +python samples/async/vector_index_rag.py +``` + +These workflows replace `test_vector_index` and their named RAG profiles, and +leave them available after closing the connection. Delete the sample index and +profile when finished. The policy answer should give a 30-calendar-day refund +window; model wording can vary. diff --git a/samples/async/conversation_prompts_tags.py b/samples/async/conversation_prompts_tags.py index 0962ec6..19473d3 100644 --- a/samples/async/conversation_prompts_tags.py +++ b/samples/async/conversation_prompts_tags.py @@ -46,7 +46,10 @@ async def main(): for prompt in prompts: print(f"Prompt {prompt.conversation_prompt_id}: {prompt.prompt}") - await conversation.delete_prompt(prompts[-1].conversation_prompt_id) + if prompts: + await conversation.delete_prompt( + prompts[-1].conversation_prompt_id + ) prompts = [prompt async for prompt in conversation.list_prompts()] print("Prompts after deletion:", prompts) await conversation.remove_tag("PROJECT") diff --git a/samples/async/profile_pipeline.py b/samples/async/profile_pipeline.py index 616447a..efb221a 100644 --- a/samples/async/profile_pipeline.py +++ b/samples/async/profile_pipeline.py @@ -8,7 +8,7 @@ # ----------------------------------------------------------------------------- # async/profile_pipeline.py # -# Demonstrates sending multiple prompts using a single Database round-trip +# Demonstrates batching multiple prompts with a database pipeline # ----------------------------------------------------------------------------- import asyncio @@ -36,12 +36,8 @@ async def main(): ("Explain the query: SELECT * FROM sh.products", "INVALID ACTION"), ] - # 1. Multiple prompts are sent in a single roundtrip to the Database - # 2. Results are returned as soon as Database has executed all prompts - # 3. Application doesn't have to wait on one response before sending - # the next prompts - # 4. Fewer round trips and database is kept busy - # 5. Efficient network usage + # Operations execute sequentially; round-trip savings depend on the + # database version. Successful results are strings, including RUNSQL JSON. results = await async_profile.run_pipeline( prompt_specifications, continue_on_error=True ) diff --git a/samples/async/profile_sql_concurrent_tasks.py b/samples/async/profile_sql_concurrent_tasks.py index 141ac01..ac752e1 100644 --- a/samples/async/profile_sql_concurrent_tasks.py +++ b/samples/async/profile_sql_concurrent_tasks.py @@ -23,20 +23,36 @@ 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", + select_ai.create_pool_async( + user=user, + password=password, + dsn=dsn, + min_size=1, + max_size=3, + increment=1, ) - sql_tasks = [ - async_profile.show_sql(prompt="How many customers?"), - async_profile.run_sql(prompt="How many promotions?"), - async_profile.explain_sql(prompt="How many promotions?"), - ] - - # Collect results from multiple asynchronous tasks - for sql_task in asyncio.as_completed(sql_tasks): - result = await sql_task - print(result) + tasks = [] + try: + async_profile = await select_ai.AsyncProfile( + profile_name="async_oci_ai_profile", + ) + # Each operation acquires its own connection from the pool. + tasks = [ + asyncio.create_task(async_profile.show_sql("How many customers?")), + asyncio.create_task(async_profile.run_sql("How many promotions?")), + asyncio.create_task( + async_profile.explain_sql("How many promotions?") + ), + ] + for task in asyncio.as_completed(tasks): + print(await task) + finally: + # Finish cancellation before closing connections, including on errors. + for task in tasks: + if not task.done(): + task.cancel() + await asyncio.gather(*tasks, return_exceptions=True) + await select_ai.async_disconnect() asyncio.run(main()) diff --git a/samples/async/profiles_list.py b/samples/async/profiles_list.py index 18a24ee..765c5e6 100644 --- a/samples/async/profiles_list.py +++ b/samples/async/profiles_list.py @@ -16,19 +16,18 @@ import select_ai -user = os.getenv("SELECT_AI_USER") -password = os.getenv("SELECT_AI_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") - async def main(): - await select_ai.async_connect(user=user, password=password, dsn=dsn) - async_profile = await select_ai.AsyncProfile() - # matches the start of string - async for fetched_profile in async_profile.list( - profile_name_pattern="^oci" - ): - print(fetched_profile.profile_name) + await select_ai.async_connect( + user=os.environ["SELECT_AI_USER"], + password=os.environ["SELECT_AI_PASSWORD"], + dsn=os.environ["SELECT_AI_DB_CONNECT_STRING"], + ) + try: + async for profile in select_ai.AsyncProfile.list(): + print(profile.qualified_name) + finally: + await select_ai.async_disconnect() asyncio.run(main()) diff --git a/samples/async/vector_index_create.py b/samples/async/vector_index_create.py index ccae29a..4a5908f 100644 --- a/samples/async/vector_index_create.py +++ b/samples/async/vector_index_create.py @@ -16,43 +16,49 @@ import select_ai -user = os.getenv("SELECT_AI_USER") -password = os.getenv("SELECT_AI_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +user = os.environ["SELECT_AI_USER"] +password = os.environ["SELECT_AI_PASSWORD"] +dsn = os.environ["SELECT_AI_DB_CONNECT_STRING"] +# Upload samples/data/refund-policy.txt to your bucket, then set its URI. +source_uri = os.environ["SELECT_AI_DOCUMENT_URI"] +provider_credential = os.getenv( + "SELECT_AI_PROVIDER_CREDENTIAL", "my_oci_ai_profile_key" +) +storage_credential = os.environ["SELECT_AI_OBJECT_STORAGE_CREDENTIAL"] async def main(): await select_ai.async_connect(user=user, password=password, dsn=dsn) - - provider = select_ai.OCIGenAIProvider( - region="us-chicago-1", - oci_apiformat="GENERIC", - embedding_model="cohere.embed-english-v3.0", - ) - profile_attributes = select_ai.ProfileAttributes( - credential_name="my_oci_ai_profile_key", - provider=provider, - ) - async_profile = await select_ai.AsyncProfile( - profile_name="async_oci_vector_ai_profile", - attributes=profile_attributes, - description="MY OCI AI Profile", - replace=True, - ) - - vector_index_attributes = select_ai.OracleVectorIndexAttributes( - location="https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph", - object_storage_credential_name="my_oci_ai_profile_key", - ) - - async_vector_index = select_ai.AsyncVectorIndex( - index_name="test_vector_index", - attributes=vector_index_attributes, - description="Vector index for conda environments", - profile=async_profile, - ) - await async_vector_index.create(replace=True, wait_for_completion=True) - print("Created vector index: test_vector_index") + try: + provider = select_ai.OCIGenAIProvider( + region=os.getenv("SELECT_AI_OCI_REGION", "us-chicago-1"), + oci_apiformat="GENERIC", + embedding_model=os.getenv( + "SELECT_AI_EMBEDDING_MODEL", "cohere.embed-v4.0" + ), + ) + profile = await select_ai.AsyncProfile( + profile_name="async_oci_vector_ai_profile", + attributes=select_ai.ProfileAttributes( + credential_name=provider_credential, + provider=provider, + ), + description="Profile for the sample refund policy", + replace=True, + ) + vector_index = select_ai.AsyncVectorIndex( + index_name="test_vector_index", + attributes=select_ai.OracleVectorIndexAttributes( + location=source_uri, + object_storage_credential_name=storage_credential, + ), + description="Sample refund policy", + profile=profile, + ) + await vector_index.create(replace=True, wait_for_completion=True) + print("Created vector index: test_vector_index") + finally: + await select_ai.async_disconnect() asyncio.run(main()) diff --git a/samples/async/vector_index_rag.py b/samples/async/vector_index_rag.py index 85c1ce8..ef8a632 100644 --- a/samples/async/vector_index_rag.py +++ b/samples/async/vector_index_rag.py @@ -16,20 +16,23 @@ import select_ai -user = os.getenv("SELECT_AI_USER") -password = os.getenv("SELECT_AI_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +user = os.environ["SELECT_AI_USER"] +password = os.environ["SELECT_AI_PASSWORD"] +dsn = os.environ["SELECT_AI_DB_CONNECT_STRING"] async def main(): await select_ai.async_connect(user=user, password=password, dsn=dsn) - async_profile = await select_ai.AsyncProfile( - profile_name="async_oci_vector_ai_profile" - ) - r = await async_profile.narrate( - "list the conda environments in my object store" - ) - print(r) + try: + profile = await select_ai.AsyncProfile( + profile_name="async_oci_vector_ai_profile" + ) + response = await profile.narrate( + "How many days do customers have to request a refund?" + ) + print(response) + finally: + await select_ai.async_disconnect() asyncio.run(main()) diff --git a/samples/conversation_prompts_tags.py b/samples/conversation_prompts_tags.py index 9af7e23..80cad1f 100644 --- a/samples/conversation_prompts_tags.py +++ b/samples/conversation_prompts_tags.py @@ -39,7 +39,8 @@ for prompt in prompts: print(f"Prompt {prompt.conversation_prompt_id}: {prompt.prompt}") - conversation.delete_prompt(prompts[-1].conversation_prompt_id) + if prompts: + conversation.delete_prompt(prompts[-1].conversation_prompt_id) print("Prompts after deletion:", list(conversation.list_prompts())) conversation.remove_tag("PROJECT") finally: diff --git a/samples/profiles_list.py b/samples/profiles_list.py index 5811ef2..54edb99 100644 --- a/samples/profiles_list.py +++ b/samples/profiles_list.py @@ -15,13 +15,13 @@ import select_ai -user = os.getenv("SELECT_AI_USER") -password = os.getenv("SELECT_AI_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") - -select_ai.connect(user=user, password=password, dsn=dsn) -profile = select_ai.Profile() - -# matches all the profiles -for fetched_profile in profile.list(): - print(fetched_profile.profile_name) +select_ai.connect( + user=os.environ["SELECT_AI_USER"], + password=os.environ["SELECT_AI_PASSWORD"], + dsn=os.environ["SELECT_AI_DB_CONNECT_STRING"], +) +try: + for profile in select_ai.Profile.list(): + print(profile.qualified_name) +finally: + select_ai.disconnect() diff --git a/samples/quickstart.py b/samples/quickstart.py new file mode 100644 index 0000000..f53aed2 --- /dev/null +++ b/samples/quickstart.py @@ -0,0 +1,134 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2026, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# https://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +"""Create a temporary OpenAI profile, query sample data, and clean up. + +Use --setup to grant package privileges and OpenAI HTTP access as admin. +""" + +import argparse +import os +import uuid + +import select_ai + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument( + "--setup", + action="store_true", + help="Grant package privileges and OpenAI HTTP access as administrator", +) +args = parser.parse_args() + +user = os.environ["SELECT_AI_USER"] +password = os.environ["SELECT_AI_PASSWORD"] +dsn = os.environ["SELECT_AI_DB_CONNECT_STRING"] +api_key = os.environ["OPENAI_API_KEY"] +model = os.environ["SELECT_AI_MODEL"] + +if args.setup: + print("Connecting as administrator...", flush=True) + select_ai.connect( + user=os.environ["SELECT_AI_ADMIN_USER"], + password=os.environ["SELECT_AI_ADMIN_PASSWORD"], + dsn=dsn, + tcp_connect_timeout=15, + ) + try: + with select_ai.cursor() as cursor: + cursor.connection.call_timeout = 120_000 + print( + f"Granting Select AI package privileges to {user}...", + flush=True, + ) + select_ai.grant_privileges(users=user) + print( + f"Granting {user} HTTP access to api.openai.com...", flush=True + ) + select_ai.grant_network_access( + users=user, host="api.openai.com", privileges="http" + ) + cursor.connection.commit() + finally: + select_ai.disconnect() + +suffix = uuid.uuid4().hex[:12].upper() +table_name = f"SDK_DEMO_{suffix}" +credential_name = f"SDK_CRED_{suffix}" +profile_name = f"SDK_PROFILE_{suffix}" + +print(f"Connecting as application user {user}...", flush=True) +select_ai.connect( + user=user, password=password, dsn=dsn, tcp_connect_timeout=15 +) + +table_created = credential_created = profile_created = False +try: + # This identifier is generated locally, rather than supplied by a caller. + with select_ai.cursor() as cursor: + cursor.connection.call_timeout = 120_000 + cursor.execute( + "SELECT SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') FROM DUAL" + ) + owner = cursor.fetchone()[0] + print("Creating the sample table...", flush=True) + cursor.execute( + f"CREATE TABLE {table_name} (id NUMBER PRIMARY KEY, name VARCHAR2(40))" + ) + table_created = True + cursor.executemany( + f"INSERT INTO {table_name} (id, name) VALUES (:1, :2)", + [(1, "Notebook"), (2, "Keyboard"), (3, "Monitor")], + ) + cursor.connection.commit() + + print("Creating the OpenAI credential and profile...", flush=True) + select_ai.create_credential( + { + "credential_name": credential_name, + "username": "openai", + "password": api_key, + } + ) + credential_created = True + profile = select_ai.Profile( + profile_name=profile_name, + attributes=select_ai.ProfileAttributes( + provider=select_ai.OpenAIProvider(model=model), + credential_name=credential_name, + object_list=[{"owner": owner, "name": table_name}], + ), + ) + profile_created = True + + prompt = ( + f"How many rows are in {table_name}? Return the count as ITEM_COUNT." + ) + print("Generating SQL with OpenAI...", flush=True) + print("Generated SQL:", profile.show_sql(prompt), flush=True) + # run_sql generates and executes a fresh response to the prompt. + print("Generating and executing the query with OpenAI...", flush=True) + print(profile.run_sql(prompt)) +finally: + print( + "Removing temporary objects and closing the connection...", flush=True + ) + # Attempt each cleanup even if an earlier cleanup fails. + try: + if profile_created: + select_ai.Profile.delete_profile(profile_name, force=True) + finally: + try: + if credential_created: + select_ai.delete_credential(credential_name, force=True) + finally: + try: + if table_created: + with select_ai.cursor() as cursor: + cursor.execute(f"DROP TABLE {table_name} PURGE") + finally: + select_ai.disconnect() diff --git a/samples/vector_index_create.py b/samples/vector_index_create.py index 26e0266..9bdfaef 100644 --- a/samples/vector_index_create.py +++ b/samples/vector_index_create.py @@ -15,46 +15,44 @@ import select_ai -user = os.getenv("SELECT_AI_USER") -password = os.getenv("SELECT_AI_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") - - -select_ai.connect(user=user, password=password, dsn=dsn) -# Configure an AI provider with an embedding model -# of your choice -provider = select_ai.OCIGenAIProvider( - region="us-chicago-1", - oci_apiformat="GENERIC", - embedding_model="cohere.embed-english-v3.0", -) - -# Create an AI profile to use the Vector index with -profile_attributes = select_ai.ProfileAttributes( - credential_name="my_oci_ai_profile_key", - provider=provider, -) -profile = select_ai.Profile( - profile_name="oci_vector_ai_profile", - attributes=profile_attributes, - description="MY OCI AI Profile", - replace=True, -) - -# Specify objects to create an embedding for. In this example, -# the objects reside in ObjectStore and the vector database is -# Oracle -vector_index_attributes = select_ai.OracleVectorIndexAttributes( - location="https://objectstorage.us-ashburn-1.oraclecloud.com/n/dwcsdev/b/conda-environment/o/tenant1-pdb3/graph/*.json", - object_storage_credential_name="my_oci_ai_profile_key", +user = os.environ["SELECT_AI_USER"] +password = os.environ["SELECT_AI_PASSWORD"] +dsn = os.environ["SELECT_AI_DB_CONNECT_STRING"] +# Upload samples/data/refund-policy.txt to your bucket, then set its URI. +source_uri = os.environ["SELECT_AI_DOCUMENT_URI"] +provider_credential = os.getenv( + "SELECT_AI_PROVIDER_CREDENTIAL", "my_oci_ai_profile_key" ) +storage_credential = os.environ["SELECT_AI_OBJECT_STORAGE_CREDENTIAL"] -# Create a Vector index object -vector_index = select_ai.VectorIndex( - index_name="test_vector_index", - attributes=vector_index_attributes, - description="Test vector index", - profile=profile, -) -vector_index.create(replace=True, wait_for_completion=True) -print("Created vector index: test_vector_index") +select_ai.connect(user=user, password=password, dsn=dsn) +try: + provider = select_ai.OCIGenAIProvider( + region=os.getenv("SELECT_AI_OCI_REGION", "us-chicago-1"), + oci_apiformat="GENERIC", + embedding_model=os.getenv( + "SELECT_AI_EMBEDDING_MODEL", "cohere.embed-v4.0" + ), + ) + profile = select_ai.Profile( + profile_name="oci_vector_ai_profile", + attributes=select_ai.ProfileAttributes( + credential_name=provider_credential, + provider=provider, + ), + description="Profile for the sample refund policy", + replace=True, + ) + vector_index = select_ai.VectorIndex( + index_name="test_vector_index", + attributes=select_ai.OracleVectorIndexAttributes( + location=source_uri, + object_storage_credential_name=storage_credential, + ), + description="Sample refund policy", + profile=profile, + ) + vector_index.create(replace=True, wait_for_completion=True) + print("Created vector index: test_vector_index") +finally: + select_ai.disconnect() diff --git a/samples/vector_index_rag.py b/samples/vector_index_rag.py index 5885592..62d3c9e 100644 --- a/samples/vector_index_rag.py +++ b/samples/vector_index_rag.py @@ -15,11 +15,16 @@ import select_ai -user = os.getenv("SELECT_AI_USER") -password = os.getenv("SELECT_AI_PASSWORD") -dsn = os.getenv("SELECT_AI_DB_CONNECT_STRING") +user = os.environ["SELECT_AI_USER"] +password = os.environ["SELECT_AI_PASSWORD"] +dsn = os.environ["SELECT_AI_DB_CONNECT_STRING"] select_ai.connect(user=user, password=password, dsn=dsn) -profile = select_ai.Profile(profile_name="oci_vector_ai_profile") -r = profile.narrate("list the conda environments in my object store") -print(r) +try: + profile = select_ai.Profile(profile_name="oci_vector_ai_profile") + response = profile.narrate( + "How many days do customers have to request a refund?" + ) + print(response) +finally: + select_ai.disconnect() diff --git a/src/select_ai/agent/a2a/context_store.py b/src/select_ai/agent/a2a/context_store.py index 74ba492..bd6d48b 100644 --- a/src/select_ai/agent/a2a/context_store.py +++ b/src/select_ai/agent/a2a/context_store.py @@ -7,6 +7,7 @@ """Oracle Database storage for A2A-to-Oracle conversation mappings.""" +import datetime from asyncio import Lock from typing import Optional @@ -52,8 +53,12 @@ class OracleContextStore: def __init__( self, owner_resolver: OwnerResolver = resolve_user_scope, + retention_days: int = 7, ) -> None: + if retention_days < 0: + raise ValueError("retention_days must be non-negative") self.owner_resolver = owner_resolver + self.retention_days = retention_days self.initialized = False self.initialize_lock = Lock() @@ -85,6 +90,7 @@ async def get_or_create( attributes=select_ai.ConversationAttributes( title=f"A2A {team_name}", description=f"A2A context {context_id}", + retention_days=datetime.timedelta(days=self.retention_days), ) ) conversation_id = await conversation.create() diff --git a/src/select_ai/agent/a2a/embedded.py b/src/select_ai/agent/a2a/embedded.py index 105ba7d..406a838 100644 --- a/src/select_ai/agent/a2a/embedded.py +++ b/src/select_ai/agent/a2a/embedded.py @@ -51,6 +51,9 @@ def __init__(self, settings: StandaloneSessionSettings) -> None: self.backend = ProcessSessionBackend( settings.session_ttl_seconds, settings.session_start_timeout_seconds, + database_call_timeout_seconds=( + settings.database_call_timeout_seconds + ), ) self._bindings: dict[tuple[str, str], str] = {} self._tasks: dict[tuple[str, str], str] = {} @@ -120,6 +123,9 @@ def open_session( username=session_info.username, password=session_info.password, team_name=session_info.team_name, + tls_ca_certificate_pem=( + session_info.tls_ca_certificate_pem + ), ) ) ) diff --git a/src/select_ai/agent/a2a/models.py b/src/select_ai/agent/a2a/models.py index 57eca76..8d925f5 100644 --- a/src/select_ai/agent/a2a/models.py +++ b/src/select_ai/agent/a2a/models.py @@ -19,6 +19,7 @@ FORM_FIELD_TO_CONNECTION_FIELD = dict( zip(CONNECTION_FORM_FIELDS, CONNECTION_FIELDS) ) +DEFAULT_DATABASE_CALL_TIMEOUT_SECONDS = 120 @dataclass(frozen=True) @@ -29,12 +30,20 @@ class ConnectionConfig: username: str | None = None password: str | None = None team_name: str | None = None + tls_ca_certificate_pem: str | None = field(default=None, repr=False) def __post_init__(self) -> None: for name in CONNECTION_FIELDS: value = getattr(self, name) if value is not None and (not isinstance(value, str) or not value): raise ValueError(f"{name} must be a non-empty string") + if self.tls_ca_certificate_pem is not None and ( + not isinstance(self.tls_ca_certificate_pem, str) + or not self.tls_ca_certificate_pem.strip() + ): + raise ValueError( + "tls_ca_certificate_pem must be a non-empty string" + ) @property def missing_fields(self) -> tuple[str, ...]: @@ -66,7 +75,10 @@ def resolve(self, submitted: dict) -> "SessionInfo": or submitted.get(form_name) for form_name, connection_name in FORM_FIELD_TO_CONNECTION_FIELD.items() } - return SessionInfo.from_values(values) + return SessionInfo.from_values( + values, + tls_ca_certificate_pem=self.tls_ca_certificate_pem, + ) @dataclass(frozen=True) @@ -80,6 +92,8 @@ class WorkerSettings: session_ttl_seconds: int session_start_timeout_seconds: int worker_endpoint: str | None = None + conversation_retention_days: int = 7 + database_call_timeout_seconds: int = DEFAULT_DATABASE_CALL_TIMEOUT_SECONDS def __post_init__(self) -> None: if self.worker_port < 1 or self.worker_port > 65_535: @@ -90,6 +104,14 @@ def __post_init__(self) -> None: raise ValueError( "session_start_timeout_seconds must be at least 1" ) + if self.conversation_retention_days < 0: + raise ValueError( + "conversation_retention_days must be non-negative" + ) + if self.database_call_timeout_seconds < 1: + raise ValueError( + "database_call_timeout_seconds must be at least 1" + ) object.__setattr__(self, "consul_url", self.consul_url.rstrip("/")) if self.worker_endpoint: object.__setattr__( @@ -114,27 +136,38 @@ class GatewaySettings: connection: ConnectionConfig = field(default_factory=ConnectionConfig) connection_form_template: tuple[dict, ...] | None = None require_oauth: bool = False + database_call_timeout_seconds: int = DEFAULT_DATABASE_CALL_TIMEOUT_SECONDS def __post_init__(self) -> None: if self.session_ttl_seconds < 1: raise ValueError("session_ttl_seconds must be at least 1") - tls_files = ( - self.worker_tls_ca_file, - self.worker_tls_cert_file, - self.worker_tls_key_file, - ) - if any(tls_files) and not all(tls_files): + if self.database_call_timeout_seconds < 1: + raise ValueError( + "database_call_timeout_seconds must be at least 1" + ) + if self.worker_tls_cert_file: + if self.worker_tls_key_file is None: + raise ValueError( + "worker client certificate and key must be supplied " + "together." + ) + elif self.worker_tls_key_file: raise ValueError( - "worker mTLS requires a CA file, client certificate, and " - "client key." + "worker client certificate and key must be supplied together." ) object.__setattr__(self, "public_url", self.public_url.rstrip("/")) object.__setattr__(self, "consul_url", self.consul_url.rstrip("/")) @property - def worker_mtls_enabled(self) -> bool: - """Whether gateway-to-worker calls require mutual TLS.""" - return self.worker_tls_ca_file is not None + def worker_connection_tls_configured(self) -> bool: + """Whether gateway-to-worker calls have explicit TLS settings.""" + return any( + ( + self.worker_tls_ca_file, + self.worker_tls_cert_file, + self.worker_tls_key_file, + ) + ) @dataclass(frozen=True) @@ -148,6 +181,7 @@ class StandaloneSessionSettings: connection: ConnectionConfig = field(default_factory=ConnectionConfig) connection_form_template: tuple[dict, ...] | None = None require_oauth: bool = False + database_call_timeout_seconds: int = DEFAULT_DATABASE_CALL_TIMEOUT_SECONDS def __post_init__(self) -> None: if self.session_ttl_seconds < 1: @@ -156,6 +190,10 @@ def __post_init__(self) -> None: raise ValueError( "session_start_timeout_seconds must be at least 1" ) + if self.database_call_timeout_seconds < 1: + raise ValueError( + "database_call_timeout_seconds must be at least 1" + ) object.__setattr__(self, "public_url", self.public_url.rstrip("/")) @@ -167,16 +205,34 @@ class SessionInfo: username: str password: str team_name: str + tls_ca_certificate_pem: str | None = field(default=None, repr=False) + + def __post_init__(self) -> None: + if self.tls_ca_certificate_pem is not None and ( + not isinstance(self.tls_ca_certificate_pem, str) + or not self.tls_ca_certificate_pem.strip() + ): + raise ValueError( + "tls_ca_certificate_pem must be a non-empty string" + ) @classmethod - def from_values(cls, values: dict) -> "SessionInfo": + def from_values( + cls, + values: dict, + *, + tls_ca_certificate_pem: str | None = None, + ) -> "SessionInfo": """Build a complete connection after resolution.""" if not all( isinstance(values.get(key), str) and values[key] for key in CONNECTION_FIELDS ): raise ValueError("All database connection fields are required.") - return cls(**{key: values[key] for key in CONNECTION_FIELDS}) + return cls( + **{key: values[key] for key in CONNECTION_FIELDS}, + tls_ca_certificate_pem=tls_ca_certificate_pem, + ) @dataclass(frozen=True) diff --git a/src/select_ai/agent/a2a/server.py b/src/select_ai/agent/a2a/server.py index 334c4d3..867c76b 100644 --- a/src/select_ai/agent/a2a/server.py +++ b/src/select_ai/agent/a2a/server.py @@ -7,6 +7,7 @@ """A2A HTTP server for Oracle Database AI Agent Teams.""" +import ssl from contextlib import asynccontextmanager from typing import Optional @@ -29,6 +30,7 @@ authentication_middleware, ) from select_ai.agent.a2a.context_store import OracleContextStore +from select_ai.agent.a2a.models import DEFAULT_DATABASE_CALL_TIMEOUT_SECONDS from select_ai.agent.a2a.results import add_team_result from select_ai.agent.a2a.task_store import OracleTaskStore from select_ai.version import __version__ @@ -88,10 +90,16 @@ def create_app( # noqa: PLR0913 description: Optional[str] = None, pool_max_size: int = 10, require_oauth: bool = False, + tls_ca_certificate_pem: Optional[str] = None, + database_call_timeout_seconds: int = ( + DEFAULT_DATABASE_CALL_TIMEOUT_SECONDS + ), ) -> Starlette: """Build an A2A JSON-RPC application for one database AI Agent Team.""" if pool_max_size < 1: raise ValueError("pool_max_size must be at least 1") + if database_call_timeout_seconds < 1: + raise ValueError("database_call_timeout_seconds must be at least 1") agent_card = _build_agent_card(team_name, public_url, description) if require_oauth: @@ -107,18 +115,27 @@ def create_app( # noqa: PLR0913 @asynccontextmanager async def lifespan(app): + async def set_database_call_timeout(connection, _requested_tag): + connection.call_timeout = database_call_timeout_seconds * 1_000 + connect_args = { "user": user, "password": password, "dsn": dsn, "min_size": 1, "max_size": pool_max_size, + "session_callback": set_database_call_timeout, } if wallet_location: connect_args["wallet_location"] = wallet_location connect_args["config_dir"] = wallet_location if wallet_password: connect_args["wallet_password"] = wallet_password + if tls_ca_certificate_pem: + connect_args["protocol"] = "tcps" + connect_args["ssl_context"] = ssl.create_default_context( + cadata=tls_ca_certificate_pem + ) select_ai.create_pool_async(**connect_args) try: await task_store.initialize() diff --git a/src/select_ai/agent/a2a/session_process.py b/src/select_ai/agent/a2a/session_process.py index 2f70b83..2c23e0a 100644 --- a/src/select_ai/agent/a2a/session_process.py +++ b/src/select_ai/agent/a2a/session_process.py @@ -18,6 +18,9 @@ from multiprocessing.connection import Connection from typing import Protocol +from select_ai.agent.a2a.models import ( + DEFAULT_DATABASE_CALL_TIMEOUT_SECONDS, +) from select_ai.agent.a2a.session_runtime import SessionRuntime from select_ai.agent.a2a.worker_protocol import ( PipeMessageType, @@ -27,6 +30,7 @@ LOGGER = logging.getLogger(__name__) _SESSION_REAPER_INTERVAL_SECONDS = 1 +_DATABASE_CALL_TIMEOUT_MS = DEFAULT_DATABASE_CALL_TIMEOUT_SECONDS * 1_000 @dataclass(frozen=True) @@ -39,6 +43,7 @@ class SessionSpec: username: str password: str team_name: str + tls_ca_certificate_pem: str | None = None @dataclass @@ -101,9 +106,22 @@ def __init__( self, session_ttl_seconds: int, session_start_timeout_seconds: int, + conversation_retention_days: int = 7, + database_call_timeout_seconds: int = ( + DEFAULT_DATABASE_CALL_TIMEOUT_SECONDS + ), ) -> None: self.session_ttl_seconds = session_ttl_seconds self.session_start_timeout_seconds = session_start_timeout_seconds + self.conversation_retention_days = conversation_retention_days + if database_call_timeout_seconds < 1: + raise ValueError( + "database_call_timeout_seconds must be at least 1" + ) + self.database_call_timeout_seconds = database_call_timeout_seconds + self.worker_dispatch_timeout_seconds = ( + database_call_timeout_seconds + 5 + ) self.sessions: dict[str, ChildSession] = {} async def open(self, spec: SessionSpec) -> None: @@ -114,6 +132,8 @@ async def open(self, spec: SessionSpec) -> None: "password": spec.password, "dsn": spec.dsn, } + if spec.tls_ca_certificate_pem is not None: + credentials["tls_ca_certificate_pem"] = spec.tls_ca_certificate_pem process = multiprocessing.Process( target=_session_process_main, args=( @@ -122,6 +142,8 @@ async def open(self, spec: SessionSpec) -> None: spec.session_id, spec.owner, spec.team_name, + self.conversation_retention_days, + self.database_call_timeout_seconds, ), daemon=True, ) @@ -172,6 +194,13 @@ async def dispatch( session = await self.get(session_id) response = None failure = None + started_at = time.monotonic() + LOGGER.debug( + "Worker command started: session_id=%s method=%s pid=%s", + session_id, + method, + session.process.pid, + ) async with session.lock: if session.process.is_alive(): try: @@ -183,28 +212,57 @@ async def dispatch( "payload": payload, }, ) + # Leave time for the database call timeout to return. response = await self._receive( session, - timeout_seconds=120, + timeout_seconds=self.worker_dispatch_timeout_seconds, ) except (EOFError, OSError, TimeoutError) as error: failure = error if failure is not None: + LOGGER.warning( + "Worker command pipe failed: session_id=%s method=%s " + "elapsed_ms=%s error_type=%s; discarding session", + session_id, + method, + int((time.monotonic() - started_at) * 1000), + type(failure).__name__, + ) await self._discard(session_id, session) raise SessionUnavailable( "Database session is unavailable; reconnect required." ) from failure if response is None: + LOGGER.warning( + "Worker session returned no response: session_id=%s method=%s " + "elapsed_ms=%s", + session_id, + method, + int((time.monotonic() - started_at) * 1000), + ) await self._discard(session_id, session) raise SessionNotFound( "Database session expired; reconnect required." ) if response.get("type") == PipeMessageType.RESULT.value: + LOGGER.debug( + "Worker command completed: session_id=%s method=%s elapsed_ms=%s", + session_id, + method, + int((time.monotonic() - started_at) * 1000), + ) return response.get( "result", WorkerResult(ResultKind.NONE), ) if response.get("type") == PipeMessageType.A2A_ERROR.value: + LOGGER.debug( + "Worker command returned an A2A error: session_id=%s " + "method=%s elapsed_ms=%s", + session_id, + method, + int((time.monotonic() - started_at) * 1000), + ) raise SessionCommandError( response.get("detail", "A2A request failed.") ) @@ -242,7 +300,18 @@ async def _reap_expired_sessions(self) -> None: for session_id, session in self.sessions.items() if session.expires_at <= now or not session.process.is_alive() ] - for session_id, _session in expired: + for session_id, session in expired: + reason = ( + "ttl_expired" + if session.expires_at <= now + else "process_exited" + ) + LOGGER.info( + "Reaping worker session: session_id=%s reason=%s pid=%s", + session_id, + reason, + session.process.pid, + ) self.sessions.pop(session_id, None) for session_id, session in expired: @@ -272,15 +341,19 @@ async def _wait_ready( return detail = response.get("detail", "Database login failed.") for secret in (spec.password, spec.username, spec.dsn): - detail = detail.replace(secret, "[REDACTED]") + if secret: + detail = detail.replace(secret, "[REDACTED]") + if spec.tls_ca_certificate_pem: + detail = detail.replace(spec.tls_ca_certificate_pem, "[REDACTED]") LOGGER.error("Select AI session startup failed: %s", detail[-2_000:]) raise SessionLoginError("Database login failed.") @staticmethod async def _receive( session: ChildSession, - timeout_seconds: float, + timeout_seconds: float | None, ) -> dict: + """Read a child response, optionally waiting without a deadline.""" available = await asyncio.to_thread( session.connection.poll, timeout_seconds, @@ -296,6 +369,11 @@ async def _discard(self, session_id: str, session: ChildSession) -> None: @staticmethod async def _terminate(session: ChildSession) -> None: + LOGGER.debug( + "Terminating worker session process: pid=%s alive=%s", + session.process.pid, + session.process.is_alive(), + ) async with session.lock: try: with contextlib.suppress(OSError): @@ -322,6 +400,10 @@ def _session_process_main( session_id: str, owner: str, team_name: str, + conversation_retention_days: int, + database_call_timeout_seconds: int = ( + DEFAULT_DATABASE_CALL_TIMEOUT_SECONDS + ), ) -> None: """Entrypoint for a child that owns one Select AI database session.""" try: @@ -332,6 +414,8 @@ def _session_process_main( session_id, owner, team_name, + conversation_retention_days, + database_call_timeout_seconds, ) ) finally: @@ -344,21 +428,47 @@ async def _run_session_process( session_id: str, owner: str, team_name: str, + conversation_retention_days: int = 7, + database_call_timeout_seconds: int = ( + DEFAULT_DATABASE_CALL_TIMEOUT_SECONDS + ), ) -> None: """Open one async connection and execute A2A operations.""" import select_ai + from select_ai.db import async_get_connection runtime: SessionRuntime | None = None ready = False try: + connection_options = {} + tls_ca_certificate_pem = credentials.get("tls_ca_certificate_pem") + if tls_ca_certificate_pem: + import ssl + + connection_options = { + "protocol": "tcps", + "ssl_context": ssl.create_default_context( + cadata=tls_ca_certificate_pem + ), + } await select_ai.async_connect( user=credentials["user"], password=credentials["password"], dsn=credentials["dsn"], + **connection_options, ) + async with async_get_connection() as database_connection: + database_connection.call_timeout = ( + database_call_timeout_seconds * 1_000 + ) if not await select_ai.async_is_connected(): raise RuntimeError("Database login failed.") - runtime = SessionRuntime(session_id, owner, team_name) + runtime = SessionRuntime( + session_id, + owner, + team_name, + conversation_retention_days, + ) await runtime.initialize() connection.send({"type": PipeMessageType.READY.value}) ready = True @@ -412,7 +522,11 @@ async def _handle_a2a_command( command.get("payload", b""), ) except Exception as error: - LOGGER.exception("Select AI session A2A command failed") + LOGGER.exception( + "Select AI session A2A command failed: session_id=%s method=%s", + runtime.session_id, + command.get("method"), + ) connection.send( {"type": PipeMessageType.A2A_ERROR.value, "detail": str(error)} ) diff --git a/src/select_ai/agent/a2a/session_runtime.py b/src/select_ai/agent/a2a/session_runtime.py index 086b049..a7cbf39 100644 --- a/src/select_ai/agent/a2a/session_runtime.py +++ b/src/select_ai/agent/a2a/session_runtime.py @@ -58,12 +58,20 @@ def user_name(self) -> str: class SessionRuntime: """Own the A2A handler and Oracle stores for one connected database.""" - def __init__(self, session_id: str, owner: str, team_name: str) -> None: + def __init__( + self, + session_id: str, + owner: str, + team_name: str, + conversation_retention_days: int = 7, + ) -> None: self.session_id = session_id self.owner = owner self.team_name = team_name self.task_store = OracleTaskStore() - self.context_store = OracleContextStore() + self.context_store = OracleContextStore( + retention_days=conversation_retention_days, + ) self.handler: DefaultRequestHandler | None = None async def initialize(self) -> None: diff --git a/src/select_ai/agent/a2a/worker.py b/src/select_ai/agent/a2a/worker.py index cb5b972..103f4af 100644 --- a/src/select_ai/agent/a2a/worker.py +++ b/src/select_ai/agent/a2a/worker.py @@ -11,6 +11,7 @@ import asyncio import contextlib +import logging import time from contextlib import asynccontextmanager @@ -36,6 +37,8 @@ WORKER_RESULT_KIND_HEADER, ) +LOGGER = logging.getLogger(__name__) + class OpenSessionRequest(BaseModel): """Sensitive request accepted only on the internal worker interface.""" @@ -46,6 +49,9 @@ class OpenSessionRequest(BaseModel): username: str = Field(min_length=1, max_length=128) password: SecretStr = Field(min_length=1, max_length=1_024) team_name: str = Field(min_length=1, max_length=128) + tls_ca_certificate_pem: str | None = Field( + default=None, min_length=1, max_length=64_000 + ) def to_session_spec(self) -> SessionSpec: """Convert validated HTTP input to the backend-neutral contract.""" @@ -56,6 +62,7 @@ def to_session_spec(self) -> SessionSpec: username=self.username, password=self.password.get_secret_value(), team_name=self.team_name, + tls_ca_certificate_pem=self.tls_ca_certificate_pem, ) @@ -130,6 +137,8 @@ def create_worker_app(settings: WorkerSettings) -> FastAPI: backend = ProcessSessionBackend( settings.session_ttl_seconds, settings.session_start_timeout_seconds, + settings.conversation_retention_days, + settings.database_call_timeout_seconds, ) @asynccontextmanager @@ -197,7 +206,21 @@ async def handle_a2a( await request.body(), ) except SessionBackendError as error: - raise _http_error(error) from error + http_error = _http_error(error) + log = ( + LOGGER.warning + if isinstance(error, SessionUnavailable) + else LOGGER.debug + ) + log( + "Worker A2A request failed: session_id=%s method=%s " + "error_type=%s status_code=%s", + session_id, + method, + type(error).__name__, + http_error.status_code, + ) + raise http_error from error return Response( content=result.payload, media_type=PROTOBUF_CONTENT_TYPE, diff --git a/src/select_ai/agent/a2a/worker_client.py b/src/select_ai/agent/a2a/worker_client.py index 71e6df5..af28f98 100644 --- a/src/select_ai/agent/a2a/worker_client.py +++ b/src/select_ai/agent/a2a/worker_client.py @@ -14,6 +14,7 @@ import json import logging import time +from dataclasses import dataclass from threading import Lock from urllib.parse import quote from uuid import uuid4 @@ -46,6 +47,14 @@ _TASK_PREFIX = "select-ai-a2a/tasks/" +@dataclass(frozen=True) +class _ConsulSessionRoute: + """A worker route and the Consul session that owns its KV entries.""" + + route: SessionRoute + consul_session_id: str + + class ReconnectRequired(RuntimeError): """The worker no longer owns the requested in-memory session.""" @@ -62,14 +71,13 @@ def __init__(self, settings: GatewaySettings): self._selection_lock = Lock() self._next_worker = 0 self._worker_request_kwargs: dict[str, object] = {} - if settings.worker_mtls_enabled: - self._worker_request_kwargs = { - "verify": settings.worker_tls_ca_file, - "cert": ( - settings.worker_tls_cert_file, - settings.worker_tls_key_file, - ), - } + if settings.worker_tls_ca_file: + self._worker_request_kwargs["verify"] = settings.worker_tls_ca_file + if settings.worker_tls_cert_file: + self._worker_request_kwargs["cert"] = ( + settings.worker_tls_cert_file, + settings.worker_tls_key_file, + ) def open_session( self, @@ -79,26 +87,35 @@ def open_session( ) -> str: """Open a session and save a non-secret route in Consul.""" session_id = str(uuid4()) - endpoint = self._select_worker() - response = requests.post( - f"{endpoint}/sessions", - json={ - "session_id": session_id, - "owner": owner, - **session_info.__dict__, - }, - timeout=45, - **self._worker_request_kwargs, - ) - response.raise_for_status() + worker = self._select_worker_record() + endpoint = self._worker_endpoint(worker) + expires_at = time.time() + self.settings.session_ttl_seconds + consul_session_id = self._create_consul_session(worker) route = SessionRoute( endpoint=endpoint, - expires_at=time.time() + self.settings.session_ttl_seconds, + expires_at=expires_at, session_id=session_id, ) - if not self._save_route(owner, context_id, route): + try: + response = requests.post( + f"{endpoint}/sessions", + json={ + "session_id": session_id, + "owner": owner, + **session_info.__dict__, + }, + timeout=45, + **self._worker_request_kwargs, + ) + response.raise_for_status() + if not self._save_route( + owner, context_id, route, consul_session_id + ): + raise RuntimeError("Could not create the database session.") + except Exception: self._close_worker_session(route, session_id) - raise RuntimeError("Could not create the database session.") + self._destroy_consul_session(consul_session_id) + raise return session_id def send_message( @@ -195,7 +212,11 @@ def task_context_id(self, owner: str, task_id: str) -> str | None: values = response.json() or [] if not values or not values[0].get("Value"): return None - return base64.b64decode(values[0]["Value"]).decode() + entry = values[0] + if not entry.get("Session"): + self._delete_task_route(owner, task_id) + return None + return base64.b64decode(entry["Value"]).decode() def _dispatch( self, @@ -206,17 +227,50 @@ def _dispatch( ) -> WorkerResult: """Send one protobuf-serialized A2A operation to a worker session.""" route = self._route_for(owner, context_id) - response = requests.post( - f"{route.endpoint}/sessions/{quote(route.session_id, safe='')}/a2a", - data=message.SerializeToString(), - headers={ - "content-type": PROTOBUF_CONTENT_TYPE, - WORKER_A2A_METHOD_HEADER: A2AMethod(method).value, - }, - timeout=130, - **getattr(self, "_worker_request_kwargs", {}), + started_at = time.monotonic() + LOGGER.debug( + "Worker request started: session_id=%s method=%s", + route.session_id, + A2AMethod(method).value, + ) + try: + response = requests.post( + f"{route.endpoint}/sessions/{quote(route.session_id, safe='')}/a2a", + data=message.SerializeToString(), + headers={ + "content-type": PROTOBUF_CONTENT_TYPE, + WORKER_A2A_METHOD_HEADER: A2AMethod(method).value, + }, + # Allow the worker deadline and child-process cleanup to finish. + timeout=self.settings.database_call_timeout_seconds + 20, + **getattr(self, "_worker_request_kwargs", {}), + ) + except requests.RequestException as error: + LOGGER.warning( + "Worker request failed: session_id=%s method=%s elapsed_ms=%s " + "error_type=%s", + route.session_id, + A2AMethod(method).value, + int((time.monotonic() - started_at) * 1000), + type(error).__name__, + ) + raise + elapsed_ms = int((time.monotonic() - started_at) * 1000) + LOGGER.debug( + "Worker response received: session_id=%s method=%s " + "status_code=%s elapsed_ms=%s", + route.session_id, + A2AMethod(method).value, + response.status_code, + elapsed_ms, ) if response.status_code in (404, 502): + LOGGER.warning( + "Worker marked session unavailable: session_id=%s " + "status_code=%s; removing route", + route.session_id, + response.status_code, + ) self._close_worker_session(route, context_id, owner) raise ReconnectRequired( "Database session ended; reconnect required.", @@ -239,17 +293,21 @@ def _save_task_route( task_id: str, context_id: str, ) -> None: - """Save only task-to-session routing metadata in Consul.""" + """Save task routing metadata under its worker session's lease.""" try: + routed_session = self._route_with_consul_session(owner, context_id) response = requests.put( f"{self.settings.consul_url}/v1/kv/{_TASK_PREFIX}" f"{_owner_key(owner)}/" f"{quote(task_id, safe='')}", + params={"acquire": routed_session.consul_session_id}, data=context_id, timeout=10, ) response.raise_for_status() - except requests.RequestException: + if response.text.strip().lower() != "true": + LOGGER.warning("Could not acquire route for task %s", task_id) + except (ReconnectRequired, requests.RequestException): # The task itself is already durable in Oracle, but task routes # are required because workers may use different databases. LOGGER.warning("Could not save route for task %s", task_id) @@ -272,6 +330,11 @@ def close_session(self, owner: str, context_id: str) -> None: self._close_worker_session(route, context_id, owner) def _select_worker(self) -> str: + """Return a healthy worker endpoint, preserving the public contract.""" + return self._worker_endpoint(self._select_worker_record()) + + def _select_worker_record(self) -> dict: + """Return the selected Consul catalog record for a healthy worker.""" response = requests.get( f"{self.settings.consul_url}/v1/health/service/" f"{self.settings.worker_service}", @@ -285,28 +348,39 @@ def _select_worker(self) -> str: with self._selection_lock: worker = workers[self._next_worker % len(workers)] self._next_worker += 1 + return worker + + def _worker_endpoint(self, worker: dict) -> str: + """Resolve a selected Consul worker record to its endpoint.""" service = worker["Service"] metadata = service.get("Meta") or {} endpoint = metadata.get("endpoint") - if endpoint: + if not endpoint: + address = service.get("Address") or worker["Node"]["Address"] + endpoint = f"http://{address}:{service['Port']}" + else: endpoint = endpoint.rstrip("/") - if self.settings.worker_mtls_enabled and not endpoint.startswith( - "https://" - ): - raise RuntimeError( - "A worker registered a non-HTTPS endpoint while mTLS is " - "required." - ) - return endpoint - if self.settings.worker_mtls_enabled: + + if ( + self.settings.worker_connection_tls_configured + and not endpoint.startswith("https://") + ): raise RuntimeError( - "Workers must register an HTTPS endpoint while mTLS is " - "required." + "Worker endpoint must use HTTPS when gateway-to-worker TLS " + "options are configured." ) - address = service.get("Address") or worker["Node"]["Address"] - return f"http://{address}:{service['Port']}" + return endpoint def _route_for(self, owner: str, context_id: str) -> SessionRoute: + """Return a live worker route backed by a Consul session.""" + return self._route_with_consul_session(owner, context_id).route + + def _route_with_consul_session( + self, + owner: str, + context_id: str, + ) -> _ConsulSessionRoute: + """Read a route and its Consul lock/session metadata.""" response = requests.get( f"{self.settings.consul_url}/v1/kv/{_SESSION_PREFIX}" f"{_owner_key(owner)}/{quote(context_id, safe='')}", @@ -318,7 +392,22 @@ def _route_for(self, owner: str, context_id: str) -> SessionRoute: context_id, ) response.raise_for_status() - value = response.json()[0]["Value"] + entries = response.json() or [] + if not entries or not entries[0].get("Value"): + raise ReconnectRequired( + "Database session expired; reconnect required.", + context_id, + ) + entry = entries[0] + consul_session_id = entry.get("Session") + if not consul_session_id: + # Discard an old, unleased route rather than treating it as live. + self._delete_route_key(owner, context_id) + raise ReconnectRequired( + "Database session expired; reconnect required.", + context_id, + ) + value = entry["Value"] route = SessionRoute(**json.loads(base64.b64decode(value).decode())) if route.expires_at <= time.time(): self._close_worker_session(route, context_id, owner) @@ -326,29 +415,98 @@ def _route_for(self, owner: str, context_id: str) -> SessionRoute: "Database session expired; reconnect required.", context_id, ) - return route + return _ConsulSessionRoute(route, consul_session_id) + + def _create_consul_session(self, worker: dict) -> str: + """Create a delete-on-expiry Consul session for one worker route.""" + ttl = self.settings.session_ttl_seconds + if ttl < 10 or ttl > 86_400: + raise ValueError( + "Consul session TTL must be between 10 and 86400 seconds." + ) + + node = (worker.get("Node") or {}).get("Node") + service_id = (worker.get("Service") or {}).get("ID") + if not node or not service_id: + raise RuntimeError( + "Consul worker record is missing its node or service ID." + ) + + response = requests.put( + f"{self.settings.consul_url}/v1/session/create", + json={ + "Name": "select-ai-a2a-worker-session", + "Node": node, + "NodeChecks": ["serfHealth"], + "ServiceChecks": [{"ID": f"service:{service_id}"}], + "TTL": f"{ttl}s", + "Behavior": "delete", + }, + timeout=10, + ) + response.raise_for_status() + consul_session_id = (response.json() or {}).get("ID") + if not consul_session_id: + raise RuntimeError("Consul did not return a session ID.") + return consul_session_id def _save_route( self, owner: str, context_id: str, route: SessionRoute, + consul_session_id: str, ) -> bool: response = requests.put( f"{self.settings.consul_url}/v1/kv/{_SESSION_PREFIX}" - f"{_owner_key(owner)}/{quote(context_id, safe='')}?cas=0", + f"{_owner_key(owner)}/{quote(context_id, safe='')}", + params={"acquire": consul_session_id}, data=json.dumps(route.__dict__), timeout=10, ) return response.ok and response.text.strip().lower() == "true" def _delete_route(self, owner: str, context_id: str) -> None: + """Destroy the owning Consul session, then remove the route key.""" + key_url = ( + f"{self.settings.consul_url}/v1/kv/{_SESSION_PREFIX}" + f"{_owner_key(owner)}/{quote(context_id, safe='')}" + ) + try: + response = requests.get(key_url, timeout=10) + if response.status_code == 404: + return + response.raise_for_status() + entries = response.json() or [] + consul_session_id = entries[0].get("Session") if entries else None + if consul_session_id: + self._destroy_consul_session(consul_session_id) + except requests.RequestException: + LOGGER.warning("Could not read Consul session for route cleanup") + self._delete_route_key(owner, context_id) + + def _delete_route_key(self, owner: str, context_id: str) -> None: + """Delete only the session route KV entry.""" requests.delete( f"{self.settings.consul_url}/v1/kv/{_SESSION_PREFIX}" f"{_owner_key(owner)}/{quote(context_id, safe='')}", timeout=10, ) + def _destroy_consul_session(self, consul_session_id: str) -> None: + """Destroy a Consul session; its acquired KV entries are deleted.""" + try: + response = requests.put( + f"{self.settings.consul_url}/v1/session/destroy/" + f"{quote(consul_session_id, safe='')}", + timeout=10, + ) + response.raise_for_status() + except requests.RequestException: + LOGGER.warning( + "Could not destroy Consul session %s", consul_session_id + ) + def _close_worker_session( self, route: SessionRoute, diff --git a/src/select_ai/agent/tool.py b/src/select_ai/agent/tool.py index 37f9f8e..c3987d3 100644 --- a/src/select_ai/agent/tool.py +++ b/src/select_ai/agent/tool.py @@ -359,7 +359,7 @@ def create_built_in_tool( :param str tool_name: The name of the tool :param select_ai.agent.ToolParams tool_params: Parameters required by built-in tool - :param select_ai.agent.ToolType tool_type: The built-in tool type + :param select_ai.agent.tool.ToolType tool_type: The built-in tool type :param str description: Description of the tool :param bool replace: Whether to replace the existing tool. Default value is False @@ -854,7 +854,7 @@ async def create_built_in_tool( :param str tool_name: The name of the tool :param select_ai.agent.ToolParams tool_params: Parameters required by built-in tool - :param select_ai.agent.ToolType tool_type: The built-in tool type + :param select_ai.agent.tool.ToolType tool_type: The built-in tool type :param str description: Description of the tool :param bool replace: Whether to replace the existing tool. Default value is False diff --git a/src/select_ai/async_profile.py b/src/select_ai/async_profile.py index bb82e18..097348b 100644 --- a/src/select_ai/async_profile.py +++ b/src/select_ai/async_profile.py @@ -145,7 +145,7 @@ async def _get_attributes( :param str profile_name: Name of the profile :param str owner: Owner of a shared profile. Defaults to current schema. :param bool raise_on_empty: Raise an error if attributes are empty - :return: select_ai.provider.ProviderAttributes + :return: select_ai.ProfileAttributes :raises: select_ai.errors.ProfileAttributesEmptyError """ @@ -168,7 +168,7 @@ async def _get_attributes( async def get_attributes(self) -> ProfileAttributes: """Asynchronously gets AI profile attributes from the Database - :return: select_ai.provider.ProviderAttributes + :return: select_ai.ProfileAttributes :raises: ProfileNotFoundError """ return await self._get_attributes( @@ -405,8 +405,8 @@ async def add_positive_feedback( """ Give positive feedback to the LLM - :param Tuple[str, Action] prompt_spec: First element is the prompt and - second is the corresponding action + :param Tuple[str, select_ai.action.Action] prompt_spec: + First element is the prompt; second is the corresponding action :param str sql_id: SQL identifier from V$MAPPED_SQL view """ await self._save_feedback( @@ -425,8 +425,8 @@ async def add_negative_feedback( """ Give negative feedback to the LLM - :param Tuple[str, Action] prompt_spec: First element is the prompt and - second is the corresponding action + :param Tuple[str, select_ai.action.Action] prompt_spec: + First element is the prompt; second is the corresponding action :param str sql_id: SQL identifier from V$MAPPED_SQL view :param str response: Expected SQL from LLM :param str feedback_content: Actual feedback in natural language @@ -447,8 +447,8 @@ async def delete_feedback( """ Delete feedback from the database - :param Tuple[str, Action] prompt_spec: First element is the prompt and - second is the corresponding action + :param Tuple[str, select_ai.action.Action] prompt_spec: + First element is the prompt; second is the corresponding action :param str sql_id: SQL identifier from V$MAPPED_SQL view """ @@ -500,7 +500,7 @@ async def _generate_with_cursor( """Asynchronously perform AI translation using this profile :param str prompt: Natural language prompt to translate - :param select_ai.profile.Action action: + :param select_ai.action.Action action: :param params: Parameters to include in the LLM request. For e.g. conversation_id for context-aware chats :param Mapping attributes: Profile attributes to override for this @@ -613,7 +613,7 @@ async def generate( """Asynchronously perform AI translation using this profile :param str prompt: Natural language prompt to translate - :param select_ai.profile.Action action: + :param select_ai.action.Action action: :param params: Parameters to include in the LLM request. For e.g. conversation_id for context-aware chats :param Mapping attributes: Profile attributes to override for this @@ -893,17 +893,23 @@ async def run_pipeline( continue_on_error: bool = False, *, attributes: Mapping = None, - ) -> List[Union[str, pandas.DataFrame]]: - """Send Multiple prompts in a single roundtrip to the Database + ) -> List[object]: + """Submit prompt/action pairs as a database pipeline - :param List[Tuple[str, Action]] prompt_specifications: List of - 2-element tuples. First element is the prompt and second is the + :param List[Tuple[str, select_ai.action.Action]] prompt_specifications: + List of 2-element tuples. First element is the prompt and second is the corresponding action :param bool continue_on_error: True to continue on error else False :param Mapping attributes: Profile attributes to override for every request in the pipeline - :return: List[Union[str, pandas.DataFrame]] + :return: Ordered list of strings for successful operations and driver + error objects for failures when continue_on_error=True. RUNSQL + results remain JSON strings; they are not converted to DataFrames. + + Operations execute sequentially. Round-trip reduction requires a + supporting database version; reading returned CLOBs adds calls. + With continue_on_error=False, an operation failure raises an exception. """ serialized_attributes = None if attributes is not None: diff --git a/src/select_ai/cli/a2a.py b/src/select_ai/cli/a2a.py index 8e0d622..360ecb5 100644 --- a/src/select_ai/cli/a2a.py +++ b/src/select_ai/cli/a2a.py @@ -6,27 +6,68 @@ # ----------------------------------------------------------------------------- import json +import logging import os import socket import ssl import click +from select_ai.agent.a2a.models import ( + DEFAULT_DATABASE_CALL_TIMEOUT_SECONDS, +) from select_ai.cli.common import connection_options from select_ai.version import __version__ -try: - import uvicorn - from select_ai.agent.a2a.server import create_app -except ImportError: - create_app = None - uvicorn = None +def _require_a2a_support(): + """Load optional A2A server dependencies when an A2A runtime is used.""" + try: + import uvicorn + from select_ai.agent.a2a.server import create_app + except ImportError as error: + raise click.ClickException( + "A2A dependencies are missing. Install them with: " + "pip install 'select_ai[a2a]'" + ) from error + return uvicorn, create_app + + +def _uvicorn_tls_options(cert_file, key_file, ca_file): + """Build Uvicorn TLS settings; a CA enables client certificate checks.""" + if cert_file and key_file: + options = { + "ssl_certfile": cert_file, + "ssl_keyfile": key_file, + } + if ca_file: + options.update( + { + "ssl_ca_certs": ca_file, + "ssl_cert_reqs": ssl.CERT_REQUIRED, + } + ) + return options -@click.group() -def a2a(): + if cert_file or key_file: + raise click.ClickException( + "--tls-cert-file and --tls-key-file must be supplied together." + ) + if ca_file: + raise click.ClickException( + "--tls-ca-file requires --tls-cert-file and --tls-key-file." + ) + return {} + + +@click.group(invoke_without_command=True) +@click.pass_context +def a2a(context): """Serve Select AI database agent teams through A2A.""" + if context.invoked_subcommand is None: + _require_a2a_support() + click.echo(context.get_help()) @a2a.command() @@ -51,6 +92,21 @@ def a2a(): envvar="PORT", type=click.IntRange(min=1, max=65_535), ) +@click.option( + "--tls-cert-file", + type=click.Path(exists=True, dir_okay=False, readable=True), + help="A2A server TLS certificate. Requires --tls-key-file.", +) +@click.option( + "--tls-key-file", + type=click.Path(exists=True, dir_okay=False, readable=True), + help="A2A server TLS private key. Requires --tls-cert-file.", +) +@click.option( + "--tls-ca-file", + type=click.Path(exists=True, dir_okay=False, readable=True), + help="CA used to require and validate client certificates (mTLS).", +) @click.option( "--public-url", envvar="PUBLIC_URL", @@ -85,11 +141,20 @@ def a2a(): type=click.IntRange(min=1), envvar="SESSION_TTL_SECONDS", ) +@click.option( + "--database-call-timeout-seconds", + default=DEFAULT_DATABASE_CALL_TIMEOUT_SECONDS, + show_default=True, + type=click.IntRange(min=1), + help=( + "Oracle call timeout; clustered gateway deadline uses the same value." + ), +) @click.option( "--worker-tls-ca-file", envvar="WORKER_TLS_CA_FILE", type=click.Path(exists=True, dir_okay=False, readable=True), - help="CA bundle used to validate clustered workers.", + help="CA bundle for worker certificates; omit to use system trust.", ) @click.option( "--worker-tls-cert-file", @@ -103,6 +168,12 @@ def a2a(): type=click.Path(exists=True, dir_okay=False, readable=True), help="Client private key presented to clustered workers.", ) +@click.option( + "--database-tls-ca-file", + envvar="SELECT_AI_DATABASE_TLS_CA_FILE", + type=click.Path(exists=True, dir_okay=False, readable=True), + help="PEM CA certificate file used to validate the Oracle Database server.", +) @click.option( "--a2ui-form", type=click.Path(exists=True, dir_okay=False, readable=True), @@ -123,15 +194,20 @@ def serve( team_name, host, port, + tls_cert_file, + tls_key_file, + tls_ca_file, public_url, description, pool_max_size, consul_url, worker_service, session_ttl_seconds, + database_call_timeout_seconds, worker_tls_ca_file, worker_tls_cert_file, worker_tls_key_file, + database_tls_ca_file, a2ui_form, require_oauth, user, @@ -141,14 +217,16 @@ def serve( wallet_password, ): """Start the public A2A server in standalone or clustered mode.""" - if uvicorn is None: - raise click.ClickException( - "A2A server support requires the optional 'cli' extra. " - "Install it with: pip install 'select_ai[cli]'" - ) + uvicorn, create_app = _require_a2a_support() + uvicorn_options = _uvicorn_tls_options( + tls_cert_file, + tls_key_file, + tls_ca_file, + ) if public_url is None: - public_url = f"http://{host}:{port}" + scheme = "https" if tls_cert_file else "http" + public_url = f"{scheme}://{host}:{port}" try: from select_ai.agent.a2a import ( @@ -165,11 +243,23 @@ def serve( "Install it with: pip install 'select_ai[a2a]'" ) from error + tls_ca_certificate_pem = None + if database_tls_ca_file: + try: + with open(database_tls_ca_file, encoding="utf-8") as ca_file: + tls_ca_certificate_pem = ca_file.read() + except (OSError, UnicodeError) as error: + raise click.ClickException( + "Could not read database CA certificate file: " + f"{database_tls_ca_file}" + ) from error + connection = ConnectionConfig( dsn=dsn, username=user, password=password, team_name=team_name, + tls_ca_certificate_pem=tls_ca_certificate_pem, ) form_template = None if a2ui_form: @@ -198,6 +288,8 @@ def serve( description=description, pool_max_size=pool_max_size, require_oauth=require_oauth, + tls_ca_certificate_pem=tls_ca_certificate_pem, + database_call_timeout_seconds=database_call_timeout_seconds, ) elif deployment == "standalone": if os.environ.get("WEB_CONCURRENCY", "1") != "1": @@ -208,6 +300,7 @@ def serve( public_url=public_url, session_ttl_seconds=session_ttl_seconds, description=description, + database_call_timeout_seconds=database_call_timeout_seconds, connection=connection, connection_form_template=form_template, require_oauth=require_oauth, @@ -219,6 +312,7 @@ def serve( consul_url=consul_url, worker_service=worker_service, session_ttl_seconds=session_ttl_seconds, + database_call_timeout_seconds=database_call_timeout_seconds, worker_tls_ca_file=worker_tls_ca_file, worker_tls_cert_file=worker_tls_cert_file, worker_tls_key_file=worker_tls_key_file, @@ -232,7 +326,7 @@ def serve( click.echo( f"A2A Agent Card: {public_url.rstrip('/')}/.well-known/agent-card.json" ) - uvicorn.run(app, host=host, port=port) + uvicorn.run(app, host=host, port=port, **uvicorn_options) @a2a.command("worker") @@ -268,26 +362,54 @@ def serve( show_default=True, type=click.IntRange(min=1), ) +@click.option( + "--conversation-retention-days", + default=7, + show_default=True, + type=click.IntRange(min=0), + envvar="CONVERSATION_RETENTION_DAYS", + help=( + "Days to retain Oracle conversations. Use 0 to keep them until " + "manually deleted." + ), +) @click.option( "--session-start-timeout-seconds", default=30, show_default=True, type=click.IntRange(min=1), ) +@click.option( + "--database-call-timeout-seconds", + default=DEFAULT_DATABASE_CALL_TIMEOUT_SECONDS, + show_default=True, + type=click.IntRange(min=1), + help="Maximum duration of an individual Oracle Database call.", +) +@click.option( + "--log-level", + type=click.Choice( + ("critical", "error", "warning", "info", "debug"), + case_sensitive=False, + ), + default="info", + show_default=True, + help="Logging level for Select AI worker and Uvicorn logs.", +) @click.option( "--tls-cert-file", type=click.Path(exists=True, dir_okay=False, readable=True), - help="Worker TLS server certificate. Requires all --tls-* options.", + help="Worker TLS server certificate. Requires --tls-key-file.", ) @click.option( "--tls-key-file", type=click.Path(exists=True, dir_okay=False, readable=True), - help="Worker TLS server private key. Requires all --tls-* options.", + help="Worker TLS server private key. Requires --tls-cert-file.", ) @click.option( "--tls-ca-file", type=click.Path(exists=True, dir_okay=False, readable=True), - help="CA used to validate the gateway client certificate.", + help="Optional CA used to require and validate client certificates (mTLS).", ) def worker( host, @@ -296,12 +418,16 @@ def worker( consul_url, worker_endpoint, session_ttl_seconds, + conversation_retention_days, session_start_timeout_seconds, + database_call_timeout_seconds, + log_level, tls_cert_file, tls_key_file, tls_ca_file, ): """Start the internal Select AI session worker.""" + uvicorn, _ = _require_a2a_support() try: from select_ai.agent.a2a import WorkerSettings, create_worker_app except ImportError as error: @@ -310,6 +436,12 @@ def worker( "Install it with: pip install 'select_ai[a2a]'" ) from error + uvicorn_options = _uvicorn_tls_options( + tls_cert_file, + tls_key_file, + tls_ca_file, + ) + settings = WorkerSettings( consul_url=consul_url, worker_id=worker_id, @@ -317,24 +449,23 @@ def worker( worker_port=port, session_ttl_seconds=session_ttl_seconds, session_start_timeout_seconds=session_start_timeout_seconds, + database_call_timeout_seconds=database_call_timeout_seconds, + conversation_retention_days=conversation_retention_days, worker_endpoint=worker_endpoint, ) app = create_worker_app(settings) - tls_files = (tls_cert_file, tls_key_file, tls_ca_file) - if any(tls_files) and not all(tls_files): - raise click.ClickException( - "Worker mTLS requires --tls-cert-file, --tls-key-file, and " - "--tls-ca-file." - ) - uvicorn_options = {} - if tls_cert_file: - uvicorn_options = { - "ssl_certfile": tls_cert_file, - "ssl_keyfile": tls_key_file, - "ssl_ca_certs": tls_ca_file, - "ssl_cert_reqs": ssl.CERT_REQUIRED, - } - uvicorn.run(app, host=host, port=port, **uvicorn_options) + logging.basicConfig(level=logging.INFO, force=True) + logging.getLogger("select_ai").setLevel(log_level.upper()) + + logging.getLogger("httpx").setLevel(logging.WARNING) + + uvicorn.run( + app, + host=host, + port=port, + log_level=log_level.lower(), + **uvicorn_options, + ) @a2a.command("agent-card") diff --git a/src/select_ai/profile.py b/src/select_ai/profile.py index 33f45fc..ab508da 100644 --- a/src/select_ai/profile.py +++ b/src/select_ai/profile.py @@ -195,7 +195,7 @@ def set_attributes(self, attributes: ProfileAttributes): """Updates AI profile attributes on the Python object and also saves it in the database - :param ProviderAttributes attributes: Object specifying AI profile + :param select_ai.ProfileAttributes attributes: Object specifying AI profile attributes :return: None """ @@ -391,8 +391,8 @@ def add_positive_feedback( """ Give positive feedback to the LLM - :param Tuple[str, Action] prompt_spec: First element is the prompt and - second is the corresponding action + :param Tuple[str, select_ai.action.Action] prompt_spec: + First element is the prompt; second is the corresponding action :param str sql_id: SQL identifier from V$MAPPED_SQL view """ self._save_feedback( @@ -411,8 +411,8 @@ def add_negative_feedback( """ Give negative feedback to the LLM - :param Tuple[str, Action] prompt_spec: First element is the prompt and - second is the corresponding action + :param Tuple[str, select_ai.action.Action] prompt_spec: + First element is the prompt; second is the corresponding action :param str sql_id: SQL identifier from V$MAPPED_SQL view :param str response: Expected SQL from LLM :param str feedback_content: Actual feedback in natural language @@ -433,8 +433,8 @@ def delete_feedback( """ Delete feedback from the database - :param Tuple[str, Action] prompt_spec: First element is the prompt and - second is the corresponding action + :param Tuple[str, select_ai.action.Action] prompt_spec: + First element is the prompt; second is the corresponding action :param str sql_id: SQL identifier from V$MAPPED_SQL view """ @@ -485,7 +485,7 @@ def _generate_with_cursor( """Perform AI translation using this profile :param str prompt: Natural language prompt to translate - :param select_ai.profile.Action action: + :param select_ai.action.Action action: :param params: Parameters to include in the LLM request. For e.g. conversation_id for context-aware chats :param Mapping attributes: Profile attributes to override for this @@ -595,7 +595,7 @@ def generate( """Perform AI translation using this profile :param str prompt: Natural language prompt to translate - :param select_ai.profile.Action action: + :param select_ai.action.Action action: :param params: Parameters to include in the LLM request. For e.g. conversation_id for context-aware chats :param Mapping attributes: Profile attributes to override for this diff --git a/src/select_ai/provider.py b/src/select_ai/provider.py index 83e547a..d53aab2 100644 --- a/src/select_ai/provider.py +++ b/src/select_ai/provider.py @@ -92,7 +92,7 @@ def profile_dict(self, exclude_null=True): The result contains only values held by this provider instance. In particular, native provider endpoints remain available for network access configuration but are omitted from database profile payloads. - OpenAI and endpoint-only custom providers retain provider_endpoint. + Endpoint-only custom providers retain provider_endpoint. """ attributes = self.dict(exclude_null=exclude_null) if not self.should_include_provider_endpoint(): @@ -135,12 +135,19 @@ def should_include_provider_endpoint(self) -> bool: @dataclass class OpenAIProvider(Provider): """ - OpenAI specific attributes + OpenAI specific attributes. + + The default hostname is retained for network ACL configuration and omitted + from database profiles so DBMS_CLOUD_AI selects its native public endpoint. + Use Provider for a custom OpenAI-compatible endpoint. """ provider_name: str = OPENAI provider_endpoint: Optional[str] = "api.openai.com" + def should_include_provider_endpoint(self) -> bool: + return False + @dataclass class OCIGenAIProvider(Provider): diff --git a/src/select_ai/vector_index.py b/src/select_ai/vector_index.py index a18384f..11c8b19 100644 --- a/src/select_ai/vector_index.py +++ b/src/select_ai/vector_index.py @@ -64,9 +64,11 @@ class VectorIndexAttributes(SelectAIDataClass): The unit is minutes. :param float similarity_threshold: Defines the minimum level of similarity required for two items to be considered a match - :param VectorDistanceMetric vector_distance_metric: Specifies the type of + :param select_ai.vector_index.VectorDistanceMetric vector_distance_metric: + Specifies the type of distance calculation used to compare vectors in a database - :param VectorDBProvider vector_db_provider: Name of the Vector database + :param select_ai.vector_index.VectorDBProvider vector_db_provider: + Name of the Vector database provider. Default value is "oracle" :param str vector_db_endpoint: Endpoint to access the Vector database :param str vector_db_credential_name: Name of the credentials for accessing diff --git a/tests/a2a/test_worker_runtime.py b/tests/a2a/test_worker_runtime.py index 4c26b79..3fab394 100644 --- a/tests/a2a/test_worker_runtime.py +++ b/tests/a2a/test_worker_runtime.py @@ -8,6 +8,7 @@ """Unit tests for the private multiprocessing session runtime.""" import asyncio +from contextlib import asynccontextmanager import pytest import requests @@ -86,6 +87,7 @@ def __init__(self, *, target, args, daemon): self.target = target self.args = args self.daemon = daemon + self.pid = 1234 self.started = False self.alive = True @@ -284,10 +286,13 @@ def test_session_runtime_uses_one_async_connection_and_dispatches_a2a( connection_arguments = {} class Runtime: - def __init__(self, session_id, owner, team_name): + def __init__( + self, session_id, owner, team_name, conversation_retention_days + ): assert session_id == "session-1" assert owner == "owner-1" assert team_name == "TEAM" + assert conversation_retention_days == 7 async def initialize(self): return None @@ -306,9 +311,20 @@ async def connected(): async def disconnect(): return None + database_connection = type("DatabaseConnection", (), {})() + + @asynccontextmanager + async def async_get_connection(): + yield database_connection + monkeypatch.setattr(select_ai, "async_connect", async_connect) monkeypatch.setattr(select_ai, "async_is_connected", connected) monkeypatch.setattr(select_ai, "async_disconnect", disconnect) + monkeypatch.setattr( + select_ai.db, + "async_get_connection", + async_get_connection, + ) monkeypatch.setattr(session_process, "SessionRuntime", Runtime) asyncio.run( @@ -330,6 +346,10 @@ async def disconnect(): "password": "password", "dsn": "database", } + assert ( + database_connection.call_timeout + == session_process._DATABASE_CALL_TIMEOUT_MS + ) assert connection.sent[0] == {"type": "ready"} assert connection.sent[1] == { "type": "result", @@ -347,6 +367,9 @@ async def fetch(team_name): initialized.append("team") class Store: + def __init__(self, retention_days=7): + self.retention_days = retention_days + async def initialize(self): initialized.append(self) @@ -393,6 +416,9 @@ async def fetch(team_name): raise RuntimeError("team does not exist") class Store: + def __init__(self, retention_days=7): + self.retention_days = retention_days + async def initialize(self): initialized.append(self) @@ -490,6 +516,13 @@ def post(*args, **kwargs): post, ) client = WorkerClient.__new__(WorkerClient) + client.settings = GatewaySettings( + public_url="http://gateway", + consul_url="http://consul:8500", + worker_service="select-ai-a2a-worker", + session_ttl_seconds=60, + database_call_timeout_seconds=45, + ) monkeypatch.setattr( client, "_route_for", @@ -517,6 +550,7 @@ def post(*args, **kwargs): "x-select-ai-a2a-method": "SendMessage", } assert sent["data"] == request.SerializeToString() + assert sent["timeout"] == 65 def test_worker_client_uses_consul_https_endpoint_with_mtls(monkeypatch): @@ -559,14 +593,16 @@ def json(): } -def test_mtls_requires_all_three_gateway_files(): - with pytest.raises(ValueError, match="worker mTLS requires"): +def test_worker_client_certificate_requires_a_key(): + with pytest.raises( + ValueError, match="certificate and key must be supplied" + ): GatewaySettings( public_url="https://gateway.example.com", consul_url="http://consul:8500", worker_service="select-ai-a2a-worker", session_ttl_seconds=60, - worker_tls_ca_file="/tls/ca.pem", + worker_tls_cert_file="/tls/gateway.pem", ) diff --git a/tests/agents/conftest.py b/tests/agents/conftest.py index 19dc331..23c0974 100644 --- a/tests/agents/conftest.py +++ b/tests/agents/conftest.py @@ -12,6 +12,7 @@ @pytest.fixture(scope="module") def provider(oci_compartment_id): return select_ai.OCIGenAIProvider( + embedding_model="cohere.embed-v4.0", region="us-chicago-1", oci_apiformat="GENERIC", oci_compartment_id=oci_compartment_id, diff --git a/tests/conftest.py b/tests/conftest.py index de6f024..c9a213d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -39,6 +39,27 @@ GRANT_PRIVILEGES_TO_USER, ) + +@pytest.hookimpl(hookwrapper=True) +def pytest_runtest_makereport(item, call): + outcome = yield + report = outcome.get_result() + excinfo = call.excinfo + + if ( + report.failed + and excinfo is not None + and isinstance(excinfo.value, oracledb.exceptions.DatabaseError) + and "ORA-29276: transfer timeout" in str(excinfo.value) + ): + report.outcome = "skipped" + report.longrepr = ( + report.location[0], + report.location[1], + "Skipped due to Oracle transfer timeout", + ) + + PYSAI_TEST_USER = "PYSAI_TEST_USER" PYSAI_OCI_CREDENTIAL_NAME = f"PYSAI_OCI_CREDENTIAL_{uuid.uuid4().hex.upper()}" _BASIC_SCHEMA_PRIVILEGES = ( diff --git a/tests/feedback/test_4000_sync_profile.py b/tests/feedback/test_4000_sync_profile.py index 3be9cad..ec93e9f 100644 --- a/tests/feedback/test_4000_sync_profile.py +++ b/tests/feedback/test_4000_sync_profile.py @@ -10,6 +10,7 @@ """ import logging import uuid + import oracledb import pytest import select_ai @@ -85,6 +86,7 @@ def profile(oci_credential, oci_compartment_id, test_env, cursor): credential_name=oci_credential["credential_name"], object_list=object_list, provider=select_ai.OCIGenAIProvider( + embedding_model="cohere.embed-v4.0", oci_compartment_id=oci_compartment_id, oci_apiformat="GENERIC", ), @@ -95,6 +97,7 @@ def profile(oci_credential, oci_compartment_id, test_env, cursor): yield profile profile.delete(force=True) + ############################################### NEGATIVE FEEDBACK TESTS def test_4001(profile, cursor): """Add negative feedback using SHOWSQL prompt_spec, response, and feedback_content.""" @@ -123,6 +126,7 @@ def test_4001(profile, cursor): logger.info("show_prompt response: %s", show_prompt) assert response in show_prompt + def test_4002(profile, cursor): """Add negative feedback using RUNSQL prompt_spec, response, and feedback_content.""" prompt = PROMPT @@ -150,6 +154,7 @@ def test_4002(profile, cursor): logger.info("show_prompt response: %s", show_prompt) assert response in show_prompt + def test_4003(profile, cursor): """Add negative feedback using EXPLAINSQL prompt_spec, response, and feedback_content.""" prompt = PROMPT @@ -177,6 +182,7 @@ def test_4003(profile, cursor): logger.info("show_prompt response: %s", show_prompt) assert response in show_prompt + def test_4004(profile, cursor): """Add negative feedback using SHOWSQL sql_id, response, and feedback_content.""" sql_id = SHOWSQL_SQL_ID @@ -202,6 +208,7 @@ def test_4004(profile, cursor): logger.info("show_prompt response: %s", show_prompt) assert response in show_prompt + def test_4005(profile, cursor): """Add negative feedback using RUNSQL sql_id, response, and feedback_content.""" sql_id = RUNSQL_SQL_ID @@ -227,6 +234,7 @@ def test_4005(profile, cursor): logger.info("show_prompt response: %s", show_prompt) assert response in show_prompt + def test_4006(profile, cursor): """Add negative feedback using EXPLAINSQL sql_id, response, and feedback_content.""" sql_id = EXPLAINSQL_SQL_ID @@ -252,6 +260,7 @@ def test_4006(profile, cursor): logger.info("show_prompt response: %s", show_prompt) assert response in show_prompt + def test_4007(profile, cursor): """Attempt negative feedback with both prompt_spec and sql_id.""" prompt = PROMPT @@ -280,6 +289,7 @@ def test_4007(profile, cursor): _assert_db_error(exc_info, 6550) logger.error("%s", str(exc_info.value).splitlines()[0]) + def test_4008(profile, cursor): """Attempt negative feedback without a response.""" prompt = PROMPT @@ -300,6 +310,7 @@ def test_4008(profile, cursor): assert isinstance(exc_info.value, AttributeError) logger.error("%s", str(exc_info.value).splitlines()[0]) + def test_4009(profile, cursor): """Add negative feedback with sql_id and response but without feedback_content.""" prompt = PROMPT @@ -324,6 +335,7 @@ def test_4009(profile, cursor): logger.info("show_prompt response: %s", show_prompt) assert response in show_prompt + def test_4010(profile, cursor): """Add negative feedback with prompt_spec=None and a valid sql_id.""" sql_id = SHOWSQL_SQL_ID @@ -350,6 +362,7 @@ def test_4010(profile, cursor): logger.info("show_prompt response: %s", show_prompt) assert response in show_prompt + def test_4011(profile, cursor): """Add negative feedback with sql_id=None and a valid prompt_spec.""" prompt = PROMPT @@ -378,6 +391,7 @@ def test_4011(profile, cursor): logger.info("show_prompt response: %s", show_prompt) assert response in show_prompt + def test_4012(profile, cursor): """Attempt negative feedback with response=None.""" prompt = PROMPT @@ -399,6 +413,7 @@ def test_4012(profile, cursor): assert isinstance(exc_info.value, AttributeError) logger.error("%s", str(exc_info.value).splitlines()[0]) + def test_4013(profile, cursor): """Add negative feedback with feedback_content=None using sql_id.""" prompt = PROMPT @@ -452,6 +467,7 @@ def test_4014(profile, cursor): logger.info("show_prompt response: %s", show_prompt) assert response in show_prompt + ############################################################## POSITIVE FEEDBACK TESTS def test_4015(profile, cursor): """Add positive feedback using SHOWSQL prompt_spec only.""" @@ -470,6 +486,7 @@ def test_4015(profile, cursor): assert "sql_query" in show_prompt assert "user_prompt" in show_prompt + def test_4016(profile, cursor): """Add positive feedback using RUNSQL prompt_spec only.""" prompt = PROMPT @@ -487,6 +504,7 @@ def test_4016(profile, cursor): assert "sql_query" in show_prompt assert "user_prompt" in show_prompt + def test_4017(profile, cursor): """Add positive feedback using EXPLAINSQL prompt_spec only.""" prompt = PROMPT @@ -504,6 +522,7 @@ def test_4017(profile, cursor): assert "sql_query" in show_prompt assert "user_prompt" in show_prompt + def test_4018(profile, cursor): """Attempt positive feedback with both prompt_spec and sql_id.""" prompt = PROMPT @@ -523,10 +542,13 @@ def test_4018(profile, cursor): _assert_db_error(exc_info, 6550) logger.error("%s", str(exc_info.value).splitlines()[0]) + def test_4019(profile, cursor): """Add positive feedback using SHOWSQL sql_id only.""" sql_id = SHOWSQL_SQL_ID - logger.info("Adding positive feedback without prompt_spec using sql_id=%s", sql_id) + logger.info( + "Adding positive feedback without prompt_spec using sql_id=%s", sql_id + ) profile.add_positive_feedback(sql_id=sql_id) _log_feedback_vecindex_rows(profile) logger.info("Checking if show_prompt contains feedback metadata") @@ -539,7 +561,9 @@ def test_4019(profile, cursor): def test_4020(profile, cursor): """Add positive feedback using RUNSQL sql_id only.""" sql_id = RUNSQL_SQL_ID - logger.info("Adding positive feedback without prompt_spec using sql_id=%s", sql_id) + logger.info( + "Adding positive feedback without prompt_spec using sql_id=%s", sql_id + ) profile.add_positive_feedback(sql_id=sql_id) _log_feedback_vecindex_rows(profile) logger.info("Checking if show_prompt contains feedback metadata") @@ -548,10 +572,13 @@ def test_4020(profile, cursor): assert "sql_query" in show_prompt assert "user_prompt" in show_prompt + def test_4021(profile, cursor): """Add positive feedback using EXPLAINSQL sql_id only.""" sql_id = EXPLAINSQL_SQL_ID - logger.info("Adding positive feedback without prompt_spec using sql_id=%s", sql_id) + logger.info( + "Adding positive feedback without prompt_spec using sql_id=%s", sql_id + ) profile.add_positive_feedback(sql_id=sql_id) _log_feedback_vecindex_rows(profile) logger.info("Checking if show_prompt contains feedback metadata") @@ -560,10 +587,13 @@ def test_4021(profile, cursor): assert "sql_query" in show_prompt assert "user_prompt" in show_prompt + def test_4022(profile, cursor): """Add positive feedback with prompt_spec=None and a valid sql_id.""" sql_id = SHOWSQL_SQL_ID - logger.info("Adding positive feedback with prompt_spec=None sql_id=%s", sql_id) + logger.info( + "Adding positive feedback with prompt_spec=None sql_id=%s", sql_id + ) profile.add_positive_feedback( prompt_spec=None, sql_id=sql_id, @@ -575,6 +605,7 @@ def test_4022(profile, cursor): assert "sql_query" in show_prompt assert "user_prompt" in show_prompt + def test_4023(profile, cursor): """Add positive feedback with sql_id=None and a valid prompt_spec.""" prompt = PROMPT @@ -610,6 +641,7 @@ def test_4024(profile, cursor): _assert_db_error(exc_info, 20000) logger.error("%s", str(exc_info.value).splitlines()[0]) + ############################################################## DELETE FEEDBACK TESTS def test_4025(profile, cursor): """Delete feedback by prompt_spec after adding positive SHOWSQL feedback.""" @@ -635,6 +667,7 @@ def test_4025(profile, cursor): logger.info("show_prompt response: %s", show_prompt) assert show_prompt.count(PROMPT) == 1 + def test_4026(profile, cursor): """Delete feedback by RUNSQL sql_id after adding negative feedback with sql_id.""" sql_id = RUNSQL_SQL_ID @@ -662,6 +695,7 @@ def test_4026(profile, cursor): logger.info("show_prompt response: %s", show_prompt) assert show_prompt.count(PROMPT) == 1 + def test_4027(profile, cursor): """Delete feedback by prompt_spec after adding positive EXPLAINSQL feedback.""" prompt = PROMPT @@ -685,6 +719,7 @@ def test_4027(profile, cursor): logger.info("show_prompt response: %s", show_prompt) assert show_prompt.count(PROMPT) == 1 + def test_4028(profile, cursor): """Delete SHOWSQL feedback by sql_id after adding negative prompt-based feedback.""" prompt = PROMPT @@ -746,6 +781,7 @@ def test_4029(profile, cursor): logger.info("show_prompt response: %s", show_prompt) assert show_prompt.count(PROMPT) == 1 + def test_4030(profile, cursor): """Delete EXPLAINSQL feedback by sql_id after adding negative prompt-based feedback.""" prompt = PROMPT @@ -820,7 +856,9 @@ def test_4032(profile, cursor): sql_id=sql_id, ) _log_feedback_vecindex_rows(profile) - logger.info("Deleting feedback with prompt_spec=None using sql_id=%s", sql_id) + logger.info( + "Deleting feedback with prompt_spec=None using sql_id=%s", sql_id + ) profile.delete_feedback( prompt_spec=None, sql_id=sql_id, @@ -831,6 +869,7 @@ def test_4032(profile, cursor): logger.info("show_prompt response: %s", show_prompt) assert show_prompt.count(PROMPT) == 1 + def test_4033(profile, cursor): """Attempt delete_feedback with both prompt_spec and sql_id.""" prompt = PROMPT diff --git a/tests/feedback/test_4100_async_profile.py b/tests/feedback/test_4100_async_profile.py index 5b3bae7..20c9378 100644 --- a/tests/feedback/test_4100_async_profile.py +++ b/tests/feedback/test_4100_async_profile.py @@ -10,6 +10,7 @@ """ import logging import uuid + import oracledb import pytest import select_ai @@ -79,6 +80,7 @@ async def profile(oci_credential, oci_compartment_id, test_env, async_cursor): credential_name=oci_credential["credential_name"], object_list=object_list, provider=select_ai.OCIGenAIProvider( + embedding_model="cohere.embed-v4.0", oci_compartment_id=oci_compartment_id, oci_apiformat="GENERIC", ), @@ -97,6 +99,7 @@ async def active_async_cursor(async_cursor): yield _ACTIVE_ASYNC_CURSOR = None + ############################################### NEGATIVE FEEDBACK TESTS async def test_4101(profile, async_cursor): """Add negative feedback using SHOWSQL prompt_spec, response, and feedback_content.""" @@ -433,6 +436,7 @@ async def test_4113(profile, async_cursor): logger.info("show_prompt response: %s", show_prompt) assert response in show_prompt + async def test_4114(profile, async_cursor): """Add negative feedback for a non-existent SHOWSQL prompt.""" prompt = "Adding negative feedback with non existent prompt" @@ -539,7 +543,9 @@ async def test_4118(profile, async_cursor): async def test_4119(profile, async_cursor): """Add positive feedback using SHOWSQL sql_id only.""" sql_id = SHOWSQL_SQL_ID - logger.info("Adding positive feedback without prompt_spec using sql_id=%s", sql_id) + logger.info( + "Adding positive feedback without prompt_spec using sql_id=%s", sql_id + ) await profile.add_positive_feedback(sql_id=sql_id) await _log_feedback_vecindex_rows(profile) logger.info("Checking if show_prompt contains feedback metadata") @@ -552,7 +558,9 @@ async def test_4119(profile, async_cursor): async def test_4120(profile, async_cursor): """Add positive feedback using RUNSQL sql_id only.""" sql_id = RUNSQL_SQL_ID - logger.info("Adding positive feedback without prompt_spec using sql_id=%s", sql_id) + logger.info( + "Adding positive feedback without prompt_spec using sql_id=%s", sql_id + ) await profile.add_positive_feedback(sql_id=sql_id) await _log_feedback_vecindex_rows(profile) logger.info("Checking if show_prompt contains feedback metadata") @@ -565,7 +573,9 @@ async def test_4120(profile, async_cursor): async def test_4121(profile, async_cursor): """Add positive feedback using EXPLAINSQL sql_id only.""" sql_id = EXPLAINSQL_SQL_ID - logger.info("Adding positive feedback without prompt_spec using sql_id=%s", sql_id) + logger.info( + "Adding positive feedback without prompt_spec using sql_id=%s", sql_id + ) await profile.add_positive_feedback(sql_id=sql_id) await _log_feedback_vecindex_rows(profile) logger.info("Checking if show_prompt contains feedback metadata") @@ -578,7 +588,9 @@ async def test_4121(profile, async_cursor): async def test_4122(profile, async_cursor): """Add positive feedback with prompt_spec=None and a valid sql_id.""" sql_id = SHOWSQL_SQL_ID - logger.info("Adding positive feedback with prompt_spec=None sql_id=%s", sql_id) + logger.info( + "Adding positive feedback with prompt_spec=None sql_id=%s", sql_id + ) await profile.add_positive_feedback( prompt_spec=None, sql_id=sql_id, @@ -611,6 +623,7 @@ async def test_4123(profile, async_cursor): assert "sql_query" in show_prompt assert "user_prompt" in show_prompt + async def test_4124(profile, async_cursor): """Attempt positive feedback for a non-existent SHOWSQL prompt.""" prompt = "Adding positive feedback with non existent prompt" @@ -625,6 +638,7 @@ async def test_4124(profile, async_cursor): _assert_db_error(exc_info, 20000) logger.error("%s", str(exc_info.value).splitlines()[0]) + ############################################################## DELETE FEEDBACK TESTS async def test_4125(profile, async_cursor): """Delete feedback by prompt_spec after adding positive SHOWSQL feedback.""" @@ -836,7 +850,9 @@ async def test_4132(profile, async_cursor): sql_id=sql_id, ) await _log_feedback_vecindex_rows(profile) - logger.info("Deleting feedback with prompt_spec=None using sql_id=%s", sql_id) + logger.info( + "Deleting feedback with prompt_spec=None using sql_id=%s", sql_id + ) await profile.delete_feedback( prompt_spec=None, sql_id=sql_id, diff --git a/tests/profiles/conftest.py b/tests/profiles/conftest.py index 310bd38..1673b1e 100644 --- a/tests/profiles/conftest.py +++ b/tests/profiles/conftest.py @@ -59,6 +59,7 @@ def log_test_case(request, configure_module_logging): @pytest.fixture(scope="module") def provider(oci_compartment_id): return select_ai.OCIGenAIProvider( + embedding_model="cohere.embed-v4.0", region="us-phoenix-1", oci_apiformat="GENERIC", oci_compartment_id=oci_compartment_id, diff --git a/tests/profiles/test_1200_profile.py b/tests/profiles/test_1200_profile.py index d07424c..24dc0a7 100644 --- a/tests/profiles/test_1200_profile.py +++ b/tests/profiles/test_1200_profile.py @@ -233,12 +233,13 @@ def test_1207_profile_attributes_set_provider_attribute_without_provider(): assert attributes.provider.model == "meta.llama-3.1-70b-instruct" -def test_1208(oci_credential, oci_compartment_id): +def test_1208(oci_credential, oci_compartment_id, profile_attributes): """Set multiple attributes for a Profile""" profile = Profile(PYSAI_1200_PROFILE) profile_attrs = ProfileAttributes( credential_name=oci_credential["credential_name"], provider=select_ai.OCIGenAIProvider( + embedding_model=profile_attributes.provider.embedding_model, model="meta.llama-4-maverick-17b-128e-instruct-fp8", region="us-chicago-1", oci_compartment_id=oci_compartment_id, diff --git a/tests/profiles/test_1300_profile_async.py b/tests/profiles/test_1300_profile_async.py index 0de11ee..2e30549 100644 --- a/tests/profiles/test_1300_profile_async.py +++ b/tests/profiles/test_1300_profile_async.py @@ -277,7 +277,7 @@ async def test_1307(): assert profile.attributes.provider.model == "meta.llama-3.1-70b-instruct" -async def test_1308(oci_credential, oci_compartment_id): +async def test_1308(oci_credential, oci_compartment_id, profile_attributes): """Set multiple attributes for a Profile""" logger.info( "Setting multiple attributes for async profile %s", @@ -287,6 +287,7 @@ async def test_1308(oci_credential, oci_compartment_id): profile_attrs = ProfileAttributes( credential_name=oci_credential["credential_name"], provider=select_ai.OCIGenAIProvider( + embedding_model=profile_attributes.provider.embedding_model, model="meta.llama-4-maverick-17b-128e-instruct-fp8", region="us-chicago-1", oci_apiformat="GENERIC", diff --git a/tests/profiles/test_1400_conversation.py b/tests/profiles/test_1400_conversation.py index c2fe854..0104b1b 100644 --- a/tests/profiles/test_1400_conversation.py +++ b/tests/profiles/test_1400_conversation.py @@ -184,9 +184,9 @@ def test_1411_create_with_long_values(): logger.debug("Error code: %s", error.code) logger.debug("Error message:\n%s", error.message) assert error.code == 20050 - assert ( - "Value is too long for conversation attribute - title" in error.message - ) + message = error.message.lower() + assert "title" in message + assert "too long" in message or "exceeds" in message def test_1412_set_attributes_with_invalid_id(): diff --git a/tests/profiles/test_1500_conversation_async.py b/tests/profiles/test_1500_conversation_async.py index 09f3a53..63b7ab0 100644 --- a/tests/profiles/test_1500_conversation_async.py +++ b/tests/profiles/test_1500_conversation_async.py @@ -209,9 +209,9 @@ async def test_1511_create_with_long_values(): logger.debug("Error code: %s", error.code) logger.debug("Error message:\n%s", error.message) assert error.code == 20050 - assert ( - "Value is too long for conversation attribute - title" in error.message - ) + message = error.message.lower() + assert "title" in message + assert "too long" in message or "exceeds" in message @pytest.mark.anyio diff --git a/tests/vector_index/test_5000_async_create_index.py b/tests/vector_index/test_5000_async_create_index.py index 3249d9c..ae336e7 100644 --- a/tests/vector_index/test_5000_async_create_index.py +++ b/tests/vector_index/test_5000_async_create_index.py @@ -145,7 +145,7 @@ async def create_profile(cls, profile_name=None): provider = select_ai.OCIGenAIProvider( oci_compartment_id=params["oci_compartment_id"], oci_apiformat="GENERIC", - embedding_model="cohere.embed-english-v3.0", + embedding_model="cohere.embed-v4.0", ) profile_attributes = select_ai.ProfileAttributes( credential_name=cls.genai_cred, @@ -445,7 +445,9 @@ async def test_5017(self): with pytest.raises(oracledb.DatabaseError) as exc_info: await vector_index.create(replace=True) assert "ORA-20045" in str(exc_info.value) - assert "description is too long" in str(exc_info.value) + message = str(exc_info.value).lower() + assert "description" in message + assert "too long" in message or "exceeds" in message logger.info("Expected DatabaseError raised: %s", exc_info.value) async def test_5018(self): diff --git a/tests/vector_index/test_5000_create_index.py b/tests/vector_index/test_5000_create_index.py index 40a960b..a275ec6 100644 --- a/tests/vector_index/test_5000_create_index.py +++ b/tests/vector_index/test_5000_create_index.py @@ -132,7 +132,7 @@ def create_profile(cls, profile_name=None): provider = select_ai.OCIGenAIProvider( oci_compartment_id=params["oci_compartment_id"], oci_apiformat="GENERIC", - embedding_model="cohere.embed-english-v3.0", + embedding_model="cohere.embed-v4.0", ) profile_attributes = select_ai.ProfileAttributes( credential_name=cls.genai_cred, @@ -468,7 +468,9 @@ def test_5017(self): with pytest.raises(oracledb.DatabaseError) as exc_info: vector_index.create(replace=True) assert "ORA-20045" in str(exc_info.value) - assert "description is too long" in str(exc_info.value) + message = str(exc_info.value).lower() + assert "description" in message + assert "too long" in message or "exceeds" in message logger.info( "Expected DatabaseError raised: %s", exc_info.value, diff --git a/tests/vector_index/test_5100_async_drop_index.py b/tests/vector_index/test_5100_async_drop_index.py index 313dad7..d022e97 100644 --- a/tests/vector_index/test_5100_async_drop_index.py +++ b/tests/vector_index/test_5100_async_drop_index.py @@ -156,7 +156,7 @@ async def create_profile(cls, profile_name=None): provider = select_ai.OCIGenAIProvider( oci_compartment_id=params["oci_compartment_id"], oci_apiformat="GENERIC", - embedding_model="cohere.embed-english-v3.0", + embedding_model="cohere.embed-v4.0", ) profile_attributes = select_ai.ProfileAttributes( credential_name=cls.genai_cred, diff --git a/tests/vector_index/test_5100_drop_index.py b/tests/vector_index/test_5100_drop_index.py index 276c10d..b5a14f5 100644 --- a/tests/vector_index/test_5100_drop_index.py +++ b/tests/vector_index/test_5100_drop_index.py @@ -142,7 +142,7 @@ def create_profile(cls, profile_name=None): provider = select_ai.OCIGenAIProvider( oci_compartment_id=params["oci_compartment_id"], oci_apiformat="GENERIC", - embedding_model="cohere.embed-english-v3.0", + embedding_model="cohere.embed-v4.0", ) profile_attributes = select_ai.ProfileAttributes( credential_name=cls.genai_cred, provider=provider diff --git a/tests/vector_index/test_5200_async_setindex_attributes.py b/tests/vector_index/test_5200_async_setindex_attributes.py index abcd093..e88571c 100644 --- a/tests/vector_index/test_5200_async_setindex_attributes.py +++ b/tests/vector_index/test_5200_async_setindex_attributes.py @@ -191,7 +191,7 @@ async def create_profile(cls, profile_name=None): provider=select_ai.OCIGenAIProvider( oci_compartment_id=p["oci_compartment_id"], oci_apiformat="GENERIC", - embedding_model="cohere.embed-english-v3.0", + embedding_model="cohere.embed-v4.0", ), ), description="OCI GENAI Profile", @@ -1025,7 +1025,7 @@ async def test_5236(self): await self.async_vector_index.set_attribute( "profile_name", long_name ) - assert "ORA-20048" in str(exc_info.value) + assert exc_info.value.args[0].code in (20008, 20048) logger.info("Large attribute value correctly raised DatabaseError.") async def test_5237(self): diff --git a/tests/vector_index/test_5200_setindex_attributes.py b/tests/vector_index/test_5200_setindex_attributes.py index c9d5028..cc22812 100644 --- a/tests/vector_index/test_5200_setindex_attributes.py +++ b/tests/vector_index/test_5200_setindex_attributes.py @@ -184,7 +184,7 @@ def create_profile(cls, profile_name=None): provider=select_ai.OCIGenAIProvider( oci_compartment_id=p["oci_compartment_id"], oci_apiformat="GENERIC", - embedding_model="cohere.embed-english-v3.0", + embedding_model="cohere.embed-v4.0", ), ), description="OCI GENAI Profile", @@ -990,7 +990,7 @@ def test_5236(self): long_name = "X" * 500 with pytest.raises(oracledb.DatabaseError) as exc_info: self.vector_index.set_attribute("profile_name", long_name) - assert "ORA-20048" in str(exc_info.value) + assert exc_info.value.args[0].code in (20008, 20048) logger.info("Large attribute value correctly raised DatabaseError.") def test_5237(self): diff --git a/tests/vector_index/test_5300_async_getindex_attributes.py b/tests/vector_index/test_5300_async_getindex_attributes.py index 7745140..151d03e 100644 --- a/tests/vector_index/test_5300_async_getindex_attributes.py +++ b/tests/vector_index/test_5300_async_getindex_attributes.py @@ -171,7 +171,7 @@ async def create_profile(cls, profile_name=None): provider=select_ai.OCIGenAIProvider( oci_compartment_id=p["oci_compartment_id"], oci_apiformat="GENERIC", - embedding_model="cohere.embed-english-v3.0", + embedding_model="cohere.embed-v4.0", ), ), description="OCI GENAI Profile", diff --git a/tests/vector_index/test_5300_getindex_attributes.py b/tests/vector_index/test_5300_getindex_attributes.py index a966c34..8e902d3 100644 --- a/tests/vector_index/test_5300_getindex_attributes.py +++ b/tests/vector_index/test_5300_getindex_attributes.py @@ -171,7 +171,7 @@ def create_profile(cls, profile_name=None): provider = select_ai.OCIGenAIProvider( oci_compartment_id=p["oci_compartment_id"], oci_apiformat="GENERIC", - embedding_model="cohere.embed-english-v3.0", + embedding_model="cohere.embed-v4.0", ) profile_attributes = select_ai.ProfileAttributes( credential_name=cls.genai_cred, provider=provider diff --git a/tests/vector_index/test_5400_async_list_index.py b/tests/vector_index/test_5400_async_list_index.py index 9782a4d..545be6f 100644 --- a/tests/vector_index/test_5400_async_list_index.py +++ b/tests/vector_index/test_5400_async_list_index.py @@ -158,7 +158,7 @@ async def create_profile(cls, profile_name=None): provider=select_ai.OCIGenAIProvider( oci_compartment_id=p["oci_compartment_id"], oci_apiformat="GENERIC", - embedding_model="cohere.embed-english-v3.0", + embedding_model="cohere.embed-v4.0", ), ), description="OCI GENAI Profile", diff --git a/tests/vector_index/test_5400_list_index.py b/tests/vector_index/test_5400_list_index.py index 0cd48fa..bbfd3f3 100644 --- a/tests/vector_index/test_5400_list_index.py +++ b/tests/vector_index/test_5400_list_index.py @@ -178,7 +178,7 @@ def create_profile(cls, profile_name=None): provider = select_ai.OCIGenAIProvider( oci_compartment_id=p["oci_compartment_id"], oci_apiformat="GENERIC", - embedding_model="cohere.embed-english-v3.0", + embedding_model="cohere.embed-v4.0", ) profile_attributes = select_ai.ProfileAttributes( credential_name=cls.genai_cred, provider=provider diff --git a/tests/vector_index/test_5500_async_enable_disable_index.py b/tests/vector_index/test_5500_async_enable_disable_index.py index 03b1fa2..6d30137 100644 --- a/tests/vector_index/test_5500_async_enable_disable_index.py +++ b/tests/vector_index/test_5500_async_enable_disable_index.py @@ -207,7 +207,7 @@ async def create_profile(cls, profile_name=None): provider = select_ai.OCIGenAIProvider( oci_compartment_id=p["oci_compartment_id"], oci_apiformat="GENERIC", - embedding_model="cohere.embed-english-v3.0", + embedding_model="cohere.embed-v4.0", ) profile_attributes = select_ai.ProfileAttributes( credential_name=cls.genai_cred, diff --git a/tests/vector_index/test_5500_enable_disable_index.py b/tests/vector_index/test_5500_enable_disable_index.py index fdc6bd1..0795c1e 100644 --- a/tests/vector_index/test_5500_enable_disable_index.py +++ b/tests/vector_index/test_5500_enable_disable_index.py @@ -160,7 +160,7 @@ def create_profile(cls, profile_name=None): provider = select_ai.OCIGenAIProvider( oci_compartment_id=p["oci_compartment_id"], oci_apiformat="GENERIC", - embedding_model="cohere.embed-english-v3.0", + embedding_model="cohere.embed-v4.0", ) profile_attributes = select_ai.ProfileAttributes( credential_name=cls.genai_cred, provider=provider