Add tools that read contract data
By the end of this lesson, you can add read tools that filter their results by the contract ID in the run context.
Tool calls and contract access
Section titled “Tool calls and contract access”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.
Register the three read tools
Section titled “Register the three read tools”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.
Read the contract ID from the run context
Section titled “Read 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 clobas 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 textend 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.
Give the tools to the agent
Section titled “Give the tools to the agent”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.
Run the agent with a contract ID
Section titled “Run the agent with a contract ID”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;/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.
Check the declared tool parameters
Section titled “Check the declared tool parameters”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_PARAMETERSSC_GET_CONTRACT (none)SC_LIST_CALLS max_rowsSC_LIST_INVOICES max_rowsNone 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.
Test the contract filter
Section titled “Test the contract filter”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: 3Three 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.
Key takeaways
Section titled “Key takeaways”- 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 underuc_ai.c_run_context_key._ctxdecides 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.