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:
- An image (
image/*) goes to thevision_modelof the profile, which picks the form and reads the fields and the line items. Without avision_modelthe document goes to review. An image whose type is inocr_text_extraction.apply_to.mime_typesof the profile is read with OCR instead and goes on as text. - 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, theocr_text_extractionof the profile. Text with fewer thanmin_text_charscharacters that are not blank (a scan without a text layer) goes to review without a model call. - The
markersof the active forms of the profile decide the form (seemarker_text). No form, or more than one, goes to review; for no form thetext_modelsuggests one as a hint for the reviewer. The model never picks the form of a text document. - Text longer than
max_text_charsgoes to review: a model that cuts the prompt does so without an error. - The
model_inputof the form decides what the model reads.text(the default): thetext_modelreads the fields from the text. A form with aline_patternreads 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: thevision_modelreads the fields and the line items from the file of the document (methodFILE), 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 fitmax_text_chars. Without avision_model, for a file that is no PDF and no image (image/*), and for a file larger thanmax_file_bytesof 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. adm_ai_form_config_api.check_resultchecks the result. Every check passes: statusOK. OtherwiseREVIEW, 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.
Functions and Procedures
Section titled “Functions and Procedures”document_text
Section titled “document_text”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:
- The OCR text that ADM stored on the version (
index_content_sourceOCR, from the OCR fallback, from theocr_text_extractionof a RAG collection or of a profile), when it has at leastmin_text_charscharacters that are not blank. - 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.
- Only with p_allow_ocr true: when the filter text is shorter than
min_text_charsand theocr_text_extractionof the profile applies to the document (itsapply_to, and the setting AI_TEXT_EXTRACTION isY), 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
marker_text
Section titled “marker_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
document_in_scope
Section titled “document_in_scope”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
matching_forms
Section titled “matching_forms”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
system_prompt
Section titled “system_prompt”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
answer_schema
Section titled “answer_schema”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
read_lines
Section titled “read_lines”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
extract
Section titled “extract”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
extract_document
Section titled “extract_document”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 |
documents_to_process
Section titled “documents_to_process”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 theocr_text_extractionof 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_charscharacters 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
record_failure
Section titled “record_failure”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 |
process_new_documents
Section titled “process_new_documents”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 |
set_expected
Section titled “set_expected”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 |
delete_expected
Section titled “delete_expected”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 |
approve
Section titled “approve”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. |
compare_result
Section titled “compare_result”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
measurement_outcome
Section titled “measurement_outcome”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
measure
Section titled “measure”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 |
delete_measurement
Section titled “delete_measurement”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 |