Skip to content

Add tools that read contract data

The agent from lesson 2 needs tools to retrieve contract data.

A tool is a PL/SQL function that a model can ask you to run. You register it one time, with three things: a code, a description, and a JSON schema that names the arguments it takes. UC AI then offers it to the model on every run of that agent.

During a run, the model reads the descriptions, decides that it needs SC_LIST_INVOICES, and returns the arguments for it instead of an answer. UC AI calls your function with those arguments as JSON, sends what the function returned back to the model, and the model carries on. Your function is ordinary PL/SQL, and it runs in your schema with your privileges.

The tool interface separates two responsibilities:

  • The model never reads your tables. It asks your function, and your function reads your tables.
  • The model chooses the arguments, out of the schema you wrote for them.

If one of those arguments is the contract, then the model chooses the contract. An engineer on contract 88 must never get an answer about contract 99.

The application supplies the contract ID through the run context. The handler uses that ID to filter its query and ignores a model-supplied contract ID. The application must first establish that the caller can access the selected contract.

Run @03_tools_read.sql. It registers three tools, updates the profile, and runs the agent. Keep the value printed after SESSION: for lesson 4.

The script registers SC_GET_CONTRACT, SC_LIST_CALLS, and SC_LIST_INVOICES. Each tool has the tag scdesk, which the profile uses to select tools. The following registration is an excerpt from the script:

declare
l_tool_id number;
begin
l_tool_id := uc_ai_tools_api.merge_tool_from_schema(
p_tool_code => 'SC_LIST_INVOICES'
, p_description => 'List the invoices of the contract of this conversation, with '
|| 'the parts amount, the labour amount, how much is already '
|| 'credited, and how much is still uncredited.'
, p_function_call => 'return sc_desk_pkg.list_invoices(:ARGUMENTS);'
, p_json_schema => json_object_t('{
"type": "object",
"properties": {
"max_rows": { "type": "integer", "description": "How many invoices at most. Defaults to 20." }
},
"required": []
}')
, p_tags => apex_t_varchar2('scdesk')
);
end;
/

There is no contract_id in that schema, and there is none in the other two. The model has no declared contract parameter. The handler enforces the restriction by reading the contract ID from the run context.

The application passes a run context when it starts the run. This JSON object contains application-supplied values, here {"contract_id":"88"}. UC AI adds it to the tool arguments under the reserved key _ctx after the model returns those arguments. The handler uses this application-supplied context.

The handler reads the contract from _ctx, never from the model. This excerpt shows the query structure. The package compiled in lesson 1 contains the full handler:

function list_invoices(p_arguments in clob) return clob
as
l_args json_object_t := json_object_t(p_arguments);
l_ctx json_object_t := l_args.get_object(uc_ai.c_run_context_key);
l_contract number;
l_result clob;
begin
-- The contract comes from the run context, never from the arguments.
l_contract := to_number(l_ctx.get_string('contract_id'));
select json_object(
'count' value count(*)
, 'invoices' value json_arrayagg(
json_object('invoice_no' value i.invoice_no
, 'total_amount' value i.parts_amount + i.labour_amount)
order by i.invoice_no)
returning clob)
into l_result
from sc_invoices i
where i.contract_id = l_contract; -- the one contract this run sees
return l_result; -- the model reads this JSON as text
end list_invoices;

The full sc_desk_pkg.list_invoices handler also returns credit amounts and coverage information. It returns a refusal when the run context has no contract ID.

Tools reach the model through the model configuration of the prompt profile. The three keys below are named after the package globals they override, but through an agent they live on the profile row, not in the package. The tag scdesk selects the tools with that tag. At this point in the tutorial, those are the three read tools.

declare
l_profile uc_ai_prompt_profiles%rowtype;
l_config constant clob := '{
"g_enable_tools": true,
"g_tool_tags": ["scdesk"],
"g_max_tool_calls": 8
}';
begin
l_profile := uc_ai_prompt_profiles_api.get_prompt_profile('SC_DESK_PROFILE', 1);
l_profile.model_config_json := l_config;
uc_ai_prompt_profiles_api.update_prompt_profile(p_profile => l_profile);
commit;
end;
/

This changes version 1, the version the agent already resolves to. It does not make a version 2, so the agent needs no change.

The script runs the following block with contract 88 in the run context. It asks about both remaining credit and coverage. You can run this block again to repeat the request.

declare
l_result json_object_t;
l_session varchar2(255 char) := uc_ai_agents_api.generate_session_id;
begin
l_result := uc_ai_agents_api.execute_agent(
p_agent_code => 'SC_DESK'
, p_input_parameters => json_object_t('{"engineer_name":"Petra"
,"today":"' || to_char(sysdate, 'YYYY-MM-DD') || '"
,"question":"How much can I still credit on INV-1003, and is the call inside the coverage window?"}')
, p_session_id => l_session
, p_run_context => json_object_t('{"contract_id":"88"}')
);
sys.dbms_output.put_line('SESSION: ' || l_session);
sys.dbms_output.put_line(l_result.get_clob('final_message'));
end;
/
Recorded answergpt-5.6-terra9.4s2026-09-10

INV-1003 has €0 left to credit; its full €200 has already been credited.

The related call (31 Aug 2026) is inside the coverage window.

Your wording will differ. What must match is the data, and the checks below.

In this recorded run, the model called SC_GET_CONTRACT, SC_LIST_INVOICES, and SC_LIST_CALLS before answering. The tool results included the existing credit, so the model reported the correct remaining amount. Lesson 4 shows the trace.

The script displays the declared parameters. You can repeat this query to inspect them:

select t.code
, nvl(( select listagg(p.name, ', ' on overflow truncate)
within group (order by p.name)
from uc_ai_tool_parameters p
where p.tool_id = t.id ), '(none)') as declared_parameters
from uc_ai_tools t
where t.code like 'SC\_%' escape '\'
order by t.code;
CODE DECLARED_PARAMETERS
SC_GET_CONTRACT (none)
SC_LIST_CALLS max_rows
SC_LIST_INVOICES max_rows

None of these tools declares a contract parameter. UC AI also reserves _ctx and rejects tool schemas that declare it, including nested properties and case variations. This returns ORA-20503. If you try that registration, run rollback to undo the tool row created before parameter validation.

Run this block to test the handler directly. It supplies contract 99 as a tool argument and contract 88 in the run context. The handler must use contract 88.

declare
l_args json_object_t;
l_out json_object_t;
begin
l_args := json_object_t('{"max_rows":50,"contract_id":99}');
l_args.put(uc_ai.c_run_context_key, json_object_t('{"contract_id":"88"}'));
l_out := json_object_t(sc_desk_pkg.list_invoices(l_args.to_clob));
sys.dbms_output.put_line('invoices returned: ' || l_out.get_number('count'));
end;
/
invoices returned: 3

Three invoices, and all three belong to contract 88. Contract 99 has one invoice, INV-2001, and it does not appear. The forged argument was ignored, because the handler never reads it.

This check exercises the handler without depending on the model to request another contract.

  • Read the contract ID from the application-supplied run context in each handler.
  • Test the handler directly with a conflicting contract ID in the tool arguments.
  • p_run_context => json_object_t('{"contract_id":"88"}') reaches every tool of the run under uc_ai.c_run_context_key.
  • _ctx decides what a tool answers, not which tools the agent has. Give the desk only tools that the run context can bind.

Full reference: Tools and function calling covers registration, the JSON schema and handler patterns, and the Quickstart provides a shorter registration example. The run context covers _ctx, the reserved name, and where else the run context applies.