From 76a60244e089b134b4c53fce6c74d96e30ea2d64 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 15:27:09 -0400 Subject: [PATCH] feat: add pluggable field detection system (v0.14.0) * feat: add pluggable field detection system (v0.14.0) Add external field detection via Ruby scripts and YAML profiles. Two plugin mechanisms supported side-by-side: 1. Ruby scripts (FIELD_DETECTION_SCRIPTS_DIR) - full-power handlers registered via Templates::FieldDetection.register(name, handler) 2. YAML profiles (FIELD_DETECTION_CONFIG_DIR) - declarative configs with text anchors + relative offsets OR absolute positions Core changes: - lib/templates/field_detection.rb: registry, loader, dispatcher - lib/templates/field_detection/config_based.rb: YAML profile engine with anchor-based and absolute positioning, negative page indices - Controller accepts algorithm param, dispatches accordingly - Split-button UI: main action = ML detection, dropdown = profiles - RSpec tests for both modules with fixture YAML profiles No business-specific algorithms ship with docuseal - all are injected at deploy time via ConfigMap/mounted volumes. * fix: use proper user/account setup in config_based spec * fix: include submitters in algorithm detection response --------- Co-authored-by: Bob Develop --- .../templates_detect_fields_controller.rb | 20 ++ app/javascript/application.js | 3 +- app/javascript/template_builder/builder.vue | 6 + app/javascript/template_builder/fields.vue | 121 +++++++++- app/views/templates/edit.html.erb | 2 +- lib/external_config.rb | 2 + lib/templates/field_detection.rb | 73 ++++++ lib/templates/field_detection/config_based.rb | 176 +++++++++++++++ .../field_detection/absolute_only.yml | 23 ++ spec/fixtures/field_detection/profile_a.yml | 35 +++ .../field_detection/config_based_spec.rb | 207 ++++++++++++++++++ spec/lib/templates/field_detection_spec.rb | 183 ++++++++++++++++ 12 files changed, 847 insertions(+), 4 deletions(-) create mode 100644 lib/templates/field_detection.rb create mode 100644 lib/templates/field_detection/config_based.rb create mode 100644 spec/fixtures/field_detection/absolute_only.yml create mode 100644 spec/fixtures/field_detection/profile_a.yml create mode 100644 spec/lib/templates/field_detection/config_based_spec.rb create mode 100644 spec/lib/templates/field_detection_spec.rb diff --git a/app/controllers/templates_detect_fields_controller.rb b/app/controllers/templates_detect_fields_controller.rb index 56b06cd5..c165e16c 100644 --- a/app/controllers/templates_detect_fields_controller.rb +++ b/app/controllers/templates_detect_fields_controller.rb @@ -6,6 +6,26 @@ class TemplatesDetectFieldsController < ApplicationController load_and_authorize_resource :template def create + if params[:algorithm].present? + create_with_algorithm + else + create_with_ml_detection + end + end + + private + + def create_with_algorithm + documents = @template.schema_documents.preload(:blob) + + fields = Templates::FieldDetection.call(@template, params[:algorithm], documents) + + render json: { fields: fields, submitters: @template.submitters, completed: true } + rescue ArgumentError => e + render json: { error: e.message }, status: :unprocessable_content + end + + def create_with_ml_detection response.headers['Content-Type'] = 'text/event-stream' sse = SSE.new(response.stream) diff --git a/app/javascript/application.js b/app/javascript/application.js index 24671b2c..546caf0e 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -186,7 +186,8 @@ safeRegisterElement('template-builder', class extends HTMLElement { withDownload: true, currencies: (this.dataset.currencies || '').split(',').filter(Boolean), acceptFileTypes: this.dataset.acceptFileTypes, - showTourStartForm: this.dataset.showTourStartForm === 'true' + showTourStartForm: this.dataset.showTourStartForm === 'true', + detectionAlgorithms: JSON.parse(this.dataset.detectionAlgorithms || '[]') }) this.component = this.app.mount(this.appElem) diff --git a/app/javascript/template_builder/builder.vue b/app/javascript/template_builder/builder.vue index 4830f601..5e76d8aa 100644 --- a/app/javascript/template_builder/builder.vue +++ b/app/javascript/template_builder/builder.vue @@ -509,6 +509,7 @@ :field-types="fieldTypes" :with-sticky-submitters="withStickySubmitters" :with-fields-detection="withFieldsDetection" + :detection-algorithms="detectionAlgorithms" :with-signature-id="withSignatureId" :with-prefillable="withPrefillable" :only-defined-fields="onlyDefinedFields" @@ -738,6 +739,11 @@ export default { required: false, default: false }, + detectionAlgorithms: { + type: Array, + required: false, + default: () => [] + }, withCustomFields: { type: Boolean, required: false, diff --git a/app/javascript/template_builder/fields.vue b/app/javascript/template_builder/fields.vue index 7155414d..1d345fec 100644 --- a/app/javascript/template_builder/fields.vue +++ b/app/javascript/template_builder/fields.vue @@ -325,7 +325,89 @@ v-if="!isShowVariables && withFieldsDetection && editable && fields.length < 2 && !template.schema.some((item) => item.dynamic)" class="my-2" > +
+ + +