[Developer] add KB Select AI Team ORDS MCP tool

This commit is contained in:
devmrko
2026-07-08 06:43:29 +09:00
parent 1cb8052045
commit 61f7e4c775
11 changed files with 635 additions and 2 deletions

View File

@@ -0,0 +1,104 @@
-- ============================================================
-- 56_kb_select_ai_router_team.sql
--
-- KB Select AI profile을 호출하는 최소 Agent / Tool / Task / Team 체인.
-- Tool -> Task -> Router Agent -> Team
-- Execute as POC_2 using SQLcl.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET DEFINE OFF
SET FEEDBACK ON
PROMPT === Resetting only this Select AI router chain ===
BEGIN
DBMS_CLOUD_AI_AGENT.DROP_TEAM('KB_SELECT_AI_ROUTER_TEAM', force => TRUE);
DBMS_CLOUD_AI_AGENT.DROP_TASK('KB_SELECT_AI_ROUTER_TASK', force => TRUE);
DBMS_CLOUD_AI_AGENT.DROP_AGENT('KB_SELECT_AI_ROUTER_AGENT', force => TRUE);
DBMS_CLOUD_AI_AGENT.DROP_TOOL('KB_SELECT_AI_ROUTER_TOOL', force => TRUE);
END;
/
PROMPT === Creating Select AI SQL tool ===
BEGIN
DBMS_CLOUD_AI_AGENT.CREATE_TOOL(
tool_name => 'KB_SELECT_AI_ROUTER_TOOL',
attributes => q'~{
"tool_type": "SQL",
"tool_params": {
"profile_name": "KB_AIDP_SELECTAI_GPT55_OCI_PROFILE_V2"
},
"instruction": "Use the configured Select AI profile for structured KB insurance questions. For this demo, generate SHOWSQL only: create a read-only SELECT or WITH statement, do not execute it, and never generate DDL, DML, transaction control, package calls, or data changes."
}~',
status => 'ENABLED',
description => 'Calls KB_AIDP_SELECTAI_GPT55_OCI_PROFILE_V2 to generate read-only KB SQL.'
);
END;
/
PROMPT === Creating router agent ===
BEGIN
DBMS_CLOUD_AI_AGENT.CREATE_AGENT(
agent_name => 'KB_SELECT_AI_ROUTER_AGENT',
attributes => q'~{
"profile_name": "KB_AIDP_AGENT_GPT55_OCI_PROFILE_V2",
"role": "You are the KB Select AI routing agent. Classify every user request. If it asks for KB structured insurance data, metrics, counts, contracts, claims, customers, products, coverage, holdings, or stakeholders, call the assigned KB Select AI SQL tool. Return a concise Korean answer and the generated SQL. Do not invent data, do not expose internal reasoning, and do not use any unassigned tool.",
"enable_human_tool": false
}~',
status => 'ENABLED',
description => 'Routes KB structured-data questions to the Select AI SQL tool.'
);
END;
/
PROMPT === Registering tool task ===
BEGIN
DBMS_CLOUD_AI_AGENT.CREATE_TASK(
task_name => 'KB_SELECT_AI_ROUTER_TASK',
attributes => q'~{
"instruction": "Route this user request to the KB Select AI SQL tool when it requires structured insurance data: {query}. Use SHOWSQL only. Explain briefly in Korean what the generated SQL will retrieve. If the request is outside the KB structured-data scope, state that this team only handles KB structured-data SQL generation.",
"tools": ["KB_SELECT_AI_ROUTER_TOOL"],
"enable_human_tool": false
}~',
status => 'ENABLED',
description => 'Single Select AI SQL generation task for KB structured-data requests.'
);
END;
/
PROMPT === Creating agent task team ===
BEGIN
DBMS_CLOUD_AI_AGENT.CREATE_TEAM(
team_name => 'KB_SELECT_AI_ROUTER_TEAM',
attributes => q'~{
"agents": [
{
"name": "KB_SELECT_AI_ROUTER_AGENT",
"task": "KB_SELECT_AI_ROUTER_TASK"
}
],
"process": "sequential"
}~',
status => 'ENABLED',
description => 'KB Select AI router Team: SQL tool, task, agent, and team.'
);
END;
/
PROMPT === Registered chain ===
SELECT tool_name, status
FROM USER_AI_AGENT_TOOLS
WHERE tool_name = 'KB_SELECT_AI_ROUTER_TOOL';
SELECT agent_name, status
FROM USER_AI_AGENTS
WHERE agent_name = 'KB_SELECT_AI_ROUTER_AGENT';
SELECT task_name, status
FROM USER_AI_AGENT_TASKS
WHERE task_name = 'KB_SELECT_AI_ROUTER_TASK';
SELECT agent_team_name, status
FROM USER_AI_AGENT_TEAMS
WHERE agent_team_name = 'KB_SELECT_AI_ROUTER_TEAM';
EXIT

View File

@@ -0,0 +1,17 @@
-- Execute as POC_2 using SQLcl after 56.
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET PAGESIZE 100
SET LINESIZE 240
SET LONG 30000
SET LONGCHUNKSIZE 30000
SET FEEDBACK ON
SELECT DBMS_CLOUD_AI_AGENT.RUN_TEAM(
team_name => 'KB_SELECT_AI_ROUTER_TEAM',
user_prompt => 'KB 고객 원장의 전체 고객 수를 계산하는 SQL을 만들어 줘. SQL만 생성하고 실행하지 마.',
params => '{"conversation_id":"kb-select-ai-router-' ||
TO_CHAR(SYSTIMESTAMP, 'YYYYMMDDHH24MISSFF3') || '"}'
) AS agent_response
FROM dual;
EXIT

View File

@@ -0,0 +1,57 @@
-- ============================================================
-- 58_kb_select_ai_router_api.sql
--
-- POC_2 소유 Select AI Team을 ORDS runtime(CB_ORDS)에서 안전하게
-- 호출하기 위한 definer-rights API.
--
-- Execute as POC_2 using SQLcl after 56_kb_select_ai_router_team.sql.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO OFF
SET FEEDBACK ON
SET DEFINE OFF
PROMPT === Creating POC_2 Select AI Team API ===
CREATE OR REPLACE PACKAGE kb_select_ai_router_api AUTHID DEFINER AS
FUNCTION run_team(
p_prompt IN CLOB,
p_conversation_id IN VARCHAR2 DEFAULT NULL
) RETURN CLOB;
END kb_select_ai_router_api;
/
CREATE OR REPLACE PACKAGE BODY kb_select_ai_router_api AS
FUNCTION run_team(
p_prompt IN CLOB,
p_conversation_id IN VARCHAR2 DEFAULT NULL
) RETURN CLOB AS
v_params CLOB;
v_conversation_id VARCHAR2(128) := TRIM(p_conversation_id);
BEGIN
IF p_prompt IS NULL OR DBMS_LOB.GETLENGTH(TRIM(p_prompt)) = 0 THEN
RAISE_APPLICATION_ERROR(-20801, 'prompt is required');
END IF;
IF v_conversation_id IS NOT NULL THEN
IF NOT REGEXP_LIKE(v_conversation_id, '^[A-Za-z0-9._:-]{1,128}$') THEN
RAISE_APPLICATION_ERROR(-20802, 'conversationId contains unsupported characters');
END IF;
v_params := '{"conversation_id":"' || v_conversation_id || '"}';
END IF;
RETURN DBMS_CLOUD_AI_AGENT.RUN_TEAM(
team_name => 'KB_SELECT_AI_ROUTER_TEAM',
user_prompt => p_prompt,
params => v_params
);
END run_team;
END kb_select_ai_router_api;
/
SHOW ERRORS
PROMPT === Granting only the Team API to the ORDS runtime ===
GRANT EXECUTE ON kb_select_ai_router_api TO cb_ords;
PROMPT === POC_2 Select AI Team API ready ===
EXIT

View File

@@ -0,0 +1,143 @@
-- ============================================================
-- 59_kb_select_ai_router_ords.sql
--
-- POST /ords/cb-ords/kb-select-ai-agent/run
-- Authorization: Bearer <VPD token>
-- {"prompt":"...","conversationId":"optional-safe-id"}
--
-- The ORDS runtime validates the bearer and sets the application context
-- before invoking the POC_2-owned Team API. The context is session scoped,
-- so VPD still applies to any SQL action selected by the Select AI tool.
--
-- Execute as CB_ORDS after 22 and 58.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO OFF
SET FEEDBACK ON
SET DEFINE OFF
PROMPT === Resetting only the KB Select AI Agent ORDS module ===
BEGIN
ORDS.DELETE_MODULE(p_module_name => 'kb.select.ai.agent');
EXCEPTION
WHEN OTHERS THEN NULL;
END;
/
PROMPT === Creating KB Select AI Agent ORDS endpoint ===
BEGIN
ORDS.DEFINE_MODULE(
p_module_name => 'kb.select.ai.agent',
p_base_path => 'kb-select-ai-agent/',
p_items_per_page => 0,
p_status => 'PUBLISHED'
);
ORDS.DEFINE_TEMPLATE(
p_module_name => 'kb.select.ai.agent',
p_pattern => 'run'
);
ORDS.DEFINE_HANDLER(
p_module_name => 'kb.select.ai.agent',
p_pattern => 'run',
p_method => 'POST',
p_source_type => ORDS.source_type_plsql,
p_source => q'~
DECLARE
v_body_text CLOB;
v_prompt VARCHAR2(32767);
v_requested_conversation_id VARCHAR2(128);
v_internal_conversation_id VARCHAR2(128);
v_stakeholder_user_id VARCHAR2(4000);
v_answer CLOB;
BEGIN
cb_ords_handler_pkg.set_vpd_context(:auth_header, :probe_id);
-- ORDS exposes request JSON as a stream bind. Read it once only.
v_body_text := :body_text;
v_prompt := JSON_VALUE(v_body_text, '$.prompt' RETURNING VARCHAR2(32767));
v_requested_conversation_id := JSON_VALUE(v_body_text, '$.conversationId' RETURNING VARCHAR2(128));
IF v_prompt IS NULL OR TRIM(v_prompt) IS NULL THEN
RAISE_APPLICATION_ERROR(-20801, 'prompt is required');
END IF;
IF v_requested_conversation_id IS NULL THEN
v_requested_conversation_id := 'kb-' || LOWER(RAWTOHEX(SYS_GUID()));
ELSIF NOT REGEXP_LIKE(v_requested_conversation_id, '^[A-Za-z0-9._:-]{1,128}$') THEN
RAISE_APPLICATION_ERROR(-20802, 'conversationId contains unsupported characters');
END IF;
-- A connection-pooled ORDS session can be reused. Scope the Team's
-- conversation memory to the VPD subject as well as the caller's public ID.
v_stakeholder_user_id := NVL(SYS_CONTEXT('CB_AGENT_CTX', 'STAKEHOLDER_USER_ID'), 'anonymous');
v_internal_conversation_id := 'kb-' || LOWER(RAWTOHEX(STANDARD_HASH(
v_stakeholder_user_id || ':' || v_requested_conversation_id,
'SHA256'
)));
v_answer := poc_2.kb_select_ai_router_api.run_team(
p_prompt => v_prompt,
p_conversation_id => v_internal_conversation_id
);
:status_code := 200;
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('team', 'KB_SELECT_AI_ROUTER_TEAM');
APEX_JSON.WRITE('conversationId', v_requested_conversation_id);
APEX_JSON.WRITE('answer', v_answer);
APEX_JSON.CLOSE_OBJECT;
cb_ords_handler_pkg.clear_vpd_context;
EXCEPTION
WHEN OTHERS THEN
cb_ords_handler_pkg.clear_vpd_context;
:status_code := CASE
WHEN SQLCODE IN (-20801, -20802) OR SQLCODE BETWEEN -40599 AND -40400 THEN 400
WHEN SQLCODE BETWEEN -20199 AND -20100 THEN 403
ELSE 500
END;
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('errorCode', SQLCODE);
APEX_JSON.WRITE('error', SQLERRM);
APEX_JSON.CLOSE_OBJECT;
END;
~',
p_items_per_page => 0
);
ORDS.DEFINE_PARAMETER(
p_module_name => 'kb.select.ai.agent',
p_pattern => 'run',
p_method => 'POST',
p_name => 'Authorization',
p_bind_variable_name => 'auth_header',
p_source_type => 'HEADER',
p_param_type => 'STRING',
p_access_method => 'IN'
);
ORDS.DEFINE_PARAMETER(
p_module_name => 'kb.select.ai.agent',
p_pattern => 'run',
p_method => 'POST',
p_name => 'X-VPD-Probe-Id',
p_bind_variable_name => 'probe_id',
p_source_type => 'HEADER',
p_param_type => 'STRING',
p_access_method => 'IN'
);
COMMIT;
END;
/
PROMPT === KB Select AI Agent ORDS endpoint ready ===
PROMPT Path: /ords/cb-ords/kb-select-ai-agent/run
EXIT