hazo_collab_forms 3.1.7 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGE_LOG.md +201 -0
- package/README.md +3 -0
- package/dist/components/clarification/clarification_item_body.d.ts +19 -1
- package/dist/components/clarification/clarification_item_body.d.ts.map +1 -1
- package/dist/components/clarification/clarification_item_body.js +114 -6
- package/dist/components/clarification/clarification_item_body.js.map +1 -1
- package/dist/components/clarification/clarification_thread.js +1 -1
- package/dist/components/clarification/clarification_thread.js.map +1 -1
- package/dist/components/clarification/index.d.ts +2 -0
- package/dist/components/clarification/index.d.ts.map +1 -1
- package/dist/components/clarification/index.js +1 -0
- package/dist/components/clarification/index.js.map +1 -1
- package/dist/components/clarification/resolution_status_strip.d.ts +18 -0
- package/dist/components/clarification/resolution_status_strip.d.ts.map +1 -0
- package/dist/components/clarification/resolution_status_strip.js +20 -0
- package/dist/components/clarification/resolution_status_strip.js.map +1 -0
- package/dist/components/hazo_fb_form/context.d.ts +1 -1
- package/dist/components/hazo_fb_form/context.d.ts.map +1 -1
- package/dist/components/hazo_fb_form/hazo_fb_form.d.ts.map +1 -1
- package/dist/components/hazo_fb_form/hazo_fb_form.js +330 -113
- package/dist/components/hazo_fb_form/hazo_fb_form.js.map +1 -1
- package/dist/components/hazo_fb_form/hooks/use_fb_form_state.d.ts +3 -3
- package/dist/components/hazo_fb_form/hooks/use_fb_form_state.d.ts.map +1 -1
- package/dist/components/hazo_fb_form/hooks/use_fb_form_state.js +339 -46
- package/dist/components/hazo_fb_form/hooks/use_fb_form_state.js.map +1 -1
- package/dist/components/hazo_fb_form/hooks/use_llm_run.d.ts +3 -1
- package/dist/components/hazo_fb_form/hooks/use_llm_run.d.ts.map +1 -1
- package/dist/components/hazo_fb_form/hooks/use_llm_run.js +89 -11
- package/dist/components/hazo_fb_form/hooks/use_llm_run.js.map +1 -1
- package/dist/components/hazo_fb_form/shared/agent_stepper.js +1 -1
- package/dist/components/hazo_fb_form/shared/agent_stepper.js.map +1 -1
- package/dist/components/hazo_fb_form/shared/file_status_accordion.d.ts +9 -0
- package/dist/components/hazo_fb_form/shared/file_status_accordion.d.ts.map +1 -0
- package/dist/components/hazo_fb_form/shared/file_status_accordion.js +39 -0
- package/dist/components/hazo_fb_form/shared/file_status_accordion.js.map +1 -0
- package/dist/components/hazo_fb_form/shared/format.d.ts.map +1 -1
- package/dist/components/hazo_fb_form/shared/format.js +8 -3
- package/dist/components/hazo_fb_form/shared/format.js.map +1 -1
- package/dist/components/hazo_fb_form/shared/send_back_item_card.d.ts +7 -1
- package/dist/components/hazo_fb_form/shared/send_back_item_card.d.ts.map +1 -1
- package/dist/components/hazo_fb_form/shared/send_back_item_card.js +6 -3
- package/dist/components/hazo_fb_form/shared/send_back_item_card.js.map +1 -1
- package/dist/components/hazo_fb_form/types.d.ts +3 -1
- package/dist/components/hazo_fb_form/types.d.ts.map +1 -1
- package/dist/components/hazo_fb_form/views/back_office_view.js +1 -1
- package/dist/components/hazo_fb_form/views/back_office_view.js.map +1 -1
- package/dist/components/hazo_fb_form/views/clarifications_view.js +2 -2
- package/dist/components/hazo_fb_form/views/clarifications_view.js.map +1 -1
- package/dist/components/hazo_fb_form/views/front_office_view.d.ts.map +1 -1
- package/dist/components/hazo_fb_form/views/front_office_view.js +62 -41
- package/dist/components/hazo_fb_form/views/front_office_view.js.map +1 -1
- package/dist/components/hazo_fb_form/views/interim_view.js +3 -3
- package/dist/components/hazo_fb_form/views/interim_view.js.map +1 -1
- package/dist/components/hazo_fb_form/views/review_queue_view.d.ts.map +1 -1
- package/dist/components/hazo_fb_form/views/review_queue_view.js +22 -9
- package/dist/components/hazo_fb_form/views/review_queue_view.js.map +1 -1
- package/dist/components/hazo_validation_rule_editor/components/rule_editor.d.ts.map +1 -1
- package/dist/components/hazo_validation_rule_editor/components/rule_editor.js +32 -3
- package/dist/components/hazo_validation_rule_editor/components/rule_editor.js.map +1 -1
- package/dist/components/hazo_validation_rule_editor/components/variable_chain_input.d.ts +20 -0
- package/dist/components/hazo_validation_rule_editor/components/variable_chain_input.d.ts.map +1 -0
- package/dist/components/hazo_validation_rule_editor/components/variable_chain_input.js +34 -0
- package/dist/components/hazo_validation_rule_editor/components/variable_chain_input.js.map +1 -0
- package/dist/components/hazo_validation_rule_editor/context.d.ts +3 -2
- package/dist/components/hazo_validation_rule_editor/context.d.ts.map +1 -1
- package/dist/components/hazo_validation_rule_editor/context.js +15 -3
- package/dist/components/hazo_validation_rule_editor/context.js.map +1 -1
- package/dist/components/hazo_validation_rule_editor/types.d.ts +7 -1
- package/dist/components/hazo_validation_rule_editor/types.d.ts.map +1 -1
- package/dist/components/hazo_validation_rule_editor/validation_rule_editor.d.ts +1 -1
- package/dist/components/hazo_validation_rule_editor/validation_rule_editor.d.ts.map +1 -1
- package/dist/components/hazo_validation_rule_editor/validation_rule_editor.js +2 -2
- package/dist/components/hazo_validation_rule_editor/validation_rule_editor.js.map +1 -1
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +2 -0
- package/dist/components/index.js.map +1 -1
- package/dist/components/shared/document_type_editor.d.ts +31 -0
- package/dist/components/shared/document_type_editor.d.ts.map +1 -0
- package/dist/components/shared/document_type_editor.js +60 -0
- package/dist/components/shared/document_type_editor.js.map +1 -0
- package/dist/components/shared/file_bar/file_bar.d.ts +7 -1
- package/dist/components/shared/file_bar/file_bar.d.ts.map +1 -1
- package/dist/components/shared/file_bar/file_bar.js +5 -3
- package/dist/components/shared/file_bar/file_bar.js.map +1 -1
- package/dist/components/shared/file_bar/file_bar_validation_dialog.js +4 -4
- package/dist/components/shared/file_bar/file_bar_validation_dialog.js.map +1 -1
- package/dist/components/shared/file_status_icon.d.ts +23 -0
- package/dist/components/shared/file_status_icon.d.ts.map +1 -0
- package/dist/components/shared/file_status_icon.js +38 -0
- package/dist/components/shared/file_status_icon.js.map +1 -0
- package/dist/components/shared/json_data_panel/json_data_panel.d.ts +1 -1
- package/dist/components/shared/json_data_panel/json_data_panel.d.ts.map +1 -1
- package/dist/components/shared/json_data_panel/json_data_panel.js +27 -2
- package/dist/components/shared/json_data_panel/json_data_panel.js.map +1 -1
- package/dist/components/shared/rule_result_card.d.ts.map +1 -1
- package/dist/components/shared/rule_result_card.js +5 -4
- package/dist/components/shared/rule_result_card.js.map +1 -1
- package/dist/components/thread_form/components/add_question_dialog.d.ts +12 -0
- package/dist/components/thread_form/components/add_question_dialog.d.ts.map +1 -0
- package/dist/components/thread_form/components/add_question_dialog.js +36 -0
- package/dist/components/thread_form/components/add_question_dialog.js.map +1 -0
- package/dist/components/thread_form/components/agent_compose_dialog.d.ts +30 -0
- package/dist/components/thread_form/components/agent_compose_dialog.d.ts.map +1 -0
- package/dist/components/thread_form/components/agent_compose_dialog.js +45 -0
- package/dist/components/thread_form/components/agent_compose_dialog.js.map +1 -0
- package/dist/components/thread_form/components/clarification.d.ts +14 -0
- package/dist/components/thread_form/components/clarification.d.ts.map +1 -0
- package/dist/components/thread_form/components/clarification.js +12 -0
- package/dist/components/thread_form/components/clarification.js.map +1 -0
- package/dist/components/thread_form/components/collected_data_view.d.ts +15 -0
- package/dist/components/thread_form/components/collected_data_view.d.ts.map +1 -0
- package/dist/components/thread_form/components/collected_data_view.js +121 -0
- package/dist/components/thread_form/components/collected_data_view.js.map +1 -0
- package/dist/components/thread_form/components/coverage_card.d.ts +11 -0
- package/dist/components/thread_form/components/coverage_card.d.ts.map +1 -0
- package/dist/components/thread_form/components/coverage_card.js +60 -0
- package/dist/components/thread_form/components/coverage_card.js.map +1 -0
- package/dist/components/thread_form/components/file_bar.d.ts +93 -0
- package/dist/components/thread_form/components/file_bar.d.ts.map +1 -0
- package/dist/components/thread_form/components/file_bar.js +251 -0
- package/dist/components/thread_form/components/file_bar.js.map +1 -0
- package/dist/components/thread_form/components/file_info_dialog.d.ts +15 -0
- package/dist/components/thread_form/components/file_info_dialog.d.ts.map +1 -0
- package/dist/components/thread_form/components/file_info_dialog.js +64 -0
- package/dist/components/thread_form/components/file_info_dialog.js.map +1 -0
- package/dist/components/thread_form/components/issue_group_tree.d.ts +20 -0
- package/dist/components/thread_form/components/issue_group_tree.d.ts.map +1 -0
- package/dist/components/thread_form/components/issue_group_tree.js +164 -0
- package/dist/components/thread_form/components/issue_group_tree.js.map +1 -0
- package/dist/components/thread_form/components/pdf_side_panel.d.ts +20 -0
- package/dist/components/thread_form/components/pdf_side_panel.d.ts.map +1 -0
- package/dist/components/thread_form/components/pdf_side_panel.js +63 -0
- package/dist/components/thread_form/components/pdf_side_panel.js.map +1 -0
- package/dist/components/thread_form/components/rule_decision_row.d.ts +31 -0
- package/dist/components/thread_form/components/rule_decision_row.d.ts.map +1 -0
- package/dist/components/thread_form/components/rule_decision_row.js +20 -0
- package/dist/components/thread_form/components/rule_decision_row.js.map +1 -0
- package/dist/components/thread_form/components/send_back_message.d.ts +32 -0
- package/dist/components/thread_form/components/send_back_message.d.ts.map +1 -0
- package/dist/components/thread_form/components/send_back_message.js +82 -0
- package/dist/components/thread_form/components/send_back_message.js.map +1 -0
- package/dist/components/thread_form/components/shared.d.ts +54 -0
- package/dist/components/thread_form/components/shared.d.ts.map +1 -0
- package/dist/components/thread_form/components/shared.js +136 -0
- package/dist/components/thread_form/components/shared.js.map +1 -0
- package/dist/components/thread_form/components/task_card.d.ts +90 -0
- package/dist/components/thread_form/components/task_card.d.ts.map +1 -0
- package/dist/components/thread_form/components/task_card.js +63 -0
- package/dist/components/thread_form/components/task_card.js.map +1 -0
- package/dist/components/thread_form/components/text_doc_check.d.ts +15 -0
- package/dist/components/thread_form/components/text_doc_check.d.ts.map +1 -0
- package/dist/components/thread_form/components/text_doc_check.js +16 -0
- package/dist/components/thread_form/components/text_doc_check.js.map +1 -0
- package/dist/components/thread_form/components/text_extraction.d.ts +14 -0
- package/dist/components/thread_form/components/text_extraction.d.ts.map +1 -0
- package/dist/components/thread_form/components/text_extraction.js +16 -0
- package/dist/components/thread_form/components/text_extraction.js.map +1 -0
- package/dist/components/thread_form/components/thread_composer.d.ts +15 -0
- package/dist/components/thread_form/components/thread_composer.d.ts.map +1 -0
- package/dist/components/thread_form/components/thread_composer.js +93 -0
- package/dist/components/thread_form/components/thread_composer.js.map +1 -0
- package/dist/components/thread_form/components/thread_timeline.d.ts +65 -0
- package/dist/components/thread_form/components/thread_timeline.d.ts.map +1 -0
- package/dist/components/thread_form/components/thread_timeline.js +225 -0
- package/dist/components/thread_form/components/thread_timeline.js.map +1 -0
- package/dist/components/thread_form/hooks/use_file_pipeline.d.ts +126 -0
- package/dist/components/thread_form/hooks/use_file_pipeline.d.ts.map +1 -0
- package/dist/components/thread_form/hooks/use_file_pipeline.js +760 -0
- package/dist/components/thread_form/hooks/use_file_pipeline.js.map +1 -0
- package/dist/components/thread_form/hooks/use_thread_form.d.ts +36 -0
- package/dist/components/thread_form/hooks/use_thread_form.d.ts.map +1 -0
- package/dist/components/thread_form/hooks/use_thread_form.js +126 -0
- package/dist/components/thread_form/hooks/use_thread_form.js.map +1 -0
- package/dist/components/thread_form/index.d.ts +33 -0
- package/dist/components/thread_form/index.d.ts.map +1 -0
- package/dist/components/thread_form/index.js +30 -0
- package/dist/components/thread_form/index.js.map +1 -0
- package/dist/components/thread_form/sample_data.d.ts +8 -0
- package/dist/components/thread_form/sample_data.d.ts.map +1 -0
- package/dist/components/thread_form/sample_data.js +658 -0
- package/dist/components/thread_form/sample_data.js.map +1 -0
- package/dist/components/thread_form/thread_form.d.ts +7 -0
- package/dist/components/thread_form/thread_form.d.ts.map +1 -0
- package/dist/components/thread_form/thread_form.js +1385 -0
- package/dist/components/thread_form/thread_form.js.map +1 -0
- package/dist/components/thread_form/types.d.ts +402 -0
- package/dist/components/thread_form/types.d.ts.map +1 -0
- package/dist/components/thread_form/types.js +23 -0
- package/dist/components/thread_form/types.js.map +1 -0
- package/dist/components/thread_form/utils/file_decision_state.d.ts +22 -0
- package/dist/components/thread_form/utils/file_decision_state.d.ts.map +1 -0
- package/dist/components/thread_form/utils/file_decision_state.js +37 -0
- package/dist/components/thread_form/utils/file_decision_state.js.map +1 -0
- package/dist/components/thread_form/utils/merge_send_back.d.ts +13 -0
- package/dist/components/thread_form/utils/merge_send_back.d.ts.map +1 -0
- package/dist/components/thread_form/utils/merge_send_back.js +23 -0
- package/dist/components/thread_form/utils/merge_send_back.js.map +1 -0
- package/dist/lib/autofill_handler.d.ts.map +1 -1
- package/dist/lib/autofill_handler.js +5 -44
- package/dist/lib/autofill_handler.js.map +1 -1
- package/dist/lib/classification_handler.d.ts +105 -0
- package/dist/lib/classification_handler.d.ts.map +1 -0
- package/dist/lib/classification_handler.js +342 -0
- package/dist/lib/classification_handler.js.map +1 -0
- package/dist/lib/content_gate_handler.d.ts +37 -0
- package/dist/lib/content_gate_handler.d.ts.map +1 -0
- package/dist/lib/content_gate_handler.js +126 -0
- package/dist/lib/content_gate_handler.js.map +1 -0
- package/dist/lib/index.d.ts +10 -0
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +5 -0
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/periodic_coverage_runner.d.ts +24 -0
- package/dist/lib/periodic_coverage_runner.d.ts.map +1 -0
- package/dist/lib/periodic_coverage_runner.js +121 -0
- package/dist/lib/periodic_coverage_runner.js.map +1 -0
- package/dist/lib/resolution_handler.d.ts +150 -0
- package/dist/lib/resolution_handler.d.ts.map +1 -0
- package/dist/lib/resolution_handler.js +597 -0
- package/dist/lib/resolution_handler.js.map +1 -0
- package/dist/lib/resolve_variable.d.ts +25 -0
- package/dist/lib/resolve_variable.d.ts.map +1 -0
- package/dist/lib/resolve_variable.js +77 -0
- package/dist/lib/resolve_variable.js.map +1 -0
- package/dist/lib/validation_handler.d.ts +27 -3
- package/dist/lib/validation_handler.d.ts.map +1 -1
- package/dist/lib/validation_handler.js +338 -288
- package/dist/lib/validation_handler.js.map +1 -1
- package/dist/types/clarification.d.ts +54 -0
- package/dist/types/clarification.d.ts.map +1 -1
- package/dist/types/fb_form_data.d.ts +273 -123
- package/dist/types/fb_form_data.d.ts.map +1 -1
- package/dist/types/fb_form_data.js +44 -58
- package/dist/types/fb_form_data.js.map +1 -1
- package/dist/types/fb_form_data_v1.d.ts +250 -0
- package/dist/types/fb_form_data_v1.d.ts.map +1 -0
- package/dist/types/fb_form_data_v1.js +117 -0
- package/dist/types/fb_form_data_v1.js.map +1 -0
- package/dist/types/fb_form_instance.d.ts +1 -1
- package/dist/types/fb_form_instance.d.ts.map +1 -1
- package/dist/types/index.d.ts +5 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +2 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/validation.d.ts +134 -12
- package/dist/types/validation.d.ts.map +1 -1
- package/dist/utils/expectation_extractor.d.ts +31 -0
- package/dist/utils/expectation_extractor.d.ts.map +1 -0
- package/dist/utils/expectation_extractor.js +142 -0
- package/dist/utils/expectation_extractor.js.map +1 -0
- package/dist/utils/fb_data_adapter.d.ts +7 -2
- package/dist/utils/fb_data_adapter.d.ts.map +1 -1
- package/dist/utils/fb_data_adapter.js +58 -7
- package/dist/utils/fb_data_adapter.js.map +1 -1
- package/dist/utils/fb_data_adapter_v2.d.ts +17 -0
- package/dist/utils/fb_data_adapter_v2.d.ts.map +1 -0
- package/dist/utils/fb_data_adapter_v2.js +483 -0
- package/dist/utils/fb_data_adapter_v2.js.map +1 -0
- package/dist/utils/fb_data_helpers.d.ts +1 -1
- package/dist/utils/fb_data_helpers.d.ts.map +1 -1
- package/dist/utils/fb_data_mutations.d.ts +1 -1
- package/dist/utils/fb_data_mutations.d.ts.map +1 -1
- package/dist/utils/fb_data_mutations_v2.d.ts +46 -0
- package/dist/utils/fb_data_mutations_v2.d.ts.map +1 -0
- package/dist/utils/fb_data_mutations_v2.js +341 -0
- package/dist/utils/fb_data_mutations_v2.js.map +1 -0
- package/dist/utils/fb_data_queries.d.ts +81 -0
- package/dist/utils/fb_data_queries.d.ts.map +1 -0
- package/dist/utils/fb_data_queries.js +354 -0
- package/dist/utils/fb_data_queries.js.map +1 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +6 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/issue_bucketing.d.ts +36 -0
- package/dist/utils/issue_bucketing.d.ts.map +1 -0
- package/dist/utils/issue_bucketing.js +107 -0
- package/dist/utils/issue_bucketing.js.map +1 -0
- package/dist/utils/validation_result.d.ts +32 -0
- package/dist/utils/validation_result.d.ts.map +1 -0
- package/dist/utils/validation_result.js +55 -0
- package/dist/utils/validation_result.js.map +1 -0
- package/package.json +16 -4
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Server-side validation route handler factory.
|
|
3
3
|
*
|
|
4
|
-
* Creates a POST handler that executes validation rules against uploaded
|
|
5
|
-
* using LLM analysis, and returns ClarificationItems
|
|
4
|
+
* Creates a POST handler that executes validation rules against uploaded
|
|
5
|
+
* documents or plain text using LLM analysis, and returns ClarificationItems
|
|
6
|
+
* for any issues found.
|
|
6
7
|
*
|
|
7
8
|
* Usage:
|
|
8
9
|
* import { create_validation_route } from 'hazo_collab_forms/lib';
|
|
@@ -13,112 +14,145 @@
|
|
|
13
14
|
*/
|
|
14
15
|
import 'server-only';
|
|
15
16
|
import { DEFAULT_CLARIFICATION_TEMPLATES } from '../config/clarification_templates.js';
|
|
16
|
-
import { DEFAULT_VALIDATION_PROMPT_SUFFIX } from '../config/defaults.js';
|
|
17
17
|
import { get_config } from './config.js';
|
|
18
|
+
import { make_issue_id } from '../utils/validation_result.js';
|
|
19
|
+
import { extract_expectation } from '../utils/expectation_extractor.js';
|
|
18
20
|
// ============================================================================
|
|
19
21
|
// Helpers
|
|
20
22
|
// ============================================================================
|
|
21
23
|
function generate_clarification_id() {
|
|
22
24
|
return `clr_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
23
25
|
}
|
|
24
|
-
/**
|
|
25
|
-
* Extract has_issue and issue_description from a parsed JSON object.
|
|
26
|
-
* Supports both new format (validation_passed, validation_details) and
|
|
27
|
-
* legacy format (has_issue, issue_description).
|
|
28
|
-
*/
|
|
29
|
-
function extract_validation_fields(parsed) {
|
|
30
|
-
// Determine has_issue: prefer new `validation_passed` (inverted), fall back to legacy `has_issue`
|
|
31
|
-
const has_issue = 'validation_passed' in parsed
|
|
32
|
-
? !parsed.validation_passed
|
|
33
|
-
: !!parsed.has_issue;
|
|
34
|
-
// Determine issue_description: prefer new `validation_details`, fall back to legacy names
|
|
35
|
-
const issue_description = parsed.validation_details ||
|
|
36
|
-
parsed.issue_description ||
|
|
37
|
-
parsed.description ||
|
|
38
|
-
parsed.issue ||
|
|
39
|
-
undefined;
|
|
40
|
-
return {
|
|
41
|
-
has_issue,
|
|
42
|
-
issue_description,
|
|
43
|
-
confidence: typeof parsed.confidence === 'number' ? parsed.confidence : undefined,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Parse LLM response for validation results.
|
|
48
|
-
* Supports new format: { validation_passed, validation_details, confidence }
|
|
49
|
-
* and legacy format: { has_issue, issue_description, confidence }
|
|
50
|
-
*/
|
|
51
|
-
function parse_validation_response(text) {
|
|
52
|
-
const cleaned = text.trim();
|
|
53
|
-
// Try direct parse
|
|
54
|
-
try {
|
|
55
|
-
const parsed = JSON.parse(cleaned);
|
|
56
|
-
return extract_validation_fields(parsed);
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
// Continue
|
|
60
|
-
}
|
|
61
|
-
// Strip markdown code fences
|
|
62
|
-
const fence_match = cleaned.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
|
|
63
|
-
if (fence_match) {
|
|
64
|
-
try {
|
|
65
|
-
const parsed = JSON.parse(fence_match[1].trim());
|
|
66
|
-
return extract_validation_fields(parsed);
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
// Continue
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
// Try to find JSON in text
|
|
73
|
-
const json_match = cleaned.match(/\{[\s\S]*\}/);
|
|
74
|
-
if (json_match) {
|
|
75
|
-
try {
|
|
76
|
-
const parsed = JSON.parse(json_match[0]);
|
|
77
|
-
return extract_validation_fields(parsed);
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
// Continue
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
// Heuristic: if the text mentions "issue", "problem", "invalid", etc., consider it an issue
|
|
84
|
-
const issue_keywords = ['issue', 'problem', 'invalid', 'missing', 'incomplete', 'error', 'incorrect'];
|
|
85
|
-
const lower = cleaned.toLowerCase();
|
|
86
|
-
const has_issue = issue_keywords.some(k => lower.includes(k));
|
|
87
|
-
return {
|
|
88
|
-
has_issue,
|
|
89
|
-
issue_description: has_issue ? cleaned.slice(0, 500) : undefined,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
26
|
function is_image_mime(mime_type) {
|
|
93
27
|
return mime_type.startsWith('image/');
|
|
94
28
|
}
|
|
95
29
|
function is_document_mime(mime_type) {
|
|
96
30
|
return mime_type === 'application/pdf';
|
|
97
31
|
}
|
|
98
|
-
/**
|
|
99
|
-
* Substitute {{variable}} placeholders in prompt text.
|
|
100
|
-
*/
|
|
32
|
+
/** Substitute {{variable}} placeholders in prompt text. */
|
|
101
33
|
function substitute_variables(prompt, variables) {
|
|
102
34
|
return prompt.replace(/\{\{(\w+)\}\}/g, (_, key) => {
|
|
103
35
|
return variables[key] ?? `{{${key}}}`;
|
|
104
36
|
});
|
|
105
37
|
}
|
|
106
|
-
/**
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
*/
|
|
110
|
-
function build_validation_prompt(rule_prompt) {
|
|
111
|
-
const suffix = get_config('validation', 'prompt_suffix') ?? DEFAULT_VALIDATION_PROMPT_SUFFIX;
|
|
112
|
-
return `You are a document validation assistant. Analyze the provided document according to the following rule and determine if there is an issue.
|
|
113
|
-
|
|
114
|
-
VALIDATION RULE:
|
|
115
|
-
${rule_prompt}
|
|
116
|
-
|
|
117
|
-
${suffix}`;
|
|
38
|
+
/** Wrap a per-rule prompt with the response envelope from hazo_prompts. */
|
|
39
|
+
function wrap_rule_prompt(rule_prompt, wrapper) {
|
|
40
|
+
return substitute_variables(wrapper, { rule_prompt });
|
|
118
41
|
}
|
|
42
|
+
// ============================================================================
|
|
43
|
+
// Parser (unified — v4)
|
|
44
|
+
// ============================================================================
|
|
119
45
|
/**
|
|
120
|
-
*
|
|
46
|
+
* Parse the LLM response into a ValidationRuleResult.
|
|
47
|
+
*
|
|
48
|
+
* Expected shape (per validation/response_wrapper in hazo_prompts):
|
|
49
|
+
* {
|
|
50
|
+
* "summary": "...", // optional rule-level explanation
|
|
51
|
+
* "issues": [ // empty = passed
|
|
52
|
+
* { "description": "...", "amount": "...", "date": "...", "reason": "...",
|
|
53
|
+
* "client_comment": "...", "confidence": 0.9 }
|
|
54
|
+
* ],
|
|
55
|
+
* "confidence": 0.95 // optional rule-level confidence
|
|
56
|
+
* }
|
|
57
|
+
*
|
|
58
|
+
* Tolerates: direct JSON, fenced code block, regex-extracted object, and
|
|
59
|
+
* top-level arrays (treated as the issues array).
|
|
121
60
|
*/
|
|
61
|
+
export function parse_response(text, rule_id) {
|
|
62
|
+
const cleaned = text.trim();
|
|
63
|
+
const try_parse = (raw) => {
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(raw);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const parsed = try_parse(cleaned) ??
|
|
72
|
+
(() => {
|
|
73
|
+
const fence = cleaned.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
|
|
74
|
+
return fence ? try_parse(fence[1].trim()) : null;
|
|
75
|
+
})() ??
|
|
76
|
+
(() => {
|
|
77
|
+
const obj = cleaned.match(/\{[\s\S]*\}/);
|
|
78
|
+
if (obj)
|
|
79
|
+
return try_parse(obj[0]);
|
|
80
|
+
const arr = cleaned.match(/\[[\s\S]*\]/);
|
|
81
|
+
return arr ? try_parse(arr[0]) : null;
|
|
82
|
+
})();
|
|
83
|
+
if (!parsed)
|
|
84
|
+
return { issues: [] };
|
|
85
|
+
let raw_issues = [];
|
|
86
|
+
let summary;
|
|
87
|
+
let confidence;
|
|
88
|
+
let extracted_data;
|
|
89
|
+
if (Array.isArray(parsed)) {
|
|
90
|
+
raw_issues = parsed;
|
|
91
|
+
}
|
|
92
|
+
else if (typeof parsed === 'object' && parsed !== null) {
|
|
93
|
+
const obj = parsed;
|
|
94
|
+
if (Array.isArray(obj.issues))
|
|
95
|
+
raw_issues = obj.issues;
|
|
96
|
+
else if (Array.isArray(obj.items))
|
|
97
|
+
raw_issues = obj.items;
|
|
98
|
+
if (typeof obj.summary === 'string')
|
|
99
|
+
summary = obj.summary;
|
|
100
|
+
if (typeof obj.confidence === 'number')
|
|
101
|
+
confidence = obj.confidence;
|
|
102
|
+
if (obj.extracted_data && typeof obj.extracted_data === 'object' && !Array.isArray(obj.extracted_data)) {
|
|
103
|
+
extracted_data = obj.extracted_data;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const issues = [];
|
|
107
|
+
for (const row of raw_issues) {
|
|
108
|
+
if (typeof row !== 'object' || row === null)
|
|
109
|
+
continue;
|
|
110
|
+
const r = row;
|
|
111
|
+
const description_raw = r.description ||
|
|
112
|
+
r.issue_description ||
|
|
113
|
+
r.issue ||
|
|
114
|
+
r.label ||
|
|
115
|
+
'';
|
|
116
|
+
if (!description_raw)
|
|
117
|
+
continue;
|
|
118
|
+
const amount = r.amount;
|
|
119
|
+
const date = r.date;
|
|
120
|
+
const reason = r.reason;
|
|
121
|
+
const client_comment = r.client_comment;
|
|
122
|
+
const issue_confidence = typeof r.confidence === 'number' ? r.confidence : undefined;
|
|
123
|
+
// Compose a richer human-readable description that includes amount/date
|
|
124
|
+
// so list views show useful context without reaching into sub-fields.
|
|
125
|
+
const parts = [description_raw];
|
|
126
|
+
if (amount !== undefined && amount !== '')
|
|
127
|
+
parts.push(`$${amount}`);
|
|
128
|
+
if (date)
|
|
129
|
+
parts.push(String(date));
|
|
130
|
+
const header = parts.join(' — ');
|
|
131
|
+
const issue_description = reason && reason !== description_raw
|
|
132
|
+
? `${header}. ${reason}`
|
|
133
|
+
: header;
|
|
134
|
+
const id_seed = [rule_id, description_raw, amount, date];
|
|
135
|
+
const fallback = `iss-${issues.length}`;
|
|
136
|
+
issues.push({
|
|
137
|
+
issue_id: make_issue_id(id_seed, fallback),
|
|
138
|
+
issue_description,
|
|
139
|
+
...(amount !== undefined && amount !== '' ? { amount: String(amount) } : {}),
|
|
140
|
+
...(date ? { date: String(date) } : {}),
|
|
141
|
+
...(reason ? { reason } : {}),
|
|
142
|
+
...(client_comment ? { client_comment } : {}),
|
|
143
|
+
...(issue_confidence !== undefined ? { confidence: issue_confidence } : {}),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
issues,
|
|
148
|
+
...(summary ? { summary } : {}),
|
|
149
|
+
...(confidence !== undefined ? { confidence } : {}),
|
|
150
|
+
...(extracted_data ? { extracted_data } : {}),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
// ============================================================================
|
|
154
|
+
// Clarification creation
|
|
155
|
+
// ============================================================================
|
|
122
156
|
function create_clarification_from_result(rule, result, doc_info) {
|
|
123
157
|
const template = rule.clarification_type !== 'none'
|
|
124
158
|
? DEFAULT_CLARIFICATION_TEMPLATES[rule.clarification_type]
|
|
@@ -126,6 +160,16 @@ function create_clarification_from_result(rule, result, doc_info) {
|
|
|
126
160
|
const response_options = rule.custom_response_options ??
|
|
127
161
|
template?.response_options ??
|
|
128
162
|
[];
|
|
163
|
+
// Derive a human-readable issue description: prefer the first issue's
|
|
164
|
+
// client_comment (LLM-drafted for the client), fall back to its description,
|
|
165
|
+
// then the rule-level summary, then template/default.
|
|
166
|
+
const first = result.issues[0];
|
|
167
|
+
const derived_issue_description = first?.client_comment ??
|
|
168
|
+
first?.issue_description ??
|
|
169
|
+
result.summary ??
|
|
170
|
+
template?.default_issue_description ??
|
|
171
|
+
'A validation issue was detected.';
|
|
172
|
+
const expectation = first ? extract_expectation(first) : undefined;
|
|
129
173
|
return {
|
|
130
174
|
id: generate_clarification_id(),
|
|
131
175
|
type: rule.clarification_type,
|
|
@@ -135,10 +179,8 @@ function create_clarification_from_result(rule, result, doc_info) {
|
|
|
135
179
|
rule_name: rule.name,
|
|
136
180
|
rule_id: rule.rule_id,
|
|
137
181
|
issue_description: rule.custom_issue_description ??
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
'A validation issue was detected.',
|
|
141
|
-
validation_details: result.issue_description,
|
|
182
|
+
derived_issue_description,
|
|
183
|
+
validation_details: result.summary ?? first?.issue_description,
|
|
142
184
|
doc_references: doc_info ? [{
|
|
143
185
|
file_id: '', // Client enriches with actual file_id
|
|
144
186
|
file_name: doc_info.file_name,
|
|
@@ -149,6 +191,7 @@ function create_clarification_from_result(rule, result, doc_info) {
|
|
|
149
191
|
response_options,
|
|
150
192
|
response_files: [],
|
|
151
193
|
created_at: new Date().toISOString(),
|
|
194
|
+
...(expectation ? { expectation } : {}),
|
|
152
195
|
};
|
|
153
196
|
}
|
|
154
197
|
// ============================================================================
|
|
@@ -171,6 +214,51 @@ export function create_validation_route(options) {
|
|
|
171
214
|
// Cache the dynamic import
|
|
172
215
|
let llm_api_module = null;
|
|
173
216
|
let init_promise = null;
|
|
217
|
+
// Validation wrappers live in hazo_prompts (not in code). Loaded once per
|
|
218
|
+
// route-handler lifetime and cached. Updates via the Prompt Editor require a
|
|
219
|
+
// server restart to pick up — same behaviour as every other prompt loaded
|
|
220
|
+
// here. There is intentionally no hardcoded fallback: missing wrappers must
|
|
221
|
+
// surface as a 500 with a seeding hint.
|
|
222
|
+
let wrapper_cache = null;
|
|
223
|
+
async function get_validation_wrappers() {
|
|
224
|
+
if (wrapper_cache)
|
|
225
|
+
return wrapper_cache;
|
|
226
|
+
await ensure_initialized();
|
|
227
|
+
const api = await get_llm_api();
|
|
228
|
+
const connect = api.get_hazo_connect?.() ?? null;
|
|
229
|
+
if (!connect) {
|
|
230
|
+
throw new Error('hazo_llm_api hazo_connect is not available — cannot load validation wrappers from hazo_prompts');
|
|
231
|
+
}
|
|
232
|
+
const fetch_one = async (key) => {
|
|
233
|
+
const r = await connect.get_by_area_key('validation', key);
|
|
234
|
+
return r.success ? (r.data?.prompt_text_full ?? null) : null;
|
|
235
|
+
};
|
|
236
|
+
const [response, multi_doc, text, document_extract] = await Promise.all([
|
|
237
|
+
fetch_one('response_wrapper'),
|
|
238
|
+
fetch_one('multi_doc_suffix'),
|
|
239
|
+
fetch_one('text_wrapper'),
|
|
240
|
+
fetch_one('document_extract'),
|
|
241
|
+
]);
|
|
242
|
+
const missing = [];
|
|
243
|
+
if (!response)
|
|
244
|
+
missing.push('validation/response_wrapper');
|
|
245
|
+
if (!multi_doc)
|
|
246
|
+
missing.push('validation/multi_doc_suffix');
|
|
247
|
+
if (!text)
|
|
248
|
+
missing.push('validation/text_wrapper');
|
|
249
|
+
if (!document_extract)
|
|
250
|
+
missing.push('validation/document_extract');
|
|
251
|
+
if (missing.length > 0) {
|
|
252
|
+
throw new Error(`Validation wrappers missing in hazo_prompts: ${missing.join(', ')}. Run test-app/scripts/seed-prompts.mjs.`);
|
|
253
|
+
}
|
|
254
|
+
wrapper_cache = {
|
|
255
|
+
response: response,
|
|
256
|
+
multi_doc: multi_doc,
|
|
257
|
+
text: text,
|
|
258
|
+
document_extract: document_extract,
|
|
259
|
+
};
|
|
260
|
+
return wrapper_cache;
|
|
261
|
+
}
|
|
174
262
|
async function get_llm_api() {
|
|
175
263
|
if (!llm_api_module) {
|
|
176
264
|
try {
|
|
@@ -196,138 +284,122 @@ export function create_validation_route(options) {
|
|
|
196
284
|
}
|
|
197
285
|
}
|
|
198
286
|
/**
|
|
199
|
-
*
|
|
287
|
+
* Call the LLM for a given input. Returns the raw response text.
|
|
288
|
+
* Internal: all three input kinds converge here so run_rule can stay flat.
|
|
200
289
|
*/
|
|
201
|
-
async function
|
|
290
|
+
async function call_llm_for_input(full_prompt, input, wrappers) {
|
|
202
291
|
const api = await get_llm_api();
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
let llm_response;
|
|
208
|
-
if (is_image_mime(mime_type)) {
|
|
209
|
-
llm_response = await api.hazo_llm_image_text({
|
|
210
|
-
prompt: full_prompt,
|
|
211
|
-
image_b64: file_b64,
|
|
212
|
-
image_mime_type: mime_type,
|
|
292
|
+
if (input.kind === 'text') {
|
|
293
|
+
const text_prompt = substitute_variables(wrappers.text, {
|
|
294
|
+
full_prompt,
|
|
295
|
+
text_content: input.text_content,
|
|
213
296
|
});
|
|
297
|
+
return api.hazo_llm_text_text({ prompt: text_prompt });
|
|
214
298
|
}
|
|
215
|
-
|
|
216
|
-
|
|
299
|
+
if (input.kind === 'document') {
|
|
300
|
+
if (is_image_mime(input.mime_type)) {
|
|
301
|
+
return api.hazo_llm_image_text({
|
|
302
|
+
prompt: full_prompt,
|
|
303
|
+
image_b64: input.file_b64,
|
|
304
|
+
image_mime_type: input.mime_type,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
return api.hazo_llm_document_text({
|
|
217
308
|
prompt: full_prompt,
|
|
218
|
-
document_b64: file_b64,
|
|
219
|
-
document_mime_type: mime_type,
|
|
309
|
+
document_b64: input.file_b64,
|
|
310
|
+
document_mime_type: input.mime_type,
|
|
220
311
|
});
|
|
221
312
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
313
|
+
// input.kind === 'documents' (batch)
|
|
314
|
+
const [primary, ...additional] = input.files;
|
|
315
|
+
const additional_documents = additional.map(f => ({
|
|
316
|
+
mime_type: f.mime_type,
|
|
317
|
+
data: f.file_b64,
|
|
318
|
+
}));
|
|
319
|
+
// If the primary is an image, fall back to extract-then-evaluate since
|
|
320
|
+
// image_text doesn't support additional_documents.
|
|
321
|
+
if (is_image_mime(primary.mime_type)) {
|
|
322
|
+
return batch_via_extraction(full_prompt, input.files, wrappers);
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
return await api.hazo_llm_document_text({
|
|
225
326
|
prompt: full_prompt,
|
|
226
|
-
|
|
227
|
-
|
|
327
|
+
document_b64: primary.file_b64,
|
|
328
|
+
document_mime_type: primary.mime_type,
|
|
329
|
+
additional_documents: additional_documents.length > 0 ? additional_documents : undefined,
|
|
228
330
|
});
|
|
229
331
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
has_issue: false,
|
|
234
|
-
raw_response: llm_response.error ?? 'LLM call failed',
|
|
235
|
-
};
|
|
332
|
+
catch {
|
|
333
|
+
// If additional_documents isn't supported, fall back.
|
|
334
|
+
return batch_via_extraction(full_prompt, input.files, wrappers);
|
|
236
335
|
}
|
|
237
|
-
// Parse response
|
|
238
|
-
const parsed = parse_validation_response(llm_response.text);
|
|
239
|
-
return {
|
|
240
|
-
rule_id: rule.rule_id,
|
|
241
|
-
has_issue: parsed.has_issue,
|
|
242
|
-
issue_description: parsed.issue_description,
|
|
243
|
-
confidence: parsed.confidence,
|
|
244
|
-
raw_response: llm_response.text,
|
|
245
|
-
};
|
|
246
336
|
}
|
|
247
337
|
/**
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
* Falls back to extract-then-evaluate via hazo_llm_text_text if additional_documents is not supported.
|
|
338
|
+
* Fallback: extract each file individually, then evaluate collectively via
|
|
339
|
+
* text-only LLM. Uses validation/document_extract + validation/multi_doc_suffix.
|
|
251
340
|
*/
|
|
252
|
-
async function
|
|
341
|
+
async function batch_via_extraction(full_prompt, files, wrappers) {
|
|
253
342
|
const api = await get_llm_api();
|
|
254
|
-
const
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const additional_documents = additional.map(f => ({
|
|
261
|
-
mime_type: f.mime_type,
|
|
262
|
-
data: f.file_b64,
|
|
263
|
-
}));
|
|
264
|
-
let llm_response;
|
|
265
|
-
try {
|
|
266
|
-
if (is_document_mime(primary.mime_type)) {
|
|
267
|
-
llm_response = await api.hazo_llm_document_text({
|
|
268
|
-
prompt: full_prompt,
|
|
269
|
-
document_b64: primary.file_b64,
|
|
270
|
-
document_mime_type: primary.mime_type,
|
|
271
|
-
additional_documents: additional_documents.length > 0 ? additional_documents : undefined,
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
else if (is_image_mime(primary.mime_type)) {
|
|
275
|
-
// For images, fall back to extract-then-evaluate since image_text doesn't support additional_documents
|
|
276
|
-
llm_response = await execute_batch_via_extraction(api, full_prompt, files);
|
|
277
|
-
}
|
|
278
|
-
else {
|
|
279
|
-
llm_response = await api.hazo_llm_document_text({
|
|
280
|
-
prompt: full_prompt,
|
|
281
|
-
document_b64: primary.file_b64,
|
|
282
|
-
document_mime_type: primary.mime_type,
|
|
283
|
-
additional_documents: additional_documents.length > 0 ? additional_documents : undefined,
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
catch {
|
|
288
|
-
// If additional_documents not supported, fall back to extract-then-evaluate
|
|
289
|
-
llm_response = await execute_batch_via_extraction(api, full_prompt, files);
|
|
343
|
+
const extractions = [];
|
|
344
|
+
for (const file of files) {
|
|
345
|
+
const resp = is_image_mime(file.mime_type)
|
|
346
|
+
? await api.hazo_llm_image_text({ prompt: wrappers.document_extract, image_b64: file.file_b64, image_mime_type: file.mime_type })
|
|
347
|
+
: await api.hazo_llm_document_text({ prompt: wrappers.document_extract, document_b64: file.file_b64, document_mime_type: file.mime_type });
|
|
348
|
+
extractions.push(`FILE: ${file.file_name}\n${resp.success && resp.text ? resp.text : '[Extraction failed]'}`);
|
|
290
349
|
}
|
|
350
|
+
const combined_prompt = substitute_variables(wrappers.multi_doc, {
|
|
351
|
+
full_prompt,
|
|
352
|
+
extractions: extractions.join('\n\n---\n\n'),
|
|
353
|
+
});
|
|
354
|
+
return api.hazo_llm_text_text({ prompt: combined_prompt });
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Execute a single rule against a given input. This is the one place all
|
|
358
|
+
* three paths (single document, multi-document batch, text) converge.
|
|
359
|
+
*/
|
|
360
|
+
async function run_rule(rule, input, variables, wrappers) {
|
|
361
|
+
const resolved_prompt = substitute_variables(rule.prompt, variables);
|
|
362
|
+
const full_prompt = wrap_rule_prompt(resolved_prompt, wrappers.response);
|
|
363
|
+
const llm_response = await call_llm_for_input(full_prompt, input, wrappers);
|
|
291
364
|
if (!llm_response.success || !llm_response.text) {
|
|
292
365
|
return {
|
|
293
366
|
rule_id: rule.rule_id,
|
|
294
|
-
|
|
295
|
-
raw_response: llm_response.error ?? 'LLM
|
|
367
|
+
issues: [],
|
|
368
|
+
raw_response: llm_response.error ?? 'LLM call failed',
|
|
369
|
+
check_type: rule.check_type,
|
|
296
370
|
};
|
|
297
371
|
}
|
|
298
|
-
const parsed =
|
|
372
|
+
const parsed = parse_response(llm_response.text, rule.rule_id);
|
|
299
373
|
return {
|
|
300
374
|
rule_id: rule.rule_id,
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
confidence: parsed.confidence,
|
|
375
|
+
issues: parsed.issues,
|
|
376
|
+
...(parsed.summary ? { summary: parsed.summary } : {}),
|
|
377
|
+
...(parsed.confidence !== undefined ? { confidence: parsed.confidence } : {}),
|
|
378
|
+
...(parsed.extracted_data ? { extracted_data: parsed.extracted_data } : {}),
|
|
304
379
|
raw_response: llm_response.text,
|
|
380
|
+
check_type: rule.check_type,
|
|
305
381
|
};
|
|
306
382
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
extractions.push(`FILE: ${file.file_name}\n${resp.success && resp.text ? resp.text : '[Extraction failed]'}`);
|
|
322
|
-
}
|
|
323
|
-
const combined_prompt = `${full_prompt}\n\nIMPORTANT: You are evaluating MULTIPLE documents collectively. For monetary amounts, compare the TOTAL/SUM across all documents.\n\nEXTRACTED DOCUMENT INFORMATION:\n${extractions.join('\n\n---\n\n')}`;
|
|
324
|
-
return api.hazo_llm_text_text({ prompt: combined_prompt });
|
|
383
|
+
// ── File fetching helpers (route-level) ──
|
|
384
|
+
async function fetch_file_b64(request_url, entry) {
|
|
385
|
+
if (entry.file_b64)
|
|
386
|
+
return entry.file_b64;
|
|
387
|
+
if (!entry.download_url)
|
|
388
|
+
throw new Error(`No file_b64 or download_url for "${entry.file_name}"`);
|
|
389
|
+
const absolute = entry.download_url.startsWith('http')
|
|
390
|
+
? entry.download_url
|
|
391
|
+
: `${new URL(request_url).origin}${entry.download_url}`;
|
|
392
|
+
const resp = await fetch(absolute);
|
|
393
|
+
if (!resp.ok)
|
|
394
|
+
throw new Error(`Failed to fetch file "${entry.file_name}": ${resp.status}`);
|
|
395
|
+
const buffer = Buffer.from(await resp.arrayBuffer());
|
|
396
|
+
return buffer.toString('base64');
|
|
325
397
|
}
|
|
398
|
+
// ── Route ──
|
|
326
399
|
return async function POST(request) {
|
|
327
400
|
try {
|
|
328
401
|
const body = await request.json();
|
|
329
402
|
const { file_name, mime_type, download_url, file_b64: request_file_b64, rules, variables = {}, content_tag, } = body;
|
|
330
|
-
// Validate required fields
|
|
331
403
|
if (!rules?.length) {
|
|
332
404
|
return Response.json({
|
|
333
405
|
success: false,
|
|
@@ -336,144 +408,122 @@ export function create_validation_route(options) {
|
|
|
336
408
|
errors: [{ rule_id: '', error: 'rules array is required and must not be empty' }],
|
|
337
409
|
}, { status: 400 });
|
|
338
410
|
}
|
|
339
|
-
// Initialize LLM API
|
|
340
411
|
await ensure_initialized();
|
|
341
|
-
|
|
412
|
+
const wrappers = await get_validation_wrappers();
|
|
413
|
+
// ── Build the ValidationInput based on request shape ──
|
|
414
|
+
let input;
|
|
415
|
+
const text_content = body.text_content;
|
|
416
|
+
const is_text_mode = !!text_content && !request_file_b64 && !download_url;
|
|
417
|
+
const rule_results = [];
|
|
418
|
+
const clarifications = [];
|
|
419
|
+
const errors = [];
|
|
342
420
|
if (body.mode === 'batch' && body.files?.length) {
|
|
421
|
+
// Multi-file batch
|
|
343
422
|
const batch_files = [];
|
|
344
|
-
const fetch_errors = [];
|
|
345
423
|
for (const f of body.files) {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
else if (f.download_url) {
|
|
351
|
-
const absolute_url = f.download_url.startsWith('http')
|
|
352
|
-
? f.download_url
|
|
353
|
-
: `${new URL(request.url).origin}${f.download_url}`;
|
|
354
|
-
const file_response = await fetch(absolute_url);
|
|
355
|
-
if (!file_response.ok) {
|
|
356
|
-
fetch_errors.push({ rule_id: '', error: `Failed to fetch file "${f.file_name}": ${file_response.status}` });
|
|
357
|
-
continue;
|
|
358
|
-
}
|
|
359
|
-
const buffer = Buffer.from(await file_response.arrayBuffer());
|
|
360
|
-
b64 = buffer.toString('base64');
|
|
424
|
+
try {
|
|
425
|
+
const b64 = await fetch_file_b64(request.url, f);
|
|
426
|
+
batch_files.push({ file_id: f.file_id, file_name: f.file_name, mime_type: f.mime_type, file_b64: b64 });
|
|
361
427
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
continue;
|
|
428
|
+
catch (err) {
|
|
429
|
+
errors.push({ rule_id: '', error: err instanceof Error ? err.message : 'Unknown file fetch error' });
|
|
365
430
|
}
|
|
366
|
-
batch_files.push({ file_id: f.file_id, file_name: f.file_name, file_b64: b64, mime_type: f.mime_type });
|
|
367
431
|
}
|
|
368
432
|
if (batch_files.length === 0) {
|
|
369
433
|
return Response.json({
|
|
370
|
-
success: false,
|
|
371
|
-
|
|
434
|
+
success: false,
|
|
435
|
+
clarifications: [],
|
|
436
|
+
rule_results: [],
|
|
437
|
+
errors: errors.length > 0 ? errors : [{ rule_id: '', error: 'No files could be fetched for batch validation' }],
|
|
372
438
|
}, { status: 500 });
|
|
373
439
|
}
|
|
440
|
+
input = { kind: 'documents', files: batch_files };
|
|
374
441
|
const batch_variables = {
|
|
375
442
|
...variables,
|
|
376
443
|
document_name: batch_files.map(f => f.file_name).join(', '),
|
|
377
444
|
...(content_tag ? { document_type: content_tag } : {}),
|
|
378
445
|
};
|
|
379
|
-
const batch_rule_results = [];
|
|
380
|
-
const batch_clarifications = [];
|
|
381
|
-
const batch_errors = [...fetch_errors];
|
|
382
446
|
for (const rule of rules) {
|
|
383
447
|
try {
|
|
384
|
-
const result = await
|
|
385
|
-
|
|
386
|
-
if (result.
|
|
448
|
+
const result = await run_rule(rule, input, batch_variables, wrappers);
|
|
449
|
+
rule_results.push(result);
|
|
450
|
+
if (result.issues.length > 0 && rule.clarification_type !== 'none') {
|
|
387
451
|
const clarification = create_clarification_from_result(rule, result, {
|
|
388
452
|
file_name: batch_files.map(f => f.file_name).join(', '),
|
|
389
453
|
mime_type: batch_files[0].mime_type,
|
|
390
454
|
});
|
|
391
|
-
// Set doc_references to include ALL files in the batch
|
|
392
455
|
clarification.doc_references = batch_files.map(f => ({
|
|
393
|
-
file_id: f.file_id,
|
|
394
|
-
file_name: f.file_name,
|
|
395
|
-
mime_type: f.mime_type,
|
|
456
|
+
file_id: f.file_id, file_name: f.file_name, mime_type: f.mime_type,
|
|
396
457
|
}));
|
|
397
|
-
|
|
458
|
+
clarifications.push(clarification);
|
|
398
459
|
}
|
|
399
460
|
}
|
|
400
461
|
catch (err) {
|
|
401
462
|
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
402
463
|
on_error(err, `batch rule ${rule.rule_id}: ${rule.name}`);
|
|
403
|
-
|
|
404
|
-
|
|
464
|
+
errors.push({ rule_id: rule.rule_id, error: message });
|
|
465
|
+
rule_results.push({ rule_id: rule.rule_id, issues: [], raw_response: message, check_type: rule.check_type });
|
|
405
466
|
}
|
|
406
467
|
}
|
|
407
|
-
return Response.json({
|
|
408
|
-
success: batch_errors.length === 0,
|
|
409
|
-
clarifications: batch_clarifications,
|
|
410
|
-
rule_results: batch_rule_results,
|
|
411
|
-
...(batch_errors.length > 0 ? { errors: batch_errors } : {}),
|
|
412
|
-
});
|
|
413
468
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
469
|
+
else if (is_text_mode) {
|
|
470
|
+
// Text mode
|
|
471
|
+
input = { kind: 'text', text_content: text_content, file_name };
|
|
472
|
+
const text_variables = {
|
|
473
|
+
...variables,
|
|
474
|
+
document_name: file_name || 'text input',
|
|
475
|
+
...(content_tag ? { document_type: content_tag } : {}),
|
|
476
|
+
};
|
|
477
|
+
for (const rule of rules) {
|
|
478
|
+
try {
|
|
479
|
+
const result = await run_rule(rule, input, text_variables, wrappers);
|
|
480
|
+
rule_results.push(result);
|
|
481
|
+
if (result.issues.length > 0 && rule.clarification_type !== 'none') {
|
|
482
|
+
clarifications.push(create_clarification_from_result(rule, result, { file_name: file_name || 'text input', mime_type: 'text/plain' }));
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
catch (err) {
|
|
486
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
487
|
+
on_error(err, `rule ${rule.rule_id}: ${rule.name}`);
|
|
488
|
+
errors.push({ rule_id: rule.rule_id, error: message });
|
|
489
|
+
rule_results.push({ rule_id: rule.rule_id, issues: [], raw_response: message, check_type: rule.check_type });
|
|
490
|
+
}
|
|
491
|
+
}
|
|
419
492
|
}
|
|
420
|
-
else
|
|
493
|
+
else {
|
|
494
|
+
// Single document
|
|
495
|
+
let file_b64;
|
|
421
496
|
try {
|
|
422
|
-
|
|
423
|
-
? download_url
|
|
424
|
-
: `${new URL(request.url).origin}${download_url}`;
|
|
425
|
-
const file_response = await fetch(absolute_url);
|
|
426
|
-
if (!file_response.ok) {
|
|
427
|
-
throw new Error(`Failed to fetch file: ${file_response.status}`);
|
|
428
|
-
}
|
|
429
|
-
const buffer = Buffer.from(await file_response.arrayBuffer());
|
|
430
|
-
file_b64 = buffer.toString('base64');
|
|
497
|
+
file_b64 = await fetch_file_b64(request.url, { file_b64: request_file_b64, download_url, file_name });
|
|
431
498
|
}
|
|
432
499
|
catch (err) {
|
|
433
500
|
return Response.json({
|
|
434
501
|
success: false,
|
|
435
502
|
clarifications: [],
|
|
436
503
|
rule_results: [],
|
|
437
|
-
errors: [{ rule_id: '', error:
|
|
438
|
-
}, { status: 500 });
|
|
504
|
+
errors: [{ rule_id: '', error: err instanceof Error ? err.message : 'Unknown error' }],
|
|
505
|
+
}, { status: err instanceof Error && err.message.includes('No file_b64') ? 400 : 500 });
|
|
439
506
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
for (const rule of rules) {
|
|
460
|
-
try {
|
|
461
|
-
const result = await execute_rule(rule, file_b64, mime_type, all_variables);
|
|
462
|
-
rule_results.push(result);
|
|
463
|
-
if (result.has_issue && rule.clarification_type !== 'none') {
|
|
464
|
-
const clarification = create_clarification_from_result(rule, result, { file_name, mime_type });
|
|
465
|
-
clarifications.push(clarification);
|
|
507
|
+
input = { kind: 'document', file_name, mime_type, file_b64 };
|
|
508
|
+
const doc_variables = {
|
|
509
|
+
...variables,
|
|
510
|
+
document_name: file_name || 'document',
|
|
511
|
+
...(content_tag ? { document_type: content_tag } : {}),
|
|
512
|
+
};
|
|
513
|
+
for (const rule of rules) {
|
|
514
|
+
try {
|
|
515
|
+
const result = await run_rule(rule, input, doc_variables, wrappers);
|
|
516
|
+
rule_results.push(result);
|
|
517
|
+
if (result.issues.length > 0 && rule.clarification_type !== 'none') {
|
|
518
|
+
clarifications.push(create_clarification_from_result(rule, result, { file_name, mime_type }));
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
catch (err) {
|
|
522
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
523
|
+
on_error(err, `rule ${rule.rule_id}: ${rule.name}`);
|
|
524
|
+
errors.push({ rule_id: rule.rule_id, error: message });
|
|
525
|
+
rule_results.push({ rule_id: rule.rule_id, issues: [], raw_response: message, check_type: rule.check_type });
|
|
466
526
|
}
|
|
467
|
-
}
|
|
468
|
-
catch (err) {
|
|
469
|
-
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
470
|
-
on_error(err, `rule ${rule.rule_id}: ${rule.name}`);
|
|
471
|
-
errors.push({ rule_id: rule.rule_id, error: message });
|
|
472
|
-
rule_results.push({
|
|
473
|
-
rule_id: rule.rule_id,
|
|
474
|
-
has_issue: false,
|
|
475
|
-
raw_response: message,
|
|
476
|
-
});
|
|
477
527
|
}
|
|
478
528
|
}
|
|
479
529
|
const response = {
|