Skip to content

adm_ai_form_extract_api

Form extraction: reads the data of a form (an invoice, a delivery note, …) out of a document with a language model, checks it, and publishes it as annotations of the document. The configuration (profiles, document types, forms) is managed with adm_ai_form_config_api.

The pipeline of one document:

  1. An image (image/*) goes to the vision_model of the profile, which picks the form and reads the fields and the line items. Without a vision_model the document goes to review. An image whose type is in ocr_text_extraction.apply_to.mime_types of the profile is read with OCR instead and goes on as text.
  2. Every other document is read as text (see document_text): the OCR text that ADM stored for the version, or else the text of the built-in filter, or else, when the filter finds too little, the ocr_text_extraction of the profile. Text with fewer than min_text_chars characters that are not blank (a scan without a text layer) goes to review without a model call.
  3. The markers of the active forms of the profile decide the form (see marker_text). No form, or more than one, goes to review; for no form the text_model suggests one as a hint for the reviewer. The model never picks the form of a text document.
  4. Text longer than max_text_chars goes to review: a model that cuts the prompt does so without an error.
  5. The model_input of the form decides what the model reads. text (the default): the text_model reads the fields from the text. A form with a line_pattern reads its line items with the pattern, without the model, from the text of the filter; from OCR text the model reads them. When the document type has no field besides the line items, such a form needs no model call for filter text. file: the vision_model reads the fields and the line items from the file of the document (method FILE), which keeps the layout of tables and check boxes that the text loses. The form is still found by the markers on the text, and the text still has to fit max_text_chars. Without a vision_model, for a file that is no PDF and no image (image/*), and for a file larger than max_file_bytes of the profile, the document goes to review without a model call. A file read takes longer and costs more tokens than a text read; the HTTP timeout of uc_ai (180 seconds) applies to it.
  6. adm_ai_form_config_api.check_result checks the result. Every check passes: status OK. Otherwise REVIEW, with one line for each issue.

extract runs the pipeline and writes nothing. extract_document writes the result to adm_ai_form_extractions and publishes it: an OK or APPROVED result sets the annotations <doc_type>.<field> for the annotation_fields of the document type and removes the review_tag of the profile; any other result removes those annotations and adds the review_tag. approve lets a person correct and approve a result. measure runs the pipeline over the test set in adm_ai_form_expected.

An OK or APPROVED result runs the AFTER_FORM_EXTRACTION hook (see adm_hooks_api) in the same transaction. When the hook raises, extract_document takes its result back and writes it as FAILED, with the message of the hook as the issue: a refusal from the range -20700 .. -20999 as the hook wrote it, any other error as a sentence. approve takes the approval back and raises the error of the hook.

A profile with process_new_documents true is processed without a call: the AI queue job (adm_ai_job_automations_api.process_form_extractions) extracts the documents of documents_to_process one by one and commits after each. process_new_documents does the same work in the transaction of its caller. A document whose extraction raises is rolled back and recorded as FAILED (record_failure), so every failure counts against its attempt budget. The automatic processing works with the OCR fallback (adm_ai_ocr_fallback_api): it waits for a scan that the fallback will read, unless the OCR of the profile reads it, and it extracts a scan again, once, when OCR text arrives after a result without text.

The model calls go through uc_ai.generate_text with the provider, model and config of the text_model or vision_model section of the profile. config passes through unchanged, so options such as the temperature or the context size of an Ollama model belong there:

"text_model": {"provider": "ollama", "model": "qwen3.5:9b",
"config": {"g_base_url": "https://ollama.example.com/api",
"g_extra_body": {"options": {"temperature": 0, "num_ctx": 16384}},
"ollama": {"g_use_responses_api": false}}}

No procedure commits. One call of extract_document is one unit of work.

Text of the latest version of a document, the same text that the extraction reads. Write the line pattern of a form against this text, and the markers against marker_text of it. Needs the view right on the document.

The text is the first of these:

  1. The OCR text that ADM stored on the version (index_content_source OCR, from the OCR fallback, from the ocr_text_extraction of a RAG collection or of a profile), when it has at least min_text_chars characters that are not blank.
  2. The text of the built-in filter, which reads the file again. It is left out for an image that the OCR of the profile reads.
  3. Only with p_allow_ocr true: when the filter text is shorter than min_text_chars and the ocr_text_extraction of the profile applies to the document (its apply_to, and the setting AI_TEXT_EXTRACTION is Y), the text of that OCR. The call sends the file to the OCR provider and stores the text on the version, as the extraction does. Without p_allow_ocr the call never sends the file anywhere, and returns the short filter text.

Text from llm_text_extraction is never used: its prompt can change the text, and the default prompt leaves out page headers and footers, where markers often are.

Signature:

function document_text (
p_document_id in number,
p_profile_id in number default null,
p_allow_ocr in boolean default false
) return clob;

Parameters:

Name Direction Type Description
p_document_id in number The document
p_profile_id in number default null The profile whose min_text_chars and ocr_text_extraction apply;
null takes the default min_text_chars
(adm_ai_form_config_api.c_default_min_text_chars) and no OCR |

| p_allow_ocr | in | boolean default false | True lets the call read the document with the OCR of the profile |

Returns: clob - The text


The text that the markers of a form are matched against. Blanks at the end of each line are removed. OCR text (p_text_source c_text_source_ocr) is Markdown, and loses its Markdown first, so that the same marker matches the text of the filter and the OCR text of the same form:

  • <br> becomes a line break, and the separator row of a table (|---|---|) goes
  • every other | becomes a line break, without the blanks around it, so each table cell is a line of its own, as the filter writes it
  • the # marks of a heading, ** and __, a single * or _ around text, and the blanks at the start of a line go

The model reads the OCR text as it is, with its Markdown.

Signature:

function marker_text (
p_text in clob,
p_text_source in varchar2 default c_text_source_filter
) return clob;

Parameters:

Name Direction Type Description
p_text in clob The text of a document
p_text_source in varchar2 default c_text_source_filter c_text_source_filter or c_text_source_ocr

Returns: clob - The text for the markers


Whether a document is in the scope of a profile: in or below its source_folder_path, or selected by its source_file_annotation_key - the same selection as for a RAG collection (adm_ai_source_documents_mcr).

Signature:

function document_in_scope (
p_profile_id in number,
p_document_id in number
) return boolean;

Parameters:

Name Direction Type Description
p_profile_id in number The profile
p_document_id in number The document

Returns: boolean - True when the profile extracts the document


The active forms of a profile whose markers all match a text. Each marker is a regular expression applied with the match parameter m, so ^ and $ match at the start and the end of each line. The markers see marker_text of the text.

Signature:

function matching_forms (
p_profile_id in number,
p_text in clob,
p_text_source in varchar2 default c_text_source_filter
) return apex_t_varchar2;

Parameters:

Name Direction Type Description
p_profile_id in number The profile
p_text in clob The text of a document
p_text_source in varchar2 default c_text_source_filter c_text_source_filter or c_text_source_ocr, see marker_text

Returns: apex_t_varchar2 - The form codes, sorted


The system prompt of a form: its description, then the rules of the profile, of the document type and of the form, in this order.

Signature:

function system_prompt (
p_form_id in number,
p_variant in varchar2 default c_prompt_rules
) return clob;

Parameters:

Name Direction Type Description
p_form_id in number The form
p_variant in varchar2 default c_prompt_rules c_prompt_rules, or c_prompt_naive to leave the rules out

Returns: clob - The system prompt


The JSON schema of the answer for a form: the schema of its document type, without the line items when code reads them.

Signature:

function answer_schema (
p_form_id in number,
p_with_line_items in boolean
) return json_object_t;

Parameters:

Name Direction Type Description
p_form_id in number The form
p_with_line_items in boolean False removes line_items from the properties and from required

Returns: json_object_t - The schema


Reads the line items from a text with the line_pattern of a form, without a model. The type of each column comes from the schema of the document type: a number column is converted with the decimal separator of the form (decimal_comma), a value that is no number becomes null, which the checks then report. Every other column stays text.

Signature:

function read_lines (
p_form_id in number,
p_text in clob
) return json_array_t;

Parameters:

Name Direction Type Description
p_form_id in number The form
p_text in clob The text of a document

Returns: json_array_t - The line items, or null when the form has no line_pattern


Extracts one document and returns the result. It writes nothing but the OCR text that the ocr_text_extraction of the profile reads, which it stores on the version (see document_text). Needs the view right on the document. The result:

{"form_id": 3, "form_code": "HARBOUR_INVOICE", "status": "OK", "method": "TEXT",
"text_source": "FILTER", "fields": {...}, "line_items": [...], "issues": null,
"model": "qwen3.5:9b", "prompt_tokens": 2310, "completion_tokens": 190, "seconds": 6.2}

status is c_status_ok, c_status_review or c_status_failed. method is c_method_text, c_method_image, c_method_file or c_method_none. text_source is c_text_source_filter or c_text_source_ocr (see document_text): the text that the markers ran on, which for c_method_file is not the input of the model. It is missing for an image that the vision model reads. A failed OCR call is a failure. A failure is part of the result, not an error: its issues hold a message for the reviewer, the details are in the debug log.

The text or the file of the document goes to a language model. For a user that writes one AI_READ_DOCUMENT row to the audit trail, for the document and the request, with the provider and the model in the details (see adm_audit_api.audit_read).

Signature:

function extract (
p_profile_id in number,
p_document_id in number,
p_model in varchar2 default null,
p_vision_model in varchar2 default null,
p_prompt in varchar2 default c_prompt_rules,
p_think in boolean default null
) return json_object_t;

Parameters:

Name Direction Type Description
p_profile_id in number The profile
p_document_id in number The document
p_model in varchar2 default null Another model for the text_model section, for a measurement
p_vision_model in varchar2 default null Another model for the vision_model section, for an image and for a
form with model_input file; for an image without a vision_model
section it takes the provider and config of text_model |

| p_prompt | in | varchar2 default c_prompt_rules | c_prompt_rules or c_prompt_naive, see system_prompt | | p_think | in | boolean default null | True or false sets g_enable_reasoning of the model config; null keeps the config of the profile |

Returns: json_object_t - The result


Extracts one document, writes the result to adm_ai_form_extractions (one row for each profile and document, for the latest version) and publishes it as annotations and the review tag. An OK result runs the AFTER_FORM_EXTRACTION hook; a hook that raises makes the result FAILED. Needs the edit right on the document, and the document must be in the scope of the profile. Does not commit.

A call extracts at once and does not wait for the OCR fallback: a scan that the fallback has not read yet gets the method NONE and goes to review, unless the ocr_text_extraction of the profile reads it in this call. Call it again when the OCR text is stored, or let the automatic processing do it (see documents_to_process).

The text or the file of the document goes to a language model, and with ocr_text_extraction to the OCR provider. Each time that happens for a user, one AI_READ_DOCUMENT row is written to the audit trail, for the document and the request, with the provider and the model in the details (see adm_audit_api.audit_read). The automatic processing runs as the system user and writes none.

Signature:

procedure extract_document (
p_profile_id in number,
p_document_id in number
);

Parameters:

Name Direction Type Description
p_profile_id in number The profile
p_document_id in number The document

The documents that the automatic processing of a profile extracts next: the documents in the scope of the profile that have no result for their latest version, or whose result for it is FAILED fewer than AI_JOB_MAX_ATTEMPTS times in a row. A new version starts again. The oldest latest version first, at most max_documents_per_run of the profile. Empty when the profile does not have process_new_documents true. Needs an administrator.

Two rules work with the OCR fallback (adm_ai_ocr_fallback_api):

  • A document that is no image and whose latest version waits for the fallback (adm_ai_ocr_fallback_api.waiting_versions) is left out, and gets no result. It comes back when the fallback stored its text, or when the fallback gave up after AI_JOB_MAX_ATTEMPTS failures. A document that the ocr_text_extraction of the profile reads does not wait: the extraction reads it with that OCR.
  • A REVIEW result with the method NONE that did not read OCR text is extracted again when its version now has OCR text with at least min_text_chars characters that are not blank. The new result reads that text, so this happens once for each version.

Signature:

function documents_to_process (
p_profile_id in number
) return apex_t_number;

Parameters:

Name Direction Type Description
p_profile_id in number The profile

Returns: apex_t_number - The document ids


Records a FAILED extraction of the latest version of a document, without values, after extract_document raised and its work was rolled back. The issue is a sentence for the reviewer, never the error. failed_attempts counts it like every other failure, so the document leaves documents_to_process after AI_JOB_MAX_ATTEMPTS failures. Then it adds the review tag; when that fails too, the row stays and the error is only logged. Needs an administrator.

Signature:

procedure record_failure (
p_profile_id in number,
p_document_id in number
);

Parameters:

Name Direction Type Description
p_profile_id in number The profile
p_document_id in number The document

Extracts the documents of documents_to_process with extract_document, for one profile or for every profile with process_new_documents true. Everything happens in the transaction of the caller. When extract_document raises for a document, its work is rolled back to a savepoint, record_failure writes it as FAILED, and the call goes on with the next document; an error of record_failure stops the call. The AI queue job does the same work with a commit after each document. Needs an administrator.

Signature:

procedure process_new_documents (
p_profile_id in number default null
);

Parameters:

Name Direction Type Description
p_profile_id in number default null The profile; null processes every profile

Writes the test case of a document into the test set of a profile (adm_ai_form_expected), with the source SAMPLE: the values that a correct extraction of the latest version of the document returns. A test case that the document already has is replaced. approve writes test cases too, with the source REVIEW. Needs an administrator, and the document must be in the scope of the profile.

adm_ai_form_extract_api.set_expected(
p_profile_id => adm_ai_form_config_api.get_profile_id('supplier_invoices')
, p_document_id => :document_id
, p_form_code => 'HARBOUR_INVOICE'
, p_fields => '{"invoice_number": "INV-2026-00412", "total_amount": 557.63}'
, p_line_items => '[{"item_number": "PAP-A4-80", "quantity": 4, "unit_price": 24.9, "amount": 99.6}]'
);

Signature:

procedure set_expected (
p_profile_id in number,
p_document_id in number,
p_form_code in varchar2 default null,
p_fields in clob default null,
p_line_items in clob default null
);

Parameters:

Name Direction Type Description
p_profile_id in number The profile
p_document_id in number The document
p_form_code in varchar2 default null The form of the document, from the profile. Null: the document is no
form, and its extraction must go to review; p_fields and p_line_items
must then be null too |

| p_fields | in | clob default null | The correct fields, as a JSON object. Required with a form; every key must be a scalar field of the schema of the document type | | p_line_items | in | clob default null | The correct line items, as a JSON array of objects whose keys are line item columns of the schema. Null means no line items |


Deletes the test case of a document from the test set of a profile. Does nothing when the document has no test case. Needs an administrator.

Signature:

procedure delete_expected (
p_profile_id in number,
p_document_id in number
);

Parameters:

Name Direction Type Description
p_profile_id in number The profile
p_document_id in number The document

A person approves the result of a document. The checks run again on the corrected values, the declared JSON schema included (required fields, types, enum values, and the same for every line item); when one fails, the call raises adm_ai_error.e_form_checks_failed with the issues and changes nothing. A key of p_corrections that is not a scalar field of the schema raises adm_ai_error.e_form_unknown_field, also without a change. A result that was read from an older version than the latest one of the document cannot be approved: the call raises adm_ai_error.e_form_extraction_stale and changes nothing, and no hook runs. Extract the document again first. The extraction row is locked until the transaction ends. Then the status is APPROVED, the document gets its annotations, the review tag goes, and p_add_to_test_set copies the result into adm_ai_form_expected. Then the AFTER_FORM_EXTRACTION hook runs; when it raises, the call raises its error and changes nothing. Needs the edit right on the document.

Writes one AI_EXTRACTION_APPROVE row to the audit trail, in the transaction of the caller, with the extraction, the change of the status, the form and the names - never the values - of the corrected fields. It is written for every caller, an administrator included.

Signature:

procedure approve (
p_profile_id in number,
p_document_id in number,
p_form_code in varchar2 default null,
p_corrections in clob default null,
p_line_items in clob default null,
p_add_to_test_set in boolean default true,
p_version_id in number default null
);

Parameters:

Name Direction Type Description
p_profile_id in number The profile
p_document_id in number The document
p_form_code in varchar2 default null The form, when the extraction found none or the wrong one
p_corrections in clob default null Only the fields that the person changed, as a JSON object,
for example {"total_amount": 285.24} |

| p_line_items | in | clob default null | The complete line items as a JSON array, when the person corrected them | | p_add_to_test_set | in | boolean default true | True writes the approved values to the test set | | p_version_id | in | number default null | The version whose result the person reviewed. When given and not the version of the stored extraction, the call raises adm_ai_error.e_form_extraction_stale. Null skips this check. |


Compares a result of extract with a test case. Each value is compared by the type that the document type of the form declares for it, for the fields and for the cells of the line items:

  • null (or missing in the test case): the result must have JSON null or no value
  • boolean: a JSON boolean with the same value
  • integer: a JSON number that is exactly equal
  • number: a JSON number that is equal to the cent (a difference below 0.005)
  • text, and any property that the schema does not declare: a JSON text that is equal

When the value of the test case does not have the declared type (the test case is older than the schema), the kind of that value decides. The line items must have the same count and the same values in the same order. A line that is no object is wrong; it is no error.

Signature:

function compare_result (
p_result in json_object_t,
p_expected in adm_ai_form_expected%rowtype
) return varchar2;

Parameters:

Name Direction Type Description
p_result in json_object_t A result of extract
p_expected in adm_ai_form_expected%rowtype The test case

Returns: varchar2 - Null when everything is equal, otherwise the names of the wrong fields, or form <code> when the form is wrong


The outcome of one document in a measurement:

  • RIGHT: the values are right and the status is OK, or the test case has no form and the extraction found none either
  • FALSE_ALARM: the values are right, but the checks sent them to review
  • CAUGHT: the values are wrong, and the checks sent them to review
  • MISSED: the values are wrong, but the status is OK
  • FAILED: an error stopped the extraction

Signature:

function measurement_outcome (
p_status in varchar2,
p_wrong_fields in varchar2,
p_expected_fields in clob
) return varchar2;

Parameters:

Name Direction Type Description
p_status in varchar2 The status of the extraction
p_wrong_fields in varchar2 The result of compare_result
p_expected_fields in clob The fields of the test case; null when the document is no form

Returns: varchar2 - The outcome


Extracts every document of the test set of a profile, compares it with the test case and writes one row for each document to adm_ai_form_measurements, with its outcome (see measurement_outcome). Each document is extracted in the version of its test case, see measure_document. Writes nothing else, and does not commit. Needs an administrator.

Signature:

procedure measure (
p_profile_id in number,
p_label in varchar2,
p_form_code in varchar2 default null,
p_name_like in varchar2 default '%',
p_model in varchar2 default null,
p_vision_model in varchar2 default null,
p_prompt in varchar2 default c_prompt_rules,
p_think in boolean default null
);

Parameters:

Name Direction Type Description
p_profile_id in number The profile
p_label in varchar2 Name of this run, for the report
p_form_code in varchar2 default null Only the test cases of this form; null takes all
p_name_like in varchar2 default '%' Only the documents whose name is like this, for example ‘%.pdf’
p_model in varchar2 default null Another model for text, see extract
p_vision_model in varchar2 default null Another model for images and for forms that read the file, see extract
p_prompt in varchar2 default c_prompt_rules c_prompt_rules or c_prompt_naive
p_think in boolean default null See extract

Deletes the rows of one measurement run of a profile. Needs an administrator.

Signature:

procedure delete_measurement (
p_profile_id in number,
p_label in varchar2
);

Parameters:

Name Direction Type Description
p_profile_id in number The profile
p_label in varchar2 Name of the run