Build Real AI Solutions with Oracle PL/SQL (No 23ai Required)

Learn how data engineers can leverage AI for Oracle automation using PL/SQL. Discover structured output, file analysis, reasoning, and tool calling with the open-source UC AI SDK.

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:

  1. Data Analysis: Summarize, classify, categorize, cluster, and detect anomalies (faster than humans, sometimes with hallucinations!)
  2. Unstructured to Structured Data: Transform blobs of text or documents into relational data
  3. 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.

  1. OpenAI (GPT models)
  2. Anthropic (Claude models)
  3. Google (Gemini models)
  4. Ollama (local/offline models: Meta Llama, Google Gemma, Alibaba Qwen, DeepSeek, GPT-OSS)
  5. 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, 20246
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, 202410
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:

  1. The user registers tools with defined parameters and descriptions (e.g., getWeather returns current weather info for a given city)
  2. 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)
  3. 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.“)
  4. 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:

  1. Lack of Clear Use Cases: Don’t build flashy demos (like I do here); solve real issues
  2. Poor Integration: Focusing on customer-facing features instead of back-office automation
  3. 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

  1. DBMS_SCHEDULER for automated jobs
  2. 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.