2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[ORDS] REST APIでSelect AIを実行する

2
Last updated at Posted at 2026-05-01

はじめに

この記事では認証認可に関する情報は記載しておりません。
Okta の Auth0 との連携について後日記事を投稿する予定ですが、REST API を外部に公開する際はネットワーク制限やセキュリティを強固にするための設定などを考慮した上でご検討ください。

前回の記事では、Oracle Autonomous AI Database に標準で用意されている Oracle REST Data Services (ORDS) を使って、Database 上のテーブルを REST API として公開する方法を紹介しました。

ORDS の基本的な考え方や、Module、Template、Handler の作り方については前回の記事を参考にしてください。

今回はその続編として、ORDS の REST API 経由で Select AI を実行する方法を紹介します。

Select AI を使うと、自然言語のプロンプトから Database 上のデータに対する問い合わせや要約を実行できます。
ただし、Select AI は LLM を呼び出すため、通常の SELECT API と比べて処理に時間がかかる可能性があります。

そのため、この記事では REST API のリクエスト中に Select AI の完了を待つのではなく、以下のように非同期ジョブとして実行します。

  1. POST で Select AI の実行ジョブを登録する
  2. ORDS から DBMS_SCHEDULER のジョブを起動する
  3. Scheduler Job の中で DBMS_CLOUD_AI.GENERATE を実行する
  4. ジョブ管理テーブルにステータスと結果を保存する
  5. クライアントは GET でジョブの状態を定期的に確認する
  6. ステータスが SUCCEEDED になったら結果を取得する

今回作るもの

今回は、以下の REST API を作成します。

Method Path 内容
POST /ords/<schema>/ai/v1/jobs/ Select AI の非同期ジョブを登録する
GET /ords/<schema>/ai/v1/jobs/:job_id ジョブの状態と実行結果を取得する

POST /jobs/ には、以下のような JSON を送信します。

{
  "prompt": "問い合わせる内容 (例 : 2026年4月の売上傾向を要約してください)",
  "action": "narrate",
  "profile_name": "<事前に作成したProfile名>",
  "conversation_id": "demo-session-001"
}

レスポンスは以下のようになります。

{
  "job_id": "9A1F2C3D4E5F67890123456789ABCDEF",
  "job_type": "SELECTAI",
  "status": "QUEUED",
  "profile_name": "<事前に作成したProfile名>",
  "action": "narrate",
  "status_url": "./jobs/9A1F2C3D4E5F67890123456789ABCDEF"
}

この時点では Select AI の結果はまだ返しません。
job_id を使って、後続の GET /jobs/:job_id で進捗と結果を確認します。

なぜ非同期にするのか

通常の SQL を返すだけの REST API であれば、Handler の中で SELECT を実行し、その結果をそのまま返せば十分です。

しかし Select AI は、内部で LLM に問い合わせます。
プロンプトの内容、参照するデータ量、利用する AI Provider、ネットワーク状況によって、応答時間が変わります。

そのため、フロントエンドや外部システムから呼び出す REST API としては、以下のような構成にしておくと扱いやすくなります。

  • API 呼び出しはすぐに 202 Accepted を返す
  • 実際の Select AI 実行はバックグラウンドで行う
  • 実行状態をテーブルで管理する
  • クライアントは一定間隔でステータスを確認する
  • 成功時は result、失敗時は error_message を確認する

今回の実装では、ジョブの状態を以下のように管理します。

status 意味
QUEUED ジョブ登録済み
RUNNING Select AI 実行中
SUCCEEDED 正常終了
FAILED エラー終了
CANCEL_REQUESTED キャンセル要求済み
CANCELLED キャンセル済み
EXPIRED 期限切れ
TIMEOUT タイムアウト

今回のサンプルでは、実際に使う主なステータスは QUEUEDRUNNINGSUCCEEDEDFAILED です。

全体構成

今回の構成は以下のようになります。

ORDS の Handler で直接 DBMS_CLOUD_AI.GENERATE を呼び出すのではなく、ai_rest_worker という PL/SQL パッケージに処理を閉じ込めています。

このようにしておくと、REST API 側の定義はシンプルになり、ジョブ登録、ステータス更新、Select AI 実行、エラー処理を PL/SQL パッケージ側で管理できます。

前提

この記事では、以下が準備済みである前提で進めます。

  • Autonomous AI Database を利用できること
  • ORDS が有効なスキーマで作業すること
  • Select AI profile が作成済みであること
  • DBMS_CLOUD_AI.GENERATE を実行できること
  • Scheduler Job を作成するための CREATE JOB 権限があること

手順

Select AI を実行するための AI Profile を作成する

この記事では省略します。
作成方法については、弊社の下記の公開情報をご参考ください。

ちなみに私は、下記のような感じでクレデンシャル情報と AI Profile を作成しました。

BEGIN
    DBMS_CLOUD.CREATE_CREDENTIAL(
        credential_name => 'OCI_CRED',
        user_ocid       => 'ocid1.user.oc1..xxxxxxxxxxxx',
        tenancy_ocid    => 'ocid1.tenancy.oc1..xxxxxxxxxxxxxxxx',
        private_key     => '-----BEGIN PRIVATE KEY-----
MIIEvxxx
EQzy8=
-----END PRIVATE KEY-----',
        fingerprint     => '5d:xx:xx:.....:d9:3a'
    );
END;
/

BEGIN
    DBMS_CLOUD_AI.CREATE_PROFILE(
        profile_name => 'GENAI_PROFILE',
        attributes   => '{
            "provider"              : "oci",
            "credential_name"       : "OCI_CRED",
            "oci_compartment_id"    : "ocid1.compartment.oc1..xxxxxx",
            "conversation"          : "TRUE",
            "annotations"           : "TRUE",
            "model"                 : "openai.gpt-4o-mini",
            "region"                : "ap-osaka-1",
            "object_list": [
                { "owner": "TEMP_USER", "name": "DEMO_CUSTOMER_MASTER" },
                { "owner": "TEMP_USER", "name": "DEMO_CUSTOMER_VOICES" },
                { "owner": "TEMP_USER", "name": "DEMO_PURCHASE_HISTORY" }
            ]
        }'
    );
END;
/

必要なテーブルや PL/SQL パッケージ、ORDS の定義を作成する

以下の SQL を、Autonomous AI Database の対象のスキーマで実行します。

実行前に最低限変更するのは、冒頭の SELECT_AI_PROFILE です。
ブラウザのフロントエンドから呼び出す場合は、CORS_ALLOWED_ORIGINS も環境に合わせて変更してください。

-- Select AI を ORDS REST API から非同期実行するためのセットアップ SQL です。
-- SQLcl / SQL Developer で対象スキーマに接続して実行してください。

SET DEFINE ON

DEFINE SELECT_AI_PROFILE = '<事前に作成したProfile名>'
DEFINE CORS_ALLOWED_ORIGINS = 'http://localhost:5173,http://127.0.0.1:5173,http://localhost:5174,http://127.0.0.1:5174'

-- 新規スキーマで ORDS が未有効の場合だけ使用してください。
-- 既に ORDS が有効なスキーマで実行すると ORA-20049 になる場合があります。
--
-- BEGIN
--   ORDS.ENABLE_SCHEMA(
--     p_enabled             => TRUE,
--     p_schema              => USER,
--     p_url_mapping_type    => 'BASE_PATH',
--     p_url_mapping_pattern => LOWER(USER),
--     p_auto_rest_auth      => FALSE
--   );
-- END;
-- /

BEGIN
  EXECUTE IMMEDIATE q'~
    CREATE TABLE ai_rest_jobs (
      job_id          VARCHAR2(32) DEFAULT RAWTOHEX(SYS_GUID()) NOT NULL,
      job_type        VARCHAR2(30) DEFAULT 'SELECTAI' NOT NULL,
      status          VARCHAR2(30) DEFAULT 'QUEUED' NOT NULL,
      profile_name    VARCHAR2(128) DEFAULT '&&SELECT_AI_PROFILE' NOT NULL,
      prompt          CLOB NOT NULL,
      action          VARCHAR2(30) DEFAULT 'narrate' NOT NULL,
      conversation_id VARCHAR2(128),
      result          CLOB,
      error_message   CLOB,
      created_at      TIMESTAMP WITH LOCAL TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL,
      started_at      TIMESTAMP WITH LOCAL TIME ZONE,
      completed_at    TIMESTAMP WITH LOCAL TIME ZONE,
      created_by      VARCHAR2(128) DEFAULT SYS_CONTEXT('USERENV', 'SESSION_USER') NOT NULL,
      CONSTRAINT ai_rest_jobs_pk PRIMARY KEY (job_id),
      CONSTRAINT ai_rest_jobs_type_ck CHECK (job_type IN ('SELECTAI')),
      CONSTRAINT ai_rest_jobs_status_ck CHECK (
        status IN ('QUEUED', 'RUNNING', 'SUCCEEDED', 'FAILED', 'CANCEL_REQUESTED', 'CANCELLED', 'EXPIRED', 'TIMEOUT')
      ),
      CONSTRAINT ai_rest_jobs_action_ck CHECK (action IN ('narrate', 'chat', 'showsql', 'runsql'))
    )
  ~';
EXCEPTION
  WHEN OTHERS THEN
    IF SQLCODE != -955 THEN
      RAISE;
    END IF;
END;
/

BEGIN
  EXECUTE IMMEDIATE 'CREATE INDEX ai_rest_jobs_status_ix ON ai_rest_jobs (status, created_at)';
EXCEPTION
  WHEN OTHERS THEN
    IF SQLCODE != -955 THEN
      RAISE;
    END IF;
END;
/

CREATE OR REPLACE PACKAGE ai_rest_worker AS
  PROCEDURE submit_job(
    p_prompt          IN  CLOB,
    p_action          IN  VARCHAR2 DEFAULT 'narrate',
    p_profile_name    IN  VARCHAR2 DEFAULT '&&SELECT_AI_PROFILE',
    p_conversation_id IN  VARCHAR2 DEFAULT NULL,
    p_job_id          OUT VARCHAR2,
    p_status          OUT VARCHAR2
  );

  PROCEDURE run_job(
    p_job_id IN VARCHAR2
  );
END ai_rest_worker;
/

CREATE OR REPLACE PACKAGE BODY ai_rest_worker AS
  FUNCTION normalize_action(p_action IN VARCHAR2) RETURN VARCHAR2 IS
    l_action VARCHAR2(30) := LOWER(TRIM(NVL(p_action, 'narrate')));
  BEGIN
    IF l_action NOT IN ('narrate', 'chat', 'showsql', 'runsql') THEN
      RETURN 'narrate';
    END IF;

    RETURN l_action;
  END normalize_action;

  PROCEDURE submit_job(
    p_prompt          IN  CLOB,
    p_action          IN  VARCHAR2 DEFAULT 'narrate',
    p_profile_name    IN  VARCHAR2 DEFAULT '&&SELECT_AI_PROFILE',
    p_conversation_id IN  VARCHAR2 DEFAULT NULL,
    p_job_id          OUT VARCHAR2,
    p_status          OUT VARCHAR2
  ) IS
    l_job_id       VARCHAR2(32) := RAWTOHEX(SYS_GUID());
    l_profile_name VARCHAR2(128) := TRIM(NVL(p_profile_name, '&&SELECT_AI_PROFILE'));
    l_action       VARCHAR2(30) := normalize_action(p_action);
    l_job_name     VARCHAR2(128) := 'AI_REST_JOB_' || l_job_id;
  BEGIN
    IF p_prompt IS NULL OR DBMS_LOB.GETLENGTH(p_prompt) = 0 THEN
      RAISE_APPLICATION_ERROR(-20001, 'prompt は必須です。');
    END IF;

    INSERT INTO ai_rest_jobs (
      job_id,
      job_type,
      status,
      profile_name,
      prompt,
      action,
      conversation_id,
      created_by
    ) VALUES (
      l_job_id,
      'SELECTAI',
      'QUEUED',
      l_profile_name,
      p_prompt,
      l_action,
      p_conversation_id,
      COALESCE(SYS_CONTEXT('USERENV', 'CLIENT_IDENTIFIER'), SYS_CONTEXT('USERENV', 'SESSION_USER'))
    );

    DBMS_SCHEDULER.CREATE_JOB(
      job_name   => l_job_name,
      job_type   => 'PLSQL_BLOCK',
      job_action => 'BEGIN ai_rest_worker.run_job(''' || l_job_id || '''); END;',
      enabled    => TRUE,
      auto_drop  => TRUE,
      comments   => 'Select AI 非同期実行ジョブ'
    );

    p_job_id := l_job_id;
    p_status := 'QUEUED';

    COMMIT;
  END submit_job;

  PROCEDURE run_job(
    p_job_id IN VARCHAR2
  ) IS
    l_prompt        CLOB;
    l_profile_name  ai_rest_jobs.profile_name%TYPE;
    l_action        ai_rest_jobs.action%TYPE;
    l_result        CLOB;
    l_error_message CLOB;
  BEGIN
    SELECT prompt, profile_name, action
    INTO   l_prompt, l_profile_name, l_action
    FROM   ai_rest_jobs
    WHERE  job_id = p_job_id
    FOR UPDATE;

    UPDATE ai_rest_jobs
    SET    status = 'RUNNING',
           started_at = SYSTIMESTAMP,
           error_message = NULL
    WHERE  job_id = p_job_id;

    COMMIT;

    BEGIN
      l_result := DBMS_CLOUD_AI.GENERATE(
        prompt       => l_prompt,
        profile_name => l_profile_name,
        action       => l_action
      );

      UPDATE ai_rest_jobs
      SET    status = 'SUCCEEDED',
             result = l_result,
             completed_at = SYSTIMESTAMP
      WHERE  job_id = p_job_id;
    EXCEPTION
      WHEN OTHERS THEN
        l_error_message := SQLERRM || CHR(10) || DBMS_UTILITY.FORMAT_ERROR_BACKTRACE;

        UPDATE ai_rest_jobs
        SET    status = 'FAILED',
               error_message = l_error_message,
               completed_at = SYSTIMESTAMP
        WHERE  job_id = p_job_id;
    END;

    COMMIT;
  EXCEPTION
    WHEN NO_DATA_FOUND THEN
      NULL;
  END run_job;
END ai_rest_worker;
/

BEGIN
  ORDS.DEFINE_MODULE(
    p_module_name    => 'select_ai_simple.ai.v1',
    p_base_path      => '/ai/v1/',
    p_items_per_page => 25,
    p_status         => 'PUBLISHED',
    p_comments       => 'Select AI asynchronous job API'
  );

  ORDS.SET_MODULE_ORIGINS_ALLOWED(
    p_module_name     => 'select_ai_simple.ai.v1',
    p_origins_allowed => '&&CORS_ALLOWED_ORIGINS'
  );

  ORDS.DEFINE_TEMPLATE(
    p_module_name => 'select_ai_simple.ai.v1',
    p_pattern     => 'jobs/',
    p_comments    => 'Select AI job collection'
  );

  ORDS.DEFINE_TEMPLATE(
    p_module_name => 'select_ai_simple.ai.v1',
    p_pattern     => 'jobs/:job_id',
    p_comments    => 'Select AI job status'
  );
END;
/

BEGIN
  ORDS.DEFINE_HANDLER(
    p_module_name   => 'select_ai_simple.ai.v1',
    p_pattern       => 'jobs/',
    p_method        => 'POST',
    p_source_type   => ORDS.source_type_plsql,
    p_mimes_allowed => 'application/json',
    p_comments      => 'Submit Select AI job',
    p_source        => q'~
      DECLARE
        l_job_id       VARCHAR2(32);
        l_status       VARCHAR2(30);
        l_profile_name VARCHAR2(128) := NVL(:profile_name, '&&SELECT_AI_PROFILE');
        l_action       VARCHAR2(30) := NVL(:action, 'narrate');
      BEGIN
        ai_rest_worker.submit_job(
          p_prompt          => TO_CLOB(:prompt),
          p_action          => l_action,
          p_profile_name    => l_profile_name,
          p_conversation_id => :conversation_id,
          p_job_id          => l_job_id,
          p_status          => l_status
        );

        :status_code := 202;

        OWA_UTIL.MIME_HEADER('application/json', FALSE);
        HTP.P('Cache-Control: no-store');
        OWA_UTIL.HTTP_HEADER_CLOSE;

        APEX_JSON.OPEN_OBJECT;
        APEX_JSON.WRITE('job_id', l_job_id);
        APEX_JSON.WRITE('job_type', 'SELECTAI');
        APEX_JSON.WRITE('status', l_status);
        APEX_JSON.WRITE('profile_name', l_profile_name);
        APEX_JSON.WRITE('action', l_action);
        APEX_JSON.WRITE('status_url', './jobs/' || l_job_id);
        APEX_JSON.CLOSE_OBJECT;
      EXCEPTION
        WHEN OTHERS THEN
          :status_code := 400;

          OWA_UTIL.MIME_HEADER('application/json', FALSE);
          HTP.P('Cache-Control: no-store');
          OWA_UTIL.HTTP_HEADER_CLOSE;

          APEX_JSON.OPEN_OBJECT;
          APEX_JSON.WRITE('error', SQLERRM);
          APEX_JSON.CLOSE_OBJECT;
      END;
    ~'
  );

  ORDS.DEFINE_HANDLER(
    p_module_name    => 'select_ai_simple.ai.v1',
    p_pattern        => 'jobs/:job_id',
    p_method         => 'GET',
    p_source_type    => ORDS.source_type_collection_item,
    p_items_per_page => 1,
    p_comments       => 'Get Select AI job status and result',
    p_source         => q'~
      SELECT job_id,
             job_type,
             status,
             profile_name,
             DBMS_LOB.SUBSTR(prompt, 32767, 1) AS prompt,
             action,
             conversation_id,
             created_at,
             started_at,
             completed_at,
             result,
             error_message
      FROM   ai_rest_jobs
      WHERE  job_id = :job_id
    ~'
  );
END;
/

COMMIT;

この SQL を実行すると、以下がまとめて作成されます。

作成されるもの 内容
AI_REST_JOBS Select AI のジョブ状態、プロンプト、結果、エラーを保存するテーブル
AI_REST_JOBS_STATUS_IX ステータス確認用のインデックス
AI_REST_WORKER ジョブ登録と Select AI 実行を行う PL/SQL パッケージ
select_ai_simple.ai.v1 ORDS の REST API Module
jobs/ POST でジョブを登録する Template と Handler
jobs/:job_id GET でジョブ状態と結果を取得する Template と Handler

この SQL の中で具体的に何をしているのかは、Appendix としてこの記事の最後に記載しておきます。

実際に試す

1. curl でジョブを登録する

まず POST /jobs/ を呼び出して Select AI ジョブを登録します。

curl -i -X POST \
  "https://<adb-host>/ords/<schema>/ai/v1/jobs/" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "<適当なプロンプトを入力>",
    "action": "narrate",
    "profile_name": "<事前に作成したProfile名>",
    "conversation_id": "demo-session-001"
  }'

正常に登録できると、HTTP ステータス 202 Acceptedjob_id が返ります。

{
  "job_id": "9A1F2C3D4E5F67890123456789ABCDEF",
  "job_type": "SELECTAI",
  "status": "QUEUED",
  "profile_name": "<事前に作成したProfile名>",
  "action": "narrate",
  "status_url": "./jobs/9A1F2C3D4E5F67890123456789ABCDEF"
}

image.png

もし ORA-27486: insufficient privilegesのエラーが発生した場合は、ジョブを作成する権限が無い可能性があります。
その場合は、対象ユーザーに対して GRANT CREATE JOB TO <ユーザー名>;を実行してください。

2. curl でステータスを確認する

返ってきた job_id を使って、GET /jobs/:job_id を呼び出します。

curl -s \
  "https://<adb-host>/ords/<schema>/ai/v1/jobs/9A1F2C3D4E5F67890123456789ABCDEF"

実行中であれば、以下のように RUNNING が返ります。

{
  "job_id": "9A1F2C3D4E5F67890123456789ABCDEF",
  "job_type": "SELECTAI",
  "status": "RUNNING",
  "profile_name": "<事前に作成したProfile名>",
  "prompt": "2026年4月の売上傾向を要約してください",
  "action": "narrate",
  "conversation_id": "demo-session-001",
  "result": null,
  "error_message": null
}

完了すると、statusSUCCEEDED になり、result に Select AI の結果が入ります。

{
  "job_id": "9A1F2C3D4E5F67890123456789ABCDEF",
  "job_type": "SELECTAI",
  "status": "SUCCEEDED",
  "profile_name": "<事前に作成したProfile名>",
  "prompt": "2026年4月の売上傾向を要約してください",
  "action": "narrate",
  "conversation_id": "demo-session-001",
  "result": "2026年4月の売上は、月前半にオンライン経由の購入が増加し...",
  "error_message": null
}

image.png

エラーになった場合は、statusFAILED になり、error_message に DB 側で記録したエラーが入ります。

今回のポイント

今回の実装で重要なのは、ORDS の Handler はあくまで REST API の入口にして、時間がかかる Select AI 実行は非同期ジョブに逃がしている点です。

構成としては以下の役割分担になります。

要素 役割
AI_REST_JOBS ジョブの状態、プロンプト、結果、エラーを保存する
ai_rest_worker.submit_job ジョブを登録し、Scheduler Job を作成する
ai_rest_worker.run_job Select AI を実行し、結果を保存する
POST /ai/v1/jobs/ REST API からジョブを受け付ける
GET /ai/v1/jobs/:job_id ジョブの状態と結果を返す

この形にしておくと、Select AI の処理時間が多少長くなっても、REST API の呼び出し元はタイムアウトに巻き込まれにくくなります。
また、ジョブ管理テーブルに状態が残るため、失敗時の確認や再実行の設計もしやすくなります。

まとめ

ORDS を使うと、Database 上の SQL や PL/SQL を REST API として公開できます。

前回の記事では、テーブルの SELECT 結果を REST API として返すシンプルな例を紹介しました。
今回はその応用として、ORDS から Select AI を非同期で実行する REST API を作成しました。

ポイントは以下です。

  • Select AI は LLM を呼び出すため、REST API では非同期実行にしておくと扱いやすい
  • POST /jobs/ ではジョブを登録し、202 Acceptedjob_id を返す
  • 実際の Select AI 実行は DBMS_SCHEDULER から PL/SQL パッケージを呼び出して行う
  • 実行状態と結果は AI_REST_JOBS テーブルで管理する
  • クライアントは GET /jobs/:job_id を定期的に呼び出して、SUCCEEDED になったら結果を表示する

これで、フロントエンドや外部アプリケーションから、ORDS の REST API 経由で Select AI を呼び出せるようになります。

Appendix

"必要なテーブルや PL/SQL パッケージ、ORDS の定義を作成する" にて記載したクエリの内容について解説します。

ジョブ管理テーブルを作成するブロック

Select AI の実行状態と結果を保存するテーブルです。

CREATE TABLE ai_rest_jobs (
  job_id          VARCHAR2(32) DEFAULT RAWTOHEX(SYS_GUID()) NOT NULL,
  job_type        VARCHAR2(30) DEFAULT 'SELECTAI' NOT NULL,
  status          VARCHAR2(30) DEFAULT 'QUEUED' NOT NULL,
  profile_name    VARCHAR2(128) DEFAULT '&&SELECT_AI_PROFILE' NOT NULL,
  prompt          CLOB NOT NULL,
  action          VARCHAR2(30) DEFAULT 'narrate' NOT NULL,
  conversation_id VARCHAR2(128),
  result          CLOB,
  error_message   CLOB,
  created_at      TIMESTAMP WITH LOCAL TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL,
  started_at      TIMESTAMP WITH LOCAL TIME ZONE,
  completed_at    TIMESTAMP WITH LOCAL TIME ZONE,
  created_by      VARCHAR2(128) DEFAULT SYS_CONTEXT('USERENV', 'SESSION_USER') NOT NULL,
  CONSTRAINT ai_rest_jobs_pk PRIMARY KEY (job_id),
  CONSTRAINT ai_rest_jobs_type_ck CHECK (job_type IN ('SELECTAI')),
  CONSTRAINT ai_rest_jobs_status_ck CHECK (
    status IN ('QUEUED', 'RUNNING', 'SUCCEEDED', 'FAILED', 'CANCEL_REQUESTED', 'CANCELLED', 'EXPIRED', 'TIMEOUT')
  ),
  CONSTRAINT ai_rest_jobs_action_ck CHECK (action IN ('narrate', 'chat', 'showsql', 'runsql'))
);

このテーブルに、プロンプト、Select AI profile 名、実行ステータス、実行結果、エラーメッセージを保存します。

resulterror_message は長くなる可能性があるため、CLOB にしています。
また、進捗確認 API ではステータスと作成日時で検索することが多いため、以下のインデックスも作成しています。

CREATE INDEX ai_rest_jobs_status_ix ON ai_rest_jobs (status, created_at);

Select AI 実行用の PL/SQL パッケージを作成するブロック

次に、Select AI のジョブ登録と実行処理をまとめた PL/SQL パッケージです。

パッケージ仕様は以下のようにしています。

CREATE OR REPLACE PACKAGE ai_rest_worker AS
  PROCEDURE submit_job(
    p_prompt          IN  CLOB,
    p_action          IN  VARCHAR2 DEFAULT 'narrate',
    p_profile_name    IN  VARCHAR2 DEFAULT '&&SELECT_AI_PROFILE',
    p_conversation_id IN  VARCHAR2 DEFAULT NULL,
    p_job_id          OUT VARCHAR2,
    p_status          OUT VARCHAR2
  );

  PROCEDURE run_job(
    p_job_id IN VARCHAR2
  );
END ai_rest_worker;
/

submit_job は ORDS の POST Handler から呼び出します。
ここでは AI_REST_JOBS にレコードを登録し、DBMS_SCHEDULER.CREATE_JOB でバックグラウンドジョブを作成します。

run_job は Scheduler Job から呼び出します。
ここで実際に DBMS_CLOUD_AI.GENERATE を実行し、結果を AI_REST_JOBS に保存します。

submit_job のポイント

submit_job では、まずプロンプトが空でないことを確認します。

IF p_prompt IS NULL OR DBMS_LOB.GETLENGTH(p_prompt) = 0 THEN
  RAISE_APPLICATION_ERROR(-20001, 'prompt は必須です。');
END IF;

次に、ジョブ管理テーブルへ QUEUED の状態で登録します。

INSERT INTO ai_rest_jobs (
  job_id,
  job_type,
  status,
  profile_name,
  prompt,
  action,
  conversation_id,
  created_by
) VALUES (
  l_job_id,
  'SELECTAI',
  'QUEUED',
  l_profile_name,
  p_prompt,
  l_action,
  p_conversation_id,
  COALESCE(SYS_CONTEXT('USERENV', 'CLIENT_IDENTIFIER'), SYS_CONTEXT('USERENV', 'SESSION_USER'))
);

その後、DBMS_SCHEDULER.CREATE_JOBrun_job を非同期に実行します。

DBMS_SCHEDULER.CREATE_JOB(
  job_name   => l_job_name,
  job_type   => 'PLSQL_BLOCK',
  job_action => 'BEGIN ai_rest_worker.run_job(''' || l_job_id || '''); END;',
  enabled    => TRUE,
  auto_drop  => TRUE,
  comments   => 'Select AI Simple 非同期実行ジョブ'
);

auto_drop => TRUE にしているため、Scheduler Job 自体は実行後に削除されます。
実行履歴や結果は AI_REST_JOBS テーブルに残します。

run_job のポイント

run_job では、対象のジョブを取得してステータスを RUNNING に更新します。

UPDATE ai_rest_jobs
SET    status = 'RUNNING',
       started_at = SYSTIMESTAMP,
       error_message = NULL
WHERE  job_id = p_job_id;

その後、DBMS_CLOUD_AI.GENERATE で Select AI を実行します。

l_result := DBMS_CLOUD_AI.GENERATE(
  prompt       => l_prompt,
  profile_name => l_profile_name,
  action       => l_action
);

成功した場合は SUCCEEDED にして結果を保存します。

UPDATE ai_rest_jobs
SET    status = 'SUCCEEDED',
       result = l_result,
       completed_at = SYSTIMESTAMP
WHERE  job_id = p_job_id;

失敗した場合は FAILED にして、SQLERRM とバックトレースを error_message に保存します。

UPDATE ai_rest_jobs
SET    status = 'FAILED',
       error_message = l_error_message,
       completed_at = SYSTIMESTAMP
WHERE  job_id = p_job_id;

REST API 側ではなく DB 側にエラー内容を残しておくことで、フロントエンドからも運用確認からも原因を追いやすくなります。

ORDS の REST API を定義するブロック

次に、ORDS の Module、Template、Handler 作成するブロックです。

今回の Module は select_ai_simple.ai.v1、base path は /ai/v1/ としています。

BEGIN
  ORDS.DEFINE_MODULE(
    p_module_name    => 'select_ai_simple.ai.v1',
    p_base_path      => '/ai/v1/',
    p_items_per_page => 25,
    p_status         => 'PUBLISHED',
    p_comments       => 'Select AI Simple asynchronous job API'
  );

  ORDS.DEFINE_TEMPLATE(
    p_module_name => 'select_ai_simple.ai.v1',
    p_pattern     => 'jobs/',
    p_comments    => 'Select AI job collection'
  );

  ORDS.DEFINE_TEMPLATE(
    p_module_name => 'select_ai_simple.ai.v1',
    p_pattern     => 'jobs/:job_id',
    p_comments    => 'Select AI job status'
  );
END;
/

これで以下の URL に対応する Template が作成されます。

https://<adb-host>/ords/<schema>/ai/v1/jobs/
https://<adb-host>/ords/<schema>/ai/v1/jobs/:job_id

POST /jobs/ の Handler

POST /jobs/ は、Select AI ジョブを登録する API です。
Handler の p_source_typeORDS.source_type_plsql にします。

ORDS.DEFINE_HANDLER(
  p_module_name   => 'select_ai_simple.ai.v1',
  p_pattern       => 'jobs/',
  p_method        => 'POST',
  p_source_type   => ORDS.source_type_plsql,
  p_mimes_allowed => 'application/json',
  p_comments      => 'Submit Select AI job',
  p_source        => q'~
    DECLARE
      l_job_id       VARCHAR2(32);
      l_status       VARCHAR2(30);
      l_profile_name VARCHAR2(128) := NVL(:profile_name, '&&SELECT_AI_PROFILE');
      l_action       VARCHAR2(30) := NVL(:action, 'narrate');
    BEGIN
      ai_rest_worker.submit_job(
        p_prompt          => TO_CLOB(:prompt),
        p_action          => l_action,
        p_profile_name    => l_profile_name,
        p_conversation_id => :conversation_id,
        p_job_id          => l_job_id,
        p_status          => l_status
      );

      :status_code := 202;

      OWA_UTIL.MIME_HEADER('application/json', FALSE);
      HTP.P('Cache-Control: no-store');
      OWA_UTIL.HTTP_HEADER_CLOSE;

      APEX_JSON.OPEN_OBJECT;
      APEX_JSON.WRITE('job_id', l_job_id);
      APEX_JSON.WRITE('job_type', 'SELECTAI');
      APEX_JSON.WRITE('status', l_status);
      APEX_JSON.WRITE('profile_name', l_profile_name);
      APEX_JSON.WRITE('action', l_action);
      APEX_JSON.WRITE('status_url', './jobs/' || l_job_id);
      APEX_JSON.CLOSE_OBJECT;
    EXCEPTION
      WHEN OTHERS THEN
        :status_code := 400;

        OWA_UTIL.MIME_HEADER('application/json', FALSE);
        HTP.P('Cache-Control: no-store');
        OWA_UTIL.HTTP_HEADER_CLOSE;

        APEX_JSON.OPEN_OBJECT;
        APEX_JSON.WRITE('error', SQLERRM);
        APEX_JSON.CLOSE_OBJECT;
    END;
  ~'
);

ここでは、リクエスト JSON の promptactionprofile_nameconversation_id を ORDS のバインド変数として受け取っています。

正常にジョブ登録できた場合は、HTTP ステータスを 202 にしています。
これは「リクエストは受け付けたが、処理はまだ完了していない」という意味です。

:status_code := 202;

レスポンスには Select AI の結果ではなく、ジョブ ID とステータス確認用の URL を返します。

GET /jobs/:job_id の Handler

GET /jobs/:job_id は、ジョブの状態と結果を取得する API です。

ORDS.DEFINE_HANDLER(
  p_module_name    => 'select_ai_simple.ai.v1',
  p_pattern        => 'jobs/:job_id',
  p_method         => 'GET',
  p_source_type    => ORDS.source_type_collection_item,
  p_items_per_page => 1,
  p_comments       => 'Get Select AI job status and result',
  p_source         => q'~
    SELECT job_id,
           job_type,
           status,
           profile_name,
           DBMS_LOB.SUBSTR(prompt, 32767, 1) AS prompt,
           action,
           conversation_id,
           created_at,
           started_at,
           completed_at,
           result,
           error_message
    FROM   ai_rest_jobs
    WHERE  job_id = :job_id
  ~'
);

1 件のジョブ情報を返す API なので、p_source_typeORDS.source_type_collection_item にしています。

レスポンスの例は以下のようになります。

{
  "job_id": "9A1F2C3D4E5F67890123456789ABCDEF",
  "job_type": "SELECTAI",
  "status": "SUCCEEDED",
  "profile_name": "<事前に作成したProfile名>",
  "prompt": "2026年4月の売上傾向を要約してください",
  "action": "narrate",
  "conversation_id": "demo-session-001",
  "created_at": "2026-05-01T10:00:00+09:00",
  "started_at": "2026-05-01T10:00:01+09:00",
  "completed_at": "2026-05-01T10:00:09+09:00",
  "result": "2026年4月の売上は...",
  "error_message": null
}

statusQUEUED または RUNNING の間は、クライアント側で数秒おきにこの API を呼び出します。
SUCCEEDED になったら result を表示し、FAILED になったら error_message を表示する、という流れです。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?