
Build Real AI Solutions with Oracle PL/SQL (No 23ai Required)
TL;DR
- 95% of AI pilots fail, but back-office automation succeeds - a huge opportunity for data engineers
- No new languages required - if you know PL/SQL/SQL, AI is just a thin layer on top
- Learn practical techniques: structured output, file analysis, reasoning, and tool calling
- Open-source UC AI SDK provides unified access to multiple AI providers (OpenAI, Claude, Gemini, etc.)
Watch the Livestream
This post is mostly a write-up of my webinar talk I did last week: United Codes, Live! - Practical AI in PL/SQL Made Easy. In the webinar, I discuss the concepts more thoroughly. If you want additional context while reading, check out the corresponding chapter in the video.
Table of Contents
Please use the table of contents on the right side in the header to navigate through the post.
AI and Oracle
#The AI hype train is at full speed. Naturally, stakeholders have all considered its place in the Oracle space.
Why care about AI in Oracle
#Everybody is building chat nowadays. If you have an established and working UI, consider asking your users if they would actually benefit from it. I really like that the website of my bank still is the same old one where I immediately see my balance and still can do a transaction without having to chat with a bot.
Instead, if you are a data engineer, you should look at it from a different angle. Don’t just add LLMs to the UI; think about your data being processed in the DB. AI excels in three key areas:
- Data Analysis: Summarize, classify, categorize, cluster, and detect anomalies (faster than humans, sometimes with hallucinations!)
- Unstructured to Structured Data: Transform blobs of text or documents into relational data
- Process Automation: Enable intelligent workflows and automated decision-making
The Problem with Existing Solutions
#In my view, Oracle’s current solutions fall short in key areas and are not sufficient to compete with other programming environments.
Oracle 23ai SELECT AI
- 23ai only: Only available in Oracle 23ai, which is not on-premises yet. So not interesting for most customers.
- Lack of features: The focus is on converting natural language to SQL statements. I would argue these are niche use cases. Most other techniques I describe in this post are not supported.
Oracle APEX AI Package
- Limited providers: The selection of supported AI providers is currently limited to Cohere, OpenAI, and some outdated OCI services. Big players like Google, Anthropic, xAI, and open models are not natively supported.
- Lack of features: Same as with SELECT AI, only basic text generation is supported, and it lacks the more in-depth techniques.
Of course these solutions will improve over time. The AI world is moving fast, so they will probably be in an outdated state again. Currently the APEX World is waiting for version 25.1 after almost three-quarters of the year passed. And there is no way to enhance the solutions on your own.
Frustrated, I decided to built my own solution.
Introducing UC AI: A Game-Changing PL/SQL SDK
#As I wanted to try out advanced AI features from the database myself, I wrote PL/SQL packages to call their APIs. This led to the idea of just open-sourcing it.
Key Features
#- Free and Open Source: Available on GitHub
- Broad Compatibility: Works with old Oracle versions (probably 12.2, tested with 18c)
- Unified Interface: One API for multiple AI providers
- Provider Independence: Easy switching between models and providers. Currently supports 5 providers: OpenAI GPT, Anthropic Claude, Google Gemini, Ollama (offline models), and Oracle Cloud Infrastructure (OCI) (Llama, Cohere, xAI Grok, Google Gemini)
- Advanced features: tools, reasoning, files, structured output
- Documentation: can always be improved, but I really care for good documentation (please open issues or PRs if you find room for improvement)
Unified Approach
#Different AI providers often have varying APIs. UC AI abstracts these differences, allowing you to:
- Switch providers without code changes
- Compare model performance easily
- Avoid vendor lock-in
- Focus on business logic rather than API implementations
Multiple AI Providers
#The AI provider landscape is pretty versatile and moving fast.
Why Model and Provider Choice Matters
#Different models excel in different areas:
- Intelligence
- Speed
- Cost
Unfortunately, it is not as easy as picking the best model. Intelligent models are rather slow and pricey. Picking a model is also about understanding what the task involves. It does not need expensive and intelligent models to summarize medium-sized text.
Even though OpenAI made the initial bang and ChatGPT is the most used platform, it is a mistake to just look at them. Different models have different personas and also excel at different things. Claude, for example, is by far the best for coding PL/SQL and for tool calling, in my experience. But recently only Gemini was able to answer a specific question I had correctly. My tip: get your fingers dirty and just try out the same task with different models and compare the results. For this reason I use t3.chat instead of ChatGPT or Claude, as you can run a prompt with different providers (I am not affiliated).
Also, platforms like Artificial Analysis provide great benchmarks to compare models. This picture shows one of their benchmarks over time, making it a good chart to show AI progress and competition over time. OpenRouter shares rankings on which models are mostly used and also in which industry.
Supported Providers
#For more details, please look at the documentation for each provider.
- OpenAI (GPT models)
- Anthropic (Claude models)
- Google (Gemini models)
- Ollama (local/offline models: Meta Llama, Google Gemma, Alibaba Qwen, DeepSeek, GPT-OSS)
- Oracle Cloud Infrastructure (Cohere, Meta Llama, xAI Grok, Google Gemini)
Offline Models: Privacy and Control
#Don’t underestimate offline models either. While the frontier models are not offline, they still offer tremendous performance, with the Chinese companies pushing the boundaries every month. Read my last blog post for more information, but the obvious advantages are
- Complete Privacy: No data leaves your network
- Cost Efficiency: After initial hardware investment
UC AI supports offline models via Ollama. You can browse the available models on their site. At United Codes, we have recently upgraded a local AI server dedicated to offline AI usage.
Generate Text: Your First Steps with AI
#The core function that LLMs offer is generating text. This is how you do it with UC AI:
1declare
2 l_result json_object_t;
3begin
4 l_result := uc_ai.generate_text(
5 p_user_prompt => 'What is the most common insurance claim type?',
6 p_provider => uc_ai.c_provider_openai,
7 p_model => uc_ai_openai.c_model_gpt_5
8 );
9
10 dbms_output.put_line(l_result.get_clob('final_message'));
11end;
12
System vs User Prompts
#UC AI supports both user and system prompts:
- User Prompt: The actual question or request
- System Prompt: Instructions that constrain the model’s behavior and persona
ChatGPT (the app, not the model itself), for example, uses an unrestrictive system prompt, as you can use it for many things. Inside business apps, on the other hand, “only answer questions for X” is a useful part to make the assistant at car dealerships refuse to debate the philosophy of Fight Club with customers.
You can just add the p_system_prompt
parameter to the call:
1declare
2 l_result json_object_t;
3begin
4 l_result := uc_ai.generate_text(
5 p_user_prompt => 'Explain the philosophy of fight club',
6 p_system_prompt => 'You are an assistant to help users
7 ask question about the inventory of our car dealership.
8 Only ask questions about the dealership and its cars.'
9 p_provider => uc_ai.c_provider_openai,
10 p_model => uc_ai_openai.c_model_gpt_5
11 );
12
13 dbms_output.put_line(l_result.get_clob('final_message'));
14end;
15
Switching Model and Provider
#Just change the p_model
and p_provider
parameters:
1declare
2 l_result json_object_t;
3begin
4 l_result := uc_ai.generate_text(
5 p_user_prompt => 'Explain the philosophy of fight club',
6 p_provider => uc_ai.c_provider_anthropic, -- or uc_ai.c_provider_google
7 p_model => uc_ai_anthropic.c_model_claude_4_sonnet -- or uc_ai_google.c_model_gemini_2_5_flash
8 );
9
10 dbms_output.put_line(l_result.get_clob('final_message'));
11end;
12
Practical Example: Insurance Settlement Letters
#As a theme for the demos, I have chosen an insurance company setting. We manage claims of users about damages on their property, like vehicles.
Instead of manually writing settlement letters, we can use uc_ai.generate_text
to generate them. We are using these prompts:
- System: Generate an insurance settlement letter for the following case. This is for demo purposes, so feel free to make up details to make it more realistic. The name of the insurance is: APEX Insurance in Redwood Shores. The person sending the letter is L. Ellison. Return just the letter, nothing else. Use markdown formatting, but don’t wrap the response in a markdown code block.
- User: #CLAIM_JSON# (will get replaced with actual JSON data)
The user prompt holds just a placeholder that we will swap out with the result of this query:
1SELECT
2 JSON_OBJECT(
3 'settlementId' VALUE s.settlement_id, -- 101
4 'claimNumber' VALUE s.claim_number, -- CL2025-001-AUTO
5 'policyNumber' VALUE s.policy_number, -- POL-AX789
6 'policyType' VALUE s.policy_type, -- Auto
7 'incidentDate' VALUE TO_CHAR(s.incident_date, 'YYYY-MM-DD'), -- 2025-01-15
8 'incidentDescription' VALUE s.incident_description, -- Minor fender bender on Main Street
9 'claimantFirstName' VALUE s.claimant_first_name, -- Alice
10 'claimantLastName' VALUE s.claimant_last_name, -- Smith
11 'claimantEmail' VALUE s.claimant_email, -- alice.smith@example.com
12 'claimantPhone' VALUE s.claimant_phone, -- +33612345678
13 'insuredFirstName' VALUE s.insured_first_name, -- Bob
14 'insuredLastName' VALUE s.insured_last_name, -- Smith
15 'settlementDate' VALUE TO_CHAR(s.settlement_date, 'YYYY-MM-DD'), -- 2025-02-01
16 'settlementAmount' VALUE s.settlement_amount, -- 1500
17 'currencyCode' VALUE s.currency_code, -- EUR
18 'settlementStatus' VALUE s.settlement_status, -- Proposed
19 'releaseSignedDate' VALUE TO_CHAR(s.release_signed_date, 'YYYY-MM-DD'), -- 2025-03-02
20 'paymentDueDate' VALUE TO_CHAR(s.payment_due_date, 'YYYY-MM-DD'), -- 2025-06-01
21 'paymentPaidDate' VALUE TO_CHAR(s.payment_paid_date, 'YYYY-MM-DD'), -- 2025-04-21
22 'negotiatorAgentName' VALUE s.negotiator_agent_name, -- Jean Dupont
23 'notes' VALUE s.notes -- Beneficiary confirmed receipt of funds.
24 ) AS settlement_json
25INTO :P1_SETTLEMENT_DATA
26FROM pame_settlement_demo s
27WHERE s.settlement_id = :P1_SETTLEMENT_ID;
28
AI response
APEX Insurance 123 Coastal Way Redwood Shores, CA 94065
February 1, 2025
Alice Smith [Claimant's Address - Fictional, as not provided in JSON] 101 Oak Avenue Anytown, France 12345
Subject: Settlement Confirmation for Claim CL2025-001-AUTO – Policy POL-AX789 – Insured: Bob Smith
Dear Ms. Smith,
This letter serves to formally confirm the settlement of your auto insurance claim, CL2025-001-AUTO, related to the incident on January 15, 2025, involving our policyholder, Mr. Bob Smith, under policy number POL-AX789.
We understand this claim resulted from a minor fender bender on Main Street, and we appreciate your cooperation throughout the adjustment process. Our Claims Negotiator, Jean Dupont, has worked diligently to facilitate a fair resolution.
We are pleased to inform you that APEX Insurance has processed the final settlement for your claim in the amount of €1,500.00 EUR (One Thousand Five Hundred Euros).
We confirm that the Release of Liability form, which you signed on March 2, 2025, was duly received. The payment of €1,500.00 EUR was processed and disbursed on April 21, 2025. Our records indicate that you have confirmed receipt of these funds.
We trust that this settlement brings a satisfactory close to this matter. Should you have any further questions, please do not hesitate to contact our claims department at the number below.
Sincerely,
L. Ellison Claims Adjuster APEX Insurance Phone: +1-800-555-APEX Email: claims@apexinsurance.com
I used Gemini Flash 2.5, and it was even smart enough to point out that we haven’t passed an address for the claimant.
Structured Output: From Text to JSON
#LLMs are superb at quickly processing and outputting text. In our database we store data in a structured way, tables with typed columns. AIs can help extract the important details from unstructured text like an email and return it in a defined structure.
JSON Schemas
#JSON Schema is a standard to describe JSON structures inside JSON. You can define which attributes an object has with type and required info:
1{
2 "$schema": "http://json-schema.org/draft-07/schema#",
3 "type": "object",
4 "title": "Response with confidence score",
5 "properties": {
6 "response": {
7 "type": "string",
8 "description": "The generated response text"
9 },
10 "confidence": {
11 "type": "number",
12 "description": "Confidence score between 0 and 1",
13 "minimum": 0,
14 "maximum": 1
15 }
16 },
17 "required": ["response", "confidence"]
18}
19
20// Valid example:
21// {
22// "response": "The capital of France is Paris.",
23// "confidence": 1
24//}
25
Real-World Example: Email Processing
#To help customers to conveniently open claims, we allow for them to send the details just per email. We want to create a record in our claim table with the info. Instead of letting our employees type it in there manually, we can ask AI to extract the details.
This time we will use a bigger JSON schema with nested objects and many details, including a claim number, a date of loss, and personal information. We also ask the AI to extract an array of damage types from the text, making it some kind of automated tagging system.
JSON Schema
{ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "claim_number": { "type": "string", "description": "The unique identifier for the insurance claim." }, "date_of_loss": { "type": "string", "description": "The date when the loss or damage occurred." }, "time_of_loss": { "type": "string", "description": "The approximate time when the loss or damage occurred." }, "loss_location": { "type": "object", "properties": { "street": { "type": "string", "description": "The street address of the loss location." }, "city": { "type": "string", "description": "The city of the loss location." }, "province_state": { "type": "string", "description": "The province or state of the loss location." }, "postal_code": { "type": "string", "description": "The postal code of the loss location." }, "country": { "type": "string", "description": "The country of the loss location." } }, "required": [ "street", "city", "province_state", "postal_code", "country" ] }, "damage_types": { "type": "array", "description": "An array of types of damage reported.", "items": { "type": "string" } }, "damaged_vehicles": { "type": "array", "description": "An array of details for damaged vehicles.", "items": { "type": "object", "properties": { "car": { "type": "string", "description": "Car brand and model" }, "license_plate": { "type": "string", "description": "License plate of the damaged car" } } } }, "estimated_total_damage_eur": { "type": "number", "description": "The total estimated cost of all damages in Euros." }, "police_report_id": { "type": "string", "description": "The ID of the police report filed, if any." }, "contact_info": { "type": "object", "properties": { "phone_number": { "type": "string", "description": "The claimant's phone number." }, "email": { "type": "string", "description": "The claimant's email address." } }, "required": [ "phone_number", "email" ] } }, "required": [ "claim_number", "date_of_loss", "time_of_loss", "loss_location", "damage_types", "estimated_total_damage_eur", "contact_info" ] }
As an example email, we use a natural text that has all the required information.
Property Damage sample text
Subject: Urgent: Claim #20240902-A1B2C3 - Property Damage - 123 Maple Street Dear APEX Insurance, This letter serves as a formal notification of a property damage claim. On August 28, 2024, at approximately 14:30 GMT+2, a severe hailstorm struck my residence located at 123 Maple Street, Anytown, AB 12345, Canada. The primary damage incurred includes significant roof damage (multiple shattered tiles, leaks), extensive damage to the two vehicles parked in the driveway (a 2020 Honda Civic with license plate XYZ-789 and a 2022 Tesla Model 3 with license plate ABC-456), and several broken windows on the north side of the house. I have attached photographs documenting the damage, including timestamps. The estimated cost of repairs for the roof alone is approximately €15,000. For the vehicles, I've received preliminary estimates of €3,500 for the Honda Civic and €7,000 for the Tesla Model 3. Total estimated damages are around €25,500. I have already contacted a local roofing contractor, 'RoofMasters Inc.', and an auto repair shop, 'Apex Auto Solutions', for detailed assessments. I've also filed a police report (Report ID: PR20240902-001) for the widespread storm damage in the area. Please advise on the next steps for inspection and processing this claim. My contact number is +1-555-123-4567 and email is Tyler.Durden@example.com. Sincerely, Tyler
Now we can use UC AI with an additional parameter p_response_json_schema
.
1declare
2 l_result json_object_t;
3 l_schema json_object_t;
4begin
5 l_schema := json_object_t('{"$schema": "http://json ...');
6
7 l_result := uc_ai.generate_text(
8 p_system_prompt => 'Please extract the information
9 from this text into the given JSON schema.'
10 p_user_prompt => 'Subject: Urgent: Claim #20240902-A1B2C3...',
11 p_provider => uc_ai.c_provider_openai,
12 p_model => uc_ai_openai.c_model_gpt_5,
13 p_response_json_schema => l_schema
14 );
15end;
16
In response, we get back the extracted data in the JSON format we defined.
1{
2 "claim_number": "20240902-A1B2C3",
3 "date_of_loss": "2024-08-28",
4 "time_of_loss": "14:30 GMT+2",
5 "loss_location": {
6 "street": "123 Maple Street",
7 "city": "Anytown",
8 "province_state": "AB",
9 "postal_code": "12345",
10 "country": "Canada"
11 },
12 "damage_types": ["roof damage", "vehicle damage", "broken windows"],
13 "damaged_vehicles": [
14 {
15 "car": "2020 Honda Civic",
16 "license_plate": "XYZ-789"
17 },
18 {
19 "car": "2022 Tesla Model 3",
20 "license_plate": "ABC-456"
21 }
22 ],
23 "estimated_total_damage_eur": 25500,
24 "police_report_id": "PR20240902-001",
25 "contact_info": {
26 "phone_number": "+1-555-123-4567",
27 "email": "Tyler.Durden@example.com"
28 }
29}
30
If there is missing information or uncertainty, you could then let an employee look over the email or let AI respond to the customer with questions.
Building JSON Schemas
#Constructing large JSON schemas can be tedious. To help, you can ask AI to draft one from your table structure and use the JSON Schema Builder from the UC AI documentation. There is a more visual experience that lets you create new or modify existing schemas.
File Analysis: Beyond Text Processing
#Modern AI models are actually multi-modal. That means they can process text and files. This means you don’t have to run separate OCR services; you can just send files with UC AI.
For our insurance examples, we could help AI analyze scans of letters or pictures of accidents.
- PDF Processing: Extract text from scanned documents
- Image Recognition: Analyze photos and diagrams
Analyzing Car Accident Photos
#Let’s take this photo of a car accident as an example. We are interested in what kind of car is in the picture, which parts are damaged, and what the estimated repair costs are (image source: Thue on Wikimedia).
As we learned from the last chapter, it makes sense to get the information back in a structured way. So let’s again send a JSON schema with our request.
JSON Schema for car damage analysis
{ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "title": "Car crash", "properties": { "car": { "type": "object", "description": "Car information", "properties": { "brand": { "type": "string", "description": "brand of the car" }, "model": { "type": "string", "description": "car model" }, "confidence": { "type": "number", "description": "confidence score of detected model and brand between 1 and 0", "minimum": 0, "maximum": 1 } } }, "broken_parts": { "type": "array", "description": "List protential broken car parts", "items": { "type": "string" } }, "damage_estimation_eur": { "type": "number", "description": "Estimate the car damage in euro" }, "write_off_estimation": { "type": "number", "description": "Estimate between 1 and 0 how likely it is that repairs would be more expensive then the car value", "minimum": 0, "maximum": 1 } }, "required": [ "car" ] }
The UC AI call looks a bit more complicated than before. The reason is that we both send a text message and the image. In this case we need to wrap both in a content array by using helper functions from the uc_ai_message_api
package.
1declare
2 l_result json_object_t;
3 l_schema json_object_t;
4 l_messages json_array_t := json_array_t();
5 l_content json_array_t := json_array_t();
6
7 l_file_record pame_files%rowtype;
8begin
9 -- load image file row
10 select *
11 into l_file_record
12 from pame_files
13 where file_name = :P20_FILE;
14
15 -- create file content metadata
16 l_content.append(uc_ai_message_api.create_file_content(
17 p_media_type => l_file_record.mime_type,
18 p_data_blob => l_file_record.file_content,
19 p_filename => l_file_record.file_name
20 ));
21
22 -- add text content with our prmompt
23 l_content.append(uc_ai_message_api.create_text_content(
24 'Please extract the info for the car crash. Just do your best estimations.'
25 ));
26
27 -- combine image and message into a user mesage
28 l_messages.append(uc_ai_message_api.create_user_message(l_content));
29
30 -- define structured output schema
31 l_schema := json_object_t('{ "$schema": ...');
32
33 l_result := uc_ai.generate_text(
34 p_messages => l_messages,
35 p_provider => uc_ai.c_provider_google,
36 p_model => uc_ai_google.c_model_gemini_2_5_flash,
37 p_response_json_schema => l_schema
38 );
39
40 :P20_OUTPUT := l_result.get_clob('final_message');
41 :P20_FULL_RESPONSE := l_result.to_clob;
42end;
43
44
Response
#Gemini Flash 2.5 tells us the car is a Renault Megane Scenic. I am not a car expert, but it is definitely a Renault, and what’s left of it looks similar to a Megane. Interestingly, it manages to list many broken parts correctly. The car is in a state where it is probably easier to list the parts that are not harmed, but other models do a much worse job.
1{
2 "car": {
3 "brand": "Renault",
4 "confidence": 0.8,
5 "model": "Megane Scenic"
6 },
7 "broken_parts": [
8 "front bumper",
9 "hood",
10 "windshield",
11 "front headlights",
12 "engine compartment components",
13 "right front fender",
14 "roof",
15 "front chassis",
16 "right front wheel suspension",
17 "passenger side door"
18 ],
19 "damage_estimation_eur": 10000,
20 "write_off_estimation": 0.95
21}
22
Reasoning and Thinking: Smarter AI Responses
#Reasoning (also called “thinking”) represents a significant advancement in AI capabilities, allowing models to process information internally before using the thoughts to formulate a final answer. It works the same way as we recommend other humans to think before speaking.
Benefits and Trade-offs
#Smarter AI sounds great. In benchmarks the reasoning models have a clear advantage over traditional ones. But beware, they also have downsides.
Advantages:
- Higher accuracy on complex tasks
- Better performance on analytical problems
- More thorough consideration of edge cases
- Insights into how LLM came to conclusions (but some providers don’t share thinking steps)
Costs:
- Significantly higher token usage/costs (you pay for thinking tokens)
- Longer response times
- Higher computational requirements
So it makes sense to only use reasoning when it is really needed. Just test if a non-reasoning model can do the task with similar results.
Practical Example: Data Anomaly Detection
#To put this feature to the test, we will let the LLMs analyze a batch of ten claims. We will just give the prompt “Notice anything?” and see what the response is.
If we use small models like Qwen3 with 1.7 billion parameters, the difference is quite obvious. Without reasoning enabled, it will just do a summarization of the text but from top to bottom. It feels like it reads a bit, then summarizes that part, then reads the next part, etc. But with reasoning enabled, it feels like it actually has the whole input in context and does output statistics like the most claims are about vehicle damage and Texas seems to have the most cases.
With a much smarter model GPT-4o
it already does a better job of actually summarizing the whole document without reasoning enabled. But most interestingly, if we enable it with OpenAI’s o4-mini
model, it detects anomalies in the data:
1I spotted a couple of internal inconsistencies in the data—specifically, the narrative dates don’t line up with the “incident_date” fields in two of the claims:
2
3• CLM-2024-001923 (Home – Storm Damage)
4– incident_date: 2024-07-22
5– report_text: “Severe thunderstorm … struck the property on July 24th, 2024”
6
7• CLM-2024-002789 (Auto – Hit and Run)
8– incident_date: 2024-08-28
9– report_text: “While stopped … at 5:45 PM on August 30th, 2024”
10
11In both cases the text describes the event happening on a different date than what’s recorded in the incident_date field (and, in the second, the police-report ID suggests August 28th, not the 30th mentioned). You may want to correct those date mismatches.
12
While my prompt was intentionally vague, this shows how reasoning models naturally become more analytical and thorough. If I asked the models explicitly to search for inconsistencies, also GPT-4o
would find them.
Reasoning API
#Be careful when enabling reasoning, as not all (mostly older) models support it. Additionally, this is one case where UC AI cannot unify everything, as providers have different types of settings. For Ollama, you can only enable or disable reasoning. With Anthropic and Google, you can fine-tune how many tokens will be used for reasoning, while OpenAI has settings of low
, medium
, and high
. Please read the reasoning documentation for more details and best practices.
1uc_ai.g_enable_reasoning := true;
2uc_ai_openai.g_reasoning_effort := 'medium';
3uc_ai_anthropic.g_reasoning_budget_tokens := 512;
4uc_ai_google.g_reasoning_budget := 512;
5
6l_result := uc_ai.generate_text(
7 p_user_prompt => :P30_USER_PROMPT,
8 p_system_prompt => :P30_SYSTEM_PROMPT,
9 p_provider => :P0_PROVIDER,
10 p_model => :P0_MODEL
11);
12
Tools and Function Calling: Allowing AI to Execute Actions
#Tools (also called “function calling”) represents the most powerful feature of UC AI, allowing AI models to execute PL/SQL functions directly.
The Concept: Giving AI Capabilities
#Instead of just generating text, AI can interact with the database thanks to tools. This is useful for two types of capabilities:
- Conditional context (SQL): Don’t add too much data to the system prompt; let AI run queries on the fly when needed based on the user prompt.
- Actions (insert, update, delete, invoke APIs, jobs, etc.): Let the AI execute business logic from a prompt
How It Works
#A simple example for tools is giving it access to a weather API. As models don’t have real-time data, this tool can be used to let the LLM use current weather information.
This is how it looks from the perspective of the AI:
- The user registers tools with defined parameters and descriptions (e.g., getWeather returns current weather info for a given city)
- Based on the user prompt, the AI decides if it makes sense to request tool execution (“Tell me a joke” → no, “Do I need a raincoat in Rome?” → yes)
- Instead of returning an answer, it returns a tool invocation request. It expects us to run the tool and return its output. (“The weather in Rome is sunny, 26°C.“)
- The AI evaluates the result, can call another tool, or decides to answer (“No, you don’t need a raincoat as it is warm and sunny currently”).
The important part here is that the AI does not execute the function itself. It requests its execution. The UC AI PL/SQL packages will then automatically run the tool and return the outcome.
UC AI knows what to do, as you have to register a tool with UC AI with the PL/SQL function name that you created.
Safety and Control
#Unlike Oracle’s SQLcl MCP server approach (which allows arbitrary SQL execution), UC AI tools:
- Only execute predefined PL/SQL functions
- Require explicit function registration
- Allow parameter validation
- Maintain full developer control
Real-World Example: Insurance Claim Processing
#So now we can actually automate our insurance claim entry further. When a new mail arrives, we ask AI to create a record about it in our database.
Tool Registration
#To register a tool, we have to create a JSON schema for the parameters if there are any. Then we call uc_ai_tools_api.create_tool_from_schema
with a tool code, description, PL/SQL function call, the schema, and tags. Notice how we use :parameters
in the tool call? UC AI will replace that with the JSON arguments given from the LLM. It makes sense to carefully describe the parameters and the tool itself, as this is valuable context for the AI.
1declare
2 l_schema json_object_t;
3 l_tool_id uc_ai_tools.id%type;
4begin
5 l_schema := json_object_t.parse('{
6 "$schema": "http://json-schema.org/draft-07/schema#",
7 "type": "object",
8 "title": "Create Settlement Request",
9 "description": "Create a new settlement in the database",
10 "properties": {
11 "incident_date": {
12 "type": "string",
13 "description": "Date of the incident in YYYY-MM-DD format"
14 },
15 "claimant_first_name": {
16 "type": "string",
17 "description": "First name of the person making the claim"
18 },
19 },
20 {...}
21 "required": [
22 "incident_date",
23 "claimant_first_name",
24 "claimant_last_name"
25 ]
26 }');
27
28 -- Create the tool
29 l_tool_id := uc_ai_tools_api.create_tool_from_schema(
30 p_tool_code => 'PAME_CREATE_SETTLEMENT',
31 p_description => 'Create a new insurance settlement from initial provided data',
32 p_function_call => 'return pame_pkg.create_new_settlement(:parameters);',
33 p_json_schema => l_schema,
34 p_tags => apex_t_varchar2('pame', 'pame_create_settlement')
35 );
36
37 commit;
38end;
39/
40
This is the underlying function pame_pkg.create_new_settlement
. We take one parameter and return one, both in CLOB
. Then we have to check if the JSON is in the structure we expect it to be in. Instead of just raising exceptions, we actually provide error information back. That gives the AI context to fix the input in the next try. If the insert succeeds, we will return a success message.
Get user info function
function create_new_settlement(p_settlement_data in clob) return clob as l_settlement_data json_object_t; l_settlement_id number; l_claim_number varchar2(50 char); l_policy_number varchar2(50 char); l_policy_type varchar2(100 char); l_incident_date date; l_incident_desc varchar2(500 char); l_claimant_fname varchar2(100 char); l_claimant_lname varchar2(100 char); l_claimant_email varchar2(255 char); l_claimant_phone varchar2(20 char); l_insured_fname varchar2(100 char); l_insured_lname varchar2(100 char); l_settlement_amount number(18, 2); l_currency_code varchar2(3 char); l_notes varchar2(1000 char); l_result clob; begin BEGIN l_settlement_data := json_object_t.parse(p_settlement_data); EXCEPTION when others then return '{"status": "error", "message": "Invalid JSON input: ' || sqlerrm || '"}'; END; -- Validate required fields and extract from JSON if not l_settlement_data.has('incident_date') then return '{"status": "error", "message": "Missing required field: incident_date"}'; end if; if not l_settlement_data.has('claimant_first_name') then return '{"status": "error", "message": "Missing required field: claimant_first_name"}'; end if; if not l_settlement_data.has('claimant_last_name') then return '{"status": "error", "message": "Missing required field: claimant_last_name"}'; end if; -- Extract required fields -- generate pattern like this: CL2025-009-ACC l_claim_number := 'CL' || to_char(sysdate, 'YYYY') || '-' || round(sys.dbms_random.value(100, 999999)) || '-AI'; l_policy_number := 'POL' || to_char(sysdate, 'YYYY') || '-' || round(sys.dbms_random.value(100, 999999)) || '-AI'; l_incident_date := to_date(l_settlement_data.get_String('incident_date'), 'FXYYYY-MM-DD'); l_claimant_fname := l_settlement_data.get_String('claimant_first_name'); l_claimant_lname := l_settlement_data.get_String('claimant_last_name'); -- Extract optional fields l_policy_type := case when l_settlement_data.has('policy_type') then l_settlement_data.get_String('policy_type') else null end; l_incident_desc := case when l_settlement_data.has('incident_description') then l_settlement_data.get_String('incident_description') else null end; l_claimant_email := case when l_settlement_data.has('claimant_email') then l_settlement_data.get_String('claimant_email') else null end; l_claimant_phone := case when l_settlement_data.has('claimant_phone') then l_settlement_data.get_String('claimant_phone') else null end; l_insured_fname := case when l_settlement_data.has('insured_first_name') then l_settlement_data.get_String('insured_first_name') else null end; l_insured_lname := case when l_settlement_data.has('insured_last_name') then l_settlement_data.get_String('insured_last_name') else null end; l_settlement_amount := case when l_settlement_data.has('settlement_amount') then l_settlement_data.get_Number('settlement_amount') else 0 end; l_currency_code := case when l_settlement_data.has('currency_code') then l_settlement_data.get_String('currency_code') else 'EUR' end; l_notes := case when l_settlement_data.has('notes') then l_settlement_data.get_String('notes') else null end; -- Generate new settlement ID select nvl(max(settlement_id), 0) + 1 into l_settlement_id from pame_settlement_demo; -- Insert new settlement record insert into pame_settlement_demo ( settlement_id, claim_number, policy_number, policy_type, incident_date, incident_description, claimant_first_name, claimant_last_name, claimant_email, claimant_phone, insured_first_name, insured_last_name, settlement_date, settlement_amount, currency_code, settlement_status, notes ) values ( l_settlement_id, l_claim_number, l_policy_number, l_policy_type, l_incident_date, l_incident_desc, l_claimant_fname, l_claimant_lname, l_claimant_email, l_claimant_phone, l_insured_fname, l_insured_lname, sysdate, -- settlement_date defaults to current date l_settlement_amount, l_currency_code, 'Proposed', -- initial status is always 'Proposed' l_notes ); l_result := '{"status": "success", "message": "Settlement created successfully", "settlement_id": ' || l_settlement_id || ', "claim_number": "' || l_claim_number || '"}'; return l_result; exception when dup_val_on_index then return '{"status": "error", "message": "Claim number already exists: ' || l_claim_number || '"}'; when others then return '{"status": "error", "message": "Database error: ' || sqlerrm || '", "backtrace": "' || sys.dbms_utility.format_error_backtrace || '"}'; end create_new_settlement;
Our insert does require having information about the first and last name. As customers might forget to write that in the email, we add another tool that can give user info for a provided email:
1declare
2 l_schema json_object_t;
3 l_tool_id uc_ai_tools.id%type;
4begin
5 l_schema := json_object_t.parse('{
6 "$schema": "http://json-schema.org/draft-07/schema#",
7 "type": "object",
8 "title": "Get User Info",
9 "description": "Retrieve user information (name, phone, etc.) by email address (insensitive). The parameter email is required.",
10 "properties": {
11 "email": {
12 "type": "string",
13 "description": "Email address of the user to lookup"
14 }
15 },
16 "required": [
17 "email"
18 ]
19 }');
20
21 -- Create the tool
22 l_tool_id := uc_ai_tools_api.create_tool_from_schema(
23 p_tool_code => 'PAME_GET_USER_INFO',
24 p_description => 'Get user information (name, phone, etc.) by email address (insensitive)',
25 p_function_call => 'return pame_pkg.get_user_info(:parameters);',
26 p_json_schema => l_schema,
27 p_tags => apex_t_varchar2('pame', 'pame_get_user_info')
28 );
29
30 commit;
31end;
32/
33
34
The underlying function is a bit simpler; we have fewer inputs to check and just construct a JSON of customer data to return.
Create new settlement function
function get_user_info(p_email_data in clob) return clob as l_email_data json_object_t; l_email varchar2(255 char); l_user_record pame_users%rowtype; l_result clob; begin BEGIN l_email_data := json_object_t.parse(p_email_data); EXCEPTION when others then return '{"status": "error", "message": "Invalid JSON input: ' || sqlerrm || '", "backtrace": "' || sys.dbms_utility.format_error_backtrace || '"}'; END; -- Validate required field if not l_email_data.has('email') then return '{"status": "error", "message": "Missing required field: email"}'; end if; -- Extract email from JSON l_email := l_email_data.get_String('email'); -- Validate email format (basic check) if l_email is null or length(trim(l_email)) = 0 then return '{"status": "error", "message": "Email cannot be empty"}'; end if; if instr(l_email, '@') = 0 then return '{"status": "error", "message": "Invalid email format"}'; end if; -- Query user by email begin select user_id, first_name, last_name, email, phone, created_at, updated_at into l_user_record.user_id, l_user_record.first_name, l_user_record.last_name, l_user_record.email, l_user_record.phone, l_user_record.created_at, l_user_record.updated_at from pame_users where lower(email) = lower(l_email); -- Build success response with user data l_result := '{"status": "success", "user": {' || '"user_id": "' || l_user_record.user_id || '",' || '"first_name": "' || l_user_record.first_name || '",' || '"last_name": "' || l_user_record.last_name || '",' || '"email": "' || l_user_record.email || '",' || '"phone": "' || nvl(l_user_record.phone, 'null') || '",' || '"created_at": "' || to_char(l_user_record.created_at, 'YYYY-MM-DD"T"HH24:MI:SS') || '",' || '"updated_at": "' || to_char(l_user_record.updated_at, 'YYYY-MM-DD"T"HH24:MI:SS') || '"' || '}}'; exception when no_data_found then l_result := '{"status": "error", "message": "No user found with email: ' || l_email || '"}'; when too_many_rows then l_result := '{"status": "error", "message": "Multiple users found with email: ' || l_email || ' (data integrity issue)"}'; when others then l_result := '{"status": "error", "message": "Database error in user lookup: ' || sqlerrm || '", "backtrace": "' || sys.dbms_utility.format_error_backtrace || '"}'; end; return l_result; exception when others then return '{"status": "error", "message": "Database error: ' || sqlerrm || '", "backtrace": "' || sys.dbms_utility.format_error_backtrace || '"}'; end get_user_info;
So now, to test it, we will take this email that deliberately misses any customer name.
1Subject: Car accident
2Sender: apex_fan@example.com
3
4I hope this email finds you well. I’m writing to provide details regarding the settlement for the bicycle accident I was involved in.
5
6The accident occurred on 2025-08-22. I was cycling home when a pedestrian unexpectedly stepped into the bike lane, causing me to swerve and fall. Thankfully, I only sustained minor scrapes and bruises, but there was damage to the front wheel and handlebars of my bicycle.
7
8I’d like to proceed with the settlement process. The estimated repair costs came to around €350, which I believe should be covered under my policy. Please let me know if you need any additional information from my end.
9
10Thank you for your assistance.
11
When I call it with GPT-4o
with just the system prompt “Please create a settlement from the provided user input,” it is interesting to see what is happening. The following JSON shows the conversation messages, which are also provided in the generate_text
method in the messages
key.
The first thing that happens is that the tool immediately tries to call both tools without any input. For some reason OpenAI sometimes ignores that we defined the arguments as required. For both, it gets back error messages about the missing parameters. Then it again tries to call the tools, now correctly passing the email for the customer info tool. With that information, it correctly calls the create incident tool and thus creates a record in the database.
GPT-4o message chain with tool calls
[ { "role": "system", "content": "Please create a settlement from the provided user input." }, { "role": "user", "content": [ { "type": "text", "text": "Subject: Car accident\r\nSender: apex_fan@example.com\r\n\r\nI hope this email finds you well. I’m writing to provide details regarding the settlement for the bicycle accident I was involved in.\r\n\r\nThe accident occurred on 2025-08-22. I was cycling home when a pedestrian unexpectedly stepped into the bike lane, causing me to swerve and fall. Thankfully, I only sustained minor scrapes and bruises, but there was damage to the front wheel and handlebars of my bicycle.\r\n\r\nI’d like to proceed with the settlement process. The estimated repair costs came to around €350, which I believe should be covered under my policy. Please let me know if you need any additional information from my end.\r\n\r\nThank you for your assistance." } ] }, { "role": "assistant", "content": [ { "type": "tool_call", "toolCallId": "call_ry3uRa8PaFdq3xjLKmjWtzPF", "toolName": "PAME_GET_USER_INFO", "args": "{}" }, { "type": "tool_call", "toolCallId": "call_x6zibhtANPRw5s5OQLbZK1Sg", "toolName": "PAME_CREATE_SETTLEMENT", "args": "{}" } ] }, { "role": "tool", "content": [ { "type": "tool_result", "toolCallId": "call_ry3uRa8PaFdq3xjLKmjWtzPF", "toolName": "PAME_GET_USER_INFO", "result": "{\"status\": \"error\", \"message\": \"Missing required field: email\"}" }, { "type": "tool_result", "toolCallId": "call_x6zibhtANPRw5s5OQLbZK1Sg", "toolName": "PAME_CREATE_SETTLEMENT", "result": "{\"status\": \"error\", \"message\": \"Missing required field: incident_date\"}" } ] }, { "role": "assistant", "content": [ { "type": "tool_call", "toolCallId": "call_p4VnV3jzeUsZeqOdX6J6W4mC", "toolName": "PAME_GET_USER_INFO", "args": "{\"email\": \"apex_fan@example.com\"}" }, { "type": "tool_call", "toolCallId": "call_q2rHAEDiMrffuDylnEyGZTVc", "toolName": "PAME_CREATE_SETTLEMENT", "args": "{\"incident_date\": \"2025-08-22\"}" } ] }, { "role": "tool", "content": [ { "type": "tool_result", "toolCallId": "call_p4VnV3jzeUsZeqOdX6J6W4mC", "toolName": "PAME_GET_USER_INFO", "result": "{\"status\": \"success\", \"user\": {\"user_id\": \"user2\",\"first_name\": \"Bob\",\"last_name\": \"Johnson\",\"email\": \"apex_fan@example.com\",\"phone\": \"234-567-8901\",\"created_at\": \"2025-09-05T15:47:33\",\"updated_at\": \"2025-09-05T15:47:33\"}}" }, { "type": "tool_result", "toolCallId": "call_q2rHAEDiMrffuDylnEyGZTVc", "toolName": "PAME_CREATE_SETTLEMENT", "result": "{\"status\": \"error\", \"message\": \"Missing required field: claimant_first_name\"}" } ] }, { "role": "assistant", "content": [ { "type": "tool_call", "toolCallId": "call_8kV8NV6mHCktYgDhIKOZoEgT", "toolName": "PAME_CREATE_SETTLEMENT", "args": "{\"claimant_first_name\":\"Bob\",\"claimant_last_name\":\"Johnson\",\"incident_date\":\"2025-08-22\"}" } ] }, { "role": "tool", "content": [ { "type": "tool_result", "toolCallId": "call_8kV8NV6mHCktYgDhIKOZoEgT", "toolName": "PAME_CREATE_SETTLEMENT", "result": "{\"status\": \"success\", \"message\": \"Settlement created successfully\", \"settlement_id\": 112, \"claim_number\": \"CL2025-714988-AI\"}" } ] }, { "role": "assistant", "content": [ { "type": "text", "text": "The settlement for the bicycle accident on 2025-08-22 has been created successfully. Here are the details:\n\n- **Claimant Name**: Bob Johnson\n- **Claim Number**: CL2025-714988-AI\n- **Settlement ID**: 112\n\nPlease let me know if there is anything else you need assistance with, or if there are any further details required for the process.", "providerOptions": { "refusal": null, "annotations": [] } } ] } ]
But notice that it actually just fills the required fields. Even though the input had more details, it does not fill them in the parameters. We could fine-tune it by providing a better prompt to tell it to plan steps more carefully and to make sure to give as much possible information. Also, using a reasoning model would help the performance.
More modern models like GPT-5
do a better job out of the box without improvements, but interestingly, Anthropic’s smallest and last-generation model, Haiku 3.5
even beats GPT-5 with always using the most straightforward plan of first getting the user info and then doing the insert.
Haiku 3.5 message chain with tool calls
[ { "role": "system", "content": "Please create a settlement from the provided user input." }, { "role": "user", "content": [ { "type": "text", "text": "Subject: Car accident\r\nSender: apex_fan@example.com\r\n\r\nI hope this email finds you well. I’m writing to provide details regarding the settlement for the bicycle accident I was involved in.\r\n\r\nThe accident occurred on 2025-08-22. I was cycling home when a pedestrian unexpectedly stepped into the bike lane, causing me to swerve and fall. Thankfully, I only sustained minor scrapes and bruises, but there was damage to the front wheel and handlebars of my bicycle.\r\n\r\nI’d like to proceed with the settlement process. The estimated repair costs came to around €350, which I believe should be covered under my policy. Please let me know if you need any additional information from my end.\r\n\r\nThank you for your assistance." } ] }, { "role": "assistant", "content": [ { "type": "text", "text": "I'll help you create a settlement for this bicycle accident. First, I'll retrieve the user's information using their email address, and then I'll create the settlement.", "providerOptions": {} }, { "type": "tool_call", "toolCallId": "toolu_017mN3JWFaGBYuWZmJgyLSK2", "toolName": "PAME_GET_USER_INFO", "args": "{\"email\":\"apex_fan@example.com\"}" } ] }, { "role": "tool", "content": [ { "type": "tool_result", "toolCallId": "toolu_017mN3JWFaGBYuWZmJgyLSK2", "toolName": "PAME_GET_USER_INFO", "result": "{\"status\": \"success\", \"user\": {\"user_id\": \"user2\",\"first_name\": \"Bob\",\"last_name\": \"Johnson\",\"email\": \"apex_fan@example.com\",\"phone\": \"234-567-8901\",\"created_at\": \"2025-09-05T15:47:33\",\"updated_at\": \"2025-09-05T15:47:33\"}}" } ] }, { "role": "assistant", "content": [ { "type": "text", "text": "Now, I'll create the settlement using the retrieved information and the details from the email:", "providerOptions": {} }, { "type": "tool_call", "toolCallId": "toolu_01LK98bf8BG97NVWzP29KLZD", "toolName": "PAME_CREATE_SETTLEMENT", "args": "{\"claimant_first_name\":\"Bob\",\"claimant_last_name\":\"Johnson\",\"claimant_email\":\"apex_fan@example.com\",\"claimant_phone\":\"234-567-8901\",\"incident_date\":\"2025-08-22\",\"incident_description\":\"Cyclist swerved and fell after a pedestrian unexpectedly stepped into the bike lane, resulting in damage to bicycle front wheel and handlebars\",\"policy_type\":\"Personal Liability\",\"settlement_amount\":350,\"currency_code\":\"EUR\"}" } ] }, { "role": "tool", "content": [ { "type": "tool_result", "toolCallId": "toolu_01LK98bf8BG97NVWzP29KLZD", "toolName": "PAME_CREATE_SETTLEMENT", "result": "{\"status\": \"success\", \"message\": \"Settlement created successfully\", \"settlement_id\": 113, \"claim_number\": \"CL2025-356222-AI\"}" } ] }, { "role": "assistant", "content": [ { "type": "text", "text": "I've processed the settlement claim with the following details:\n- Claimant: Bob Johnson\n- Email: apex_fan@example.com\n- Phone: 234-567-8901\n- Incident Date: 2025-08-22\n- Incident Description: Cyclist swerved and fell after a pedestrian unexpectedly stepped into the bike lane, resulting in damage to bicycle front wheel and handlebars\n- Policy Type: Personal Liability\n- Settlement Amount: €350\n- Claim Number: CL2025-356222-AI\n\nThe settlement has been created successfully in the system. The claimant can expect further communication regarding the claim processing and reimbursement.", "providerOptions": {} } ] } ]
Successful AI in Oracle
#Just using AI is not the key to success.
Current State and Challenges
#Despite the AI hype, MIT research shows that 95% of generative AI pilots at companies are failing. The primary reasons:
- Lack of Clear Use Cases: Don’t build flashy demos (like I do here); solve real issues
- Poor Integration: Focusing on customer-facing features instead of back-office automation
- Lack of transformation: Projects face resistance and don’t do required workflow re-designs
Crucially, the insight that back-office automation offers the biggest impact for us data engineers. It is now in our hands to build the automations that drive your business into the age of AI. And I think with the principles laid out here, you are in a great spot to get started with building on your already existing SQL and PL/SQL skills and now only sprinkling a layer of LLM knowledge on top.
Automations
#So use the shown principles to build workflows that autonomously help manage your data. For that you can use
- DBMS_SCHEDULER for automated jobs
- Flows for APEX or APEX Workflows for visual AI process design
But also keep an eye out for what is coming next.
UC AI Roadmap
#Planned enhancements include:
- Agentic AI for autonomous multi-step workflows
- Speech-to-text and text-to-speech capabilities
- Image generation integration
- Vector embeddings support (requires 23ai)
- Streaming responses for real-time applications
- APEX plugins for drag-and-drop AI integration for your apps
I think the next step in this chain of learning data engineering with AI is agentic AI. I am currently deep into researching how I will implement it in UC AI. Agents allow you to do much more complicated kinds of tasks (like coding agents or deep research) but also require much better prompting and fine-tuning skills.
Getting Started with UC AI
#If you also want to start using UC AI, follow the installation guide.
Remember it is free and open-source software. This project is driven heavily by my motivation. If you use it successfully, please consider sharing your stories with me or at least giving us a star on GitHub. I am also super happy if anybody is willing to contribute to the project. Even improving the documentation, reporting bugs, or new ideas helps keep momentum in the project.
UC AI is possible because I have time to build it at United Codes. If you want to support this project in other ways, please consider using our products like AOP or doing consulting with us. We can also help you with your AI strategy.
Conclusion
#The AI hype isn’t just about chatbots and customer-facing interfaces. It’s about transforming how we process and manage data. As data engineers working with Oracle, you’re uniquely positioned to capitalize on this opportunity.
Throughout this post, we’ve explored how you can leverage AI from your existing PL/SQL skills. From generating structured output and analyzing files to implementing reasoning and tool calling, these techniques represent practical automation that delivers immediate business value.
The key insights to remember:
- Focus on back-office automation where AI excels and faces less resistance
- Build on your existing skills - if you know SQL and PL/SQL, you’re already 90% there
- Start with clear use cases - solve real problems, not impressive demos
- Iterate and improve - AI is a tool that gets better with thoughtful implementation
The UC AI SDK gives you the foundation to start experimenting today. Whether you’re automating document processing, enabling intelligent data analysis, or building sophisticated workflows, the tools are available and the opportunity is now.
Start small, think big, and never stop learning.