@claritylabs/cl-sdk 0.3.1 → 0.6.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/dist/index.js CHANGED
@@ -24,9 +24,16 @@ __export(index_exports, {
24
24
  AGENT_TOOLS: () => AGENT_TOOLS,
25
25
  APPLICATION_CLASSIFY_PROMPT: () => APPLICATION_CLASSIFY_PROMPT,
26
26
  AUDIT_TYPES: () => AUDIT_TYPES,
27
+ AcroFormMappingSchema: () => AcroFormMappingSchema,
27
28
  AddressSchema: () => AddressSchema,
28
29
  AdmittedStatusSchema: () => AdmittedStatusSchema,
30
+ AnswerParsingResultSchema: () => AnswerParsingResultSchema,
31
+ ApplicationClassifyResultSchema: () => ApplicationClassifyResultSchema,
32
+ ApplicationFieldSchema: () => ApplicationFieldSchema,
33
+ ApplicationStateSchema: () => ApplicationStateSchema,
29
34
  AuditTypeSchema: () => AuditTypeSchema,
35
+ AutoFillMatchSchema: () => AutoFillMatchSchema,
36
+ AutoFillResultSchema: () => AutoFillResultSchema,
30
37
  BOAT_TYPES: () => BOAT_TYPES,
31
38
  BindingAuthoritySchema: () => BindingAuthoritySchema,
32
39
  BoatTypeSchema: () => BoatTypeSchema,
@@ -40,6 +47,7 @@ __export(index_exports, {
40
47
  COVERAGE_FORMS: () => COVERAGE_FORMS,
41
48
  COVERAGE_TRIGGERS: () => COVERAGE_TRIGGERS,
42
49
  ChunkTypeSchema: () => ChunkTypeSchema,
50
+ CitationSchema: () => CitationSchema,
43
51
  ClaimRecordSchema: () => ClaimRecordSchema,
44
52
  ClaimStatusSchema: () => ClaimStatusSchema,
45
53
  ClassificationCodeSchema: () => ClassificationCodeSchema,
@@ -82,12 +90,16 @@ __export(index_exports, {
82
90
  EnrichedSubjectivitySchema: () => EnrichedSubjectivitySchema,
83
91
  EnrichedUnderwritingConditionSchema: () => EnrichedUnderwritingConditionSchema,
84
92
  EntityTypeSchema: () => EntityTypeSchema,
93
+ EvidenceItemSchema: () => EvidenceItemSchema,
85
94
  ExclusionSchema: () => ExclusionSchema,
86
95
  ExperienceModSchema: () => ExperienceModSchema,
87
96
  ExtendedReportingPeriodSchema: () => ExtendedReportingPeriodSchema,
88
97
  FLOOD_ZONES: () => FLOOD_ZONES,
89
98
  FOUNDATION_TYPES: () => FOUNDATION_TYPES,
90
99
  FarmRanchDeclarationsSchema: () => FarmRanchDeclarationsSchema,
100
+ FieldExtractionResultSchema: () => FieldExtractionResultSchema,
101
+ FieldTypeSchema: () => FieldTypeSchema,
102
+ FlatPdfPlacementSchema: () => FlatPdfPlacementSchema,
91
103
  FloodDeclarationsSchema: () => FloodDeclarationsSchema,
92
104
  FloodZoneSchema: () => FloodZoneSchema,
93
105
  FormReferenceSchema: () => FormReferenceSchema,
@@ -106,6 +118,9 @@ __export(index_exports, {
106
118
  LimitScheduleSchema: () => LimitScheduleSchema,
107
119
  LimitTypeSchema: () => LimitTypeSchema,
108
120
  LocationPremiumSchema: () => LocationPremiumSchema,
121
+ LookupFillResultSchema: () => LookupFillResultSchema,
122
+ LookupFillSchema: () => LookupFillSchema,
123
+ LookupRequestSchema: () => LookupRequestSchema,
109
124
  LossSettlementSchema: () => LossSettlementSchema,
110
125
  LossSummarySchema: () => LossSummarySchema,
111
126
  NamedInsuredSchema: () => NamedInsuredSchema,
@@ -115,6 +130,7 @@ __export(index_exports, {
115
130
  POLICY_SECTION_TYPES: () => POLICY_SECTION_TYPES,
116
131
  POLICY_TERM_TYPES: () => POLICY_TERM_TYPES,
117
132
  POLICY_TYPES: () => POLICY_TYPES,
133
+ ParsedAnswerSchema: () => ParsedAnswerSchema,
118
134
  PaymentInstallmentSchema: () => PaymentInstallmentSchema,
119
135
  PaymentPlanSchema: () => PaymentPlanSchema,
120
136
  PersonalArticlesDeclarationsSchema: () => PersonalArticlesDeclarationsSchema,
@@ -134,6 +150,10 @@ __export(index_exports, {
134
150
  ProducerInfoSchema: () => ProducerInfoSchema,
135
151
  ProfessionalLiabilityDeclarationsSchema: () => ProfessionalLiabilityDeclarationsSchema,
136
152
  QUOTE_SECTION_TYPES: () => QUOTE_SECTION_TYPES,
153
+ QueryClassifyResultSchema: () => QueryClassifyResultSchema,
154
+ QueryIntentSchema: () => QueryIntentSchema,
155
+ QueryResultSchema: () => QueryResultSchema,
156
+ QuestionBatchResultSchema: () => QuestionBatchResultSchema,
137
157
  QuoteDocumentSchema: () => QuoteDocumentSchema,
138
158
  QuoteSectionTypeSchema: () => QuoteSectionTypeSchema,
139
159
  RATING_BASIS_TYPES: () => RATING_BASIS_TYPES,
@@ -143,12 +163,16 @@ __export(index_exports, {
143
163
  RatingBasisSchema: () => RatingBasisSchema,
144
164
  RatingBasisTypeSchema: () => RatingBasisTypeSchema,
145
165
  RecreationalVehicleDeclarationsSchema: () => RecreationalVehicleDeclarationsSchema,
166
+ ReplyIntentSchema: () => ReplyIntentSchema,
167
+ RetrievalResultSchema: () => RetrievalResultSchema,
146
168
  RoofTypeSchema: () => RoofTypeSchema,
147
169
  SCHEDULED_ITEM_CATEGORIES: () => SCHEDULED_ITEM_CATEGORIES,
148
170
  SUBJECTIVITY_CATEGORIES: () => SUBJECTIVITY_CATEGORIES,
149
171
  ScheduledItemCategorySchema: () => ScheduledItemCategorySchema,
150
172
  SectionSchema: () => SectionSchema,
151
173
  SharedLimitSchema: () => SharedLimitSchema,
174
+ SubAnswerSchema: () => SubAnswerSchema,
175
+ SubQuestionSchema: () => SubQuestionSchema,
152
176
  SubjectivityCategorySchema: () => SubjectivityCategorySchema,
153
177
  SubjectivitySchema: () => SubjectivitySchema,
154
178
  SublimitSchema: () => SublimitSchema,
@@ -165,6 +189,7 @@ __export(index_exports, {
165
189
  ValuationMethodSchema: () => ValuationMethodSchema,
166
190
  VehicleCoverageSchema: () => VehicleCoverageSchema,
167
191
  VehicleCoverageTypeSchema: () => VehicleCoverageTypeSchema,
192
+ VerifyResultSchema: () => VerifyResultSchema,
168
193
  WatercraftDeclarationsSchema: () => WatercraftDeclarationsSchema,
169
194
  WorkersCompDeclarationsSchema: () => WorkersCompDeclarationsSchema,
170
195
  buildAcroFormMappingPrompt: () => buildAcroFormMappingPrompt,
@@ -184,12 +209,18 @@ __export(index_exports, {
184
209
  buildIdentityPrompt: () => buildIdentityPrompt,
185
210
  buildIntentPrompt: () => buildIntentPrompt,
186
211
  buildLookupFillPrompt: () => buildLookupFillPrompt,
212
+ buildQueryClassifyPrompt: () => buildQueryClassifyPrompt,
187
213
  buildQuestionBatchPrompt: () => buildQuestionBatchPrompt,
188
214
  buildQuotesPoliciesPrompt: () => buildQuotesPoliciesPrompt,
215
+ buildReasonPrompt: () => buildReasonPrompt,
189
216
  buildReplyIntentClassificationPrompt: () => buildReplyIntentClassificationPrompt,
217
+ buildRespondPrompt: () => buildRespondPrompt,
190
218
  buildSafetyPrompt: () => buildSafetyPrompt,
219
+ buildVerifyPrompt: () => buildVerifyPrompt,
191
220
  chunkDocument: () => chunkDocument,
221
+ createApplicationPipeline: () => createApplicationPipeline,
192
222
  createExtractor: () => createExtractor,
223
+ createQueryAgent: () => createQueryAgent,
193
224
  extractPageRange: () => extractPageRange,
194
225
  fillAcroForm: () => fillAcroForm,
195
226
  getAcroFormFields: () => getAcroFormFields,
@@ -1669,6 +1700,206 @@ function assembleDocument(documentId, documentType, memory) {
1669
1700
  };
1670
1701
  }
1671
1702
 
1703
+ // src/prompts/coordinator/format.ts
1704
+ function buildFormatPrompt(entries) {
1705
+ const block = entries.map((e) => `===ENTRY ${e.id}===
1706
+ ${e.text}`).join("\n\n");
1707
+ return `You are a markdown formatting specialist for insurance document content. You will receive numbered content entries extracted from insurance policies, quotes, and endorsements. Your job is to clean up the formatting so every entry renders correctly as standard markdown.
1708
+
1709
+ ## Primary issues to fix
1710
+
1711
+ ### 1. Pipe-delimited data missing table syntax
1712
+ The most common issue. Content uses pipe characters as column separators but is missing the separator row required for markdown table rendering.
1713
+
1714
+ Before (broken \u2014 won't render as a table):
1715
+ COVERAGE | FORM # | LIMIT | DEDUCTIBLE
1716
+ Employee Theft | | $10,000 | $1,000
1717
+
1718
+ After (valid markdown table):
1719
+ | COVERAGE | FORM # | LIMIT | DEDUCTIBLE |
1720
+ | --- | --- | --- | --- |
1721
+ | Employee Theft | | $10,000 | $1,000 |
1722
+
1723
+ Rules for pipe tables:
1724
+ - Add leading and trailing pipes to every row
1725
+ - Add the separator row (| --- | --- |) after the header row
1726
+ - Every row must have the same number of pipe-separated columns as the header
1727
+ - Empty cells are fine \u2014 just keep the pipes: | | $10,000 |
1728
+
1729
+ ### 2. Sub-items indented within pipe tables
1730
+ Insurance schedules often have indented sub-items that belong to the previous coverage line. These break table column counts.
1731
+
1732
+ Before (broken):
1733
+ COVERAGE | LIMIT | DEDUCTIBLE
1734
+ Causes Of Loss - Equipment Breakdown | PR650END
1735
+ Described Premises Limit | | $350,804 |
1736
+ Diagnostic Equipment | | $100,000 |
1737
+ Deductible Type - Business Income: Waiting Period - Hours
1738
+ Waiting Period (Hours): 24
1739
+
1740
+ After: Pull sub-items out of the table. End the table before the sub-items, show them as an indented list, then start a new table if tabular data resumes:
1741
+ | COVERAGE | LIMIT | DEDUCTIBLE |
1742
+ | --- | --- | --- |
1743
+ | Causes Of Loss - Equipment Breakdown | PR650END | |
1744
+
1745
+ - Described Premises Limit: $350,804
1746
+ - Diagnostic Equipment: $100,000
1747
+ - Deductible Type - Business Income: Waiting Period - Hours
1748
+ - Waiting Period (Hours): 24
1749
+
1750
+ ### 3. Space-aligned tables
1751
+ Declarations often align columns with spaces instead of pipes. These render as plain monospace text and lose structure.
1752
+
1753
+ Before:
1754
+ Coverage Limit of Liability Retention
1755
+ A. Network Security Liability $500,000 $10,000
1756
+ B. Privacy Liability $500,000 $10,000
1757
+
1758
+ After (convert to proper markdown table):
1759
+ | Coverage | Limit of Liability | Retention |
1760
+ | --- | --- | --- |
1761
+ | A. Network Security Liability | $500,000 | $10,000 |
1762
+ | B. Privacy Liability | $500,000 | $10,000 |
1763
+
1764
+ ### 4. Mixed table/prose content
1765
+ A single entry often contains prose paragraphs followed by tabular data followed by more prose. Handle each segment independently \u2014 don't try to force everything into one table.
1766
+
1767
+ ### 5. General markdown cleanup
1768
+ - **Line spacing**: Remove excessive blank lines (3+ consecutive newlines \u2192 2). Ensure one blank line before and after tables and headings.
1769
+ - **Trailing whitespace**: Remove trailing spaces on all lines.
1770
+ - **Broken lists**: Ensure list items use consistent markers (-, *, or 1.) with proper nesting indentation.
1771
+ - **Orphaned formatting**: Close any unclosed bold (**), italic (*), or code (\`) markers.
1772
+ - **Heading levels**: Ensure heading markers (##) have a space after the hashes.
1773
+
1774
+ ## Rules
1775
+ - Do NOT change the meaning or substance of any content. Only fix formatting.
1776
+ - Do NOT add new information, headers, or commentary.
1777
+ - Do NOT wrap entries in code fences.
1778
+ - Preserve all dollar amounts, dates, policy numbers, form numbers, and technical terms exactly as they appear.
1779
+ - If an entry is already well-formatted, return it unchanged.
1780
+ - When in doubt about whether something is a table, prefer table formatting for structured data with multiple columns.
1781
+
1782
+ Return your output in this exact format \u2014 one block per entry, in the same order:
1783
+
1784
+ ===ENTRY 0===
1785
+ (cleaned content for entry 0)
1786
+
1787
+ ===ENTRY 1===
1788
+ (cleaned content for entry 1)
1789
+
1790
+ ...and so on for each entry.
1791
+
1792
+ Here are the entries to format:
1793
+
1794
+ ${block}`;
1795
+ }
1796
+
1797
+ // src/extraction/formatter.ts
1798
+ function collectContentFields(doc) {
1799
+ const entries = [];
1800
+ let id = 0;
1801
+ function add(path, text) {
1802
+ if (text && text.length > 20) {
1803
+ entries.push({ id: id++, path, text });
1804
+ }
1805
+ }
1806
+ add("summary", doc.summary);
1807
+ if (doc.sections) {
1808
+ for (let i = 0; i < doc.sections.length; i++) {
1809
+ const s = doc.sections[i];
1810
+ add(`sections[${i}].content`, s.content);
1811
+ if (s.subsections) {
1812
+ for (let j = 0; j < s.subsections.length; j++) {
1813
+ add(`sections[${i}].subsections[${j}].content`, s.subsections[j].content);
1814
+ }
1815
+ }
1816
+ }
1817
+ }
1818
+ if (doc.endorsements) {
1819
+ for (let i = 0; i < doc.endorsements.length; i++) {
1820
+ add(`endorsements[${i}].content`, doc.endorsements[i].content);
1821
+ }
1822
+ }
1823
+ if (doc.exclusions) {
1824
+ for (let i = 0; i < doc.exclusions.length; i++) {
1825
+ add(`exclusions[${i}].content`, doc.exclusions[i].content);
1826
+ }
1827
+ }
1828
+ if (doc.conditions) {
1829
+ for (let i = 0; i < doc.conditions.length; i++) {
1830
+ add(`conditions[${i}].content`, doc.conditions[i].content);
1831
+ }
1832
+ }
1833
+ return entries;
1834
+ }
1835
+ function parseFormatResponse(response) {
1836
+ const results = /* @__PURE__ */ new Map();
1837
+ const parts = response.split(/===ENTRY (\d+)===/);
1838
+ for (let i = 1; i < parts.length; i += 2) {
1839
+ const entryId = parseInt(parts[i], 10);
1840
+ const content = parts[i + 1]?.trim();
1841
+ if (!isNaN(entryId) && content !== void 0) {
1842
+ results.set(entryId, content);
1843
+ }
1844
+ }
1845
+ return results;
1846
+ }
1847
+ function applyFormattedContent(doc, entries, formatted) {
1848
+ for (const entry of entries) {
1849
+ const cleaned = formatted.get(entry.id);
1850
+ if (!cleaned) continue;
1851
+ const segments = entry.path.match(/^(\w+)(?:\[(\d+)\])?(?:\.(\w+)(?:\[(\d+)\])?(?:\.(\w+))?)?$/);
1852
+ if (!segments) continue;
1853
+ const [, field, idx1, sub1, idx2, sub2] = segments;
1854
+ if (!sub1) {
1855
+ doc[field] = cleaned;
1856
+ } else if (!sub2) {
1857
+ const arr = doc[field];
1858
+ if (arr && arr[Number(idx1)]) {
1859
+ arr[Number(idx1)][sub1] = cleaned;
1860
+ }
1861
+ } else {
1862
+ const arr = doc[field];
1863
+ if (arr && arr[Number(idx1)]) {
1864
+ const nested = arr[Number(idx1)][sub1];
1865
+ if (nested && nested[Number(idx2)]) {
1866
+ nested[Number(idx2)][sub2] = cleaned;
1867
+ }
1868
+ }
1869
+ }
1870
+ }
1871
+ }
1872
+ var MAX_ENTRIES_PER_BATCH = 20;
1873
+ async function formatDocumentContent(doc, generateText, options) {
1874
+ const entries = collectContentFields(doc);
1875
+ const totalUsage = { inputTokens: 0, outputTokens: 0 };
1876
+ if (entries.length === 0) {
1877
+ return { document: doc, usage: totalUsage };
1878
+ }
1879
+ options?.onProgress?.(`Formatting ${entries.length} content fields...`);
1880
+ const batches = [];
1881
+ for (let i = 0; i < entries.length; i += MAX_ENTRIES_PER_BATCH) {
1882
+ batches.push(entries.slice(i, i + MAX_ENTRIES_PER_BATCH));
1883
+ }
1884
+ for (const batch of batches) {
1885
+ const prompt = buildFormatPrompt(batch.map((e) => ({ id: e.id, text: e.text })));
1886
+ const result = await withRetry(
1887
+ () => generateText({
1888
+ prompt,
1889
+ maxTokens: 16384,
1890
+ providerOptions: options?.providerOptions
1891
+ })
1892
+ );
1893
+ if (result.usage) {
1894
+ totalUsage.inputTokens += result.usage.inputTokens;
1895
+ totalUsage.outputTokens += result.usage.outputTokens;
1896
+ }
1897
+ const formatted = parseFormatResponse(result.text);
1898
+ applyFormattedContent(doc, batch, formatted);
1899
+ }
1900
+ return { document: doc, usage: totalUsage };
1901
+ }
1902
+
1672
1903
  // src/extraction/chunking.ts
1673
1904
  function chunkDocument(doc) {
1674
1905
  const chunks = [];
@@ -3164,8 +3395,14 @@ function createExtractor(config) {
3164
3395
  }
3165
3396
  onProgress?.("Assembling document...");
3166
3397
  const document = assembleDocument(id, documentType, memory);
3167
- const chunks = chunkDocument(document);
3168
- return { document, chunks, tokenUsage: totalUsage };
3398
+ onProgress?.("Formatting extracted content...");
3399
+ const formatResult = await formatDocumentContent(document, generateText, {
3400
+ providerOptions,
3401
+ onProgress
3402
+ });
3403
+ trackUsage(formatResult.usage);
3404
+ const chunks = chunkDocument(formatResult.document);
3405
+ return { document: formatResult.document, chunks, tokenUsage: totalUsage };
3169
3406
  }
3170
3407
  return { extract };
3171
3408
  }
@@ -3383,6 +3620,129 @@ Respond with JSON only:
3383
3620
  "applicationType": string | null // e.g. "General Liability", "Professional Liability", "Commercial Property", "Workers Compensation", "ACORD 125", etc.
3384
3621
  }`;
3385
3622
 
3623
+ // src/schemas/application.ts
3624
+ var import_zod31 = require("zod");
3625
+ var FieldTypeSchema = import_zod31.z.enum([
3626
+ "text",
3627
+ "numeric",
3628
+ "currency",
3629
+ "date",
3630
+ "yes_no",
3631
+ "table",
3632
+ "declaration"
3633
+ ]);
3634
+ var ApplicationFieldSchema = import_zod31.z.object({
3635
+ id: import_zod31.z.string(),
3636
+ label: import_zod31.z.string(),
3637
+ section: import_zod31.z.string(),
3638
+ fieldType: FieldTypeSchema,
3639
+ required: import_zod31.z.boolean(),
3640
+ options: import_zod31.z.array(import_zod31.z.string()).optional(),
3641
+ columns: import_zod31.z.array(import_zod31.z.string()).optional(),
3642
+ requiresExplanationIfYes: import_zod31.z.boolean().optional(),
3643
+ condition: import_zod31.z.object({
3644
+ dependsOn: import_zod31.z.string(),
3645
+ whenValue: import_zod31.z.string()
3646
+ }).optional(),
3647
+ value: import_zod31.z.string().optional(),
3648
+ source: import_zod31.z.string().optional().describe("Where the value came from: auto-fill, user, lookup"),
3649
+ confidence: import_zod31.z.enum(["confirmed", "high", "medium", "low"]).optional()
3650
+ });
3651
+ var ApplicationClassifyResultSchema = import_zod31.z.object({
3652
+ isApplication: import_zod31.z.boolean(),
3653
+ confidence: import_zod31.z.number().min(0).max(1),
3654
+ applicationType: import_zod31.z.string().nullable()
3655
+ });
3656
+ var FieldExtractionResultSchema = import_zod31.z.object({
3657
+ fields: import_zod31.z.array(ApplicationFieldSchema)
3658
+ });
3659
+ var AutoFillMatchSchema = import_zod31.z.object({
3660
+ fieldId: import_zod31.z.string(),
3661
+ value: import_zod31.z.string(),
3662
+ confidence: import_zod31.z.enum(["confirmed"]),
3663
+ contextKey: import_zod31.z.string()
3664
+ });
3665
+ var AutoFillResultSchema = import_zod31.z.object({
3666
+ matches: import_zod31.z.array(AutoFillMatchSchema)
3667
+ });
3668
+ var QuestionBatchResultSchema = import_zod31.z.object({
3669
+ batches: import_zod31.z.array(import_zod31.z.array(import_zod31.z.string()).describe("Array of field IDs in this batch"))
3670
+ });
3671
+ var LookupRequestSchema = import_zod31.z.object({
3672
+ type: import_zod31.z.string().describe("Type of lookup: 'records', 'website', 'policy'"),
3673
+ description: import_zod31.z.string(),
3674
+ url: import_zod31.z.string().optional(),
3675
+ targetFieldIds: import_zod31.z.array(import_zod31.z.string())
3676
+ });
3677
+ var ReplyIntentSchema = import_zod31.z.object({
3678
+ primaryIntent: import_zod31.z.enum(["answers_only", "question", "lookup_request", "mixed"]),
3679
+ hasAnswers: import_zod31.z.boolean(),
3680
+ questionText: import_zod31.z.string().optional(),
3681
+ questionFieldIds: import_zod31.z.array(import_zod31.z.string()).optional(),
3682
+ lookupRequests: import_zod31.z.array(LookupRequestSchema).optional()
3683
+ });
3684
+ var ParsedAnswerSchema = import_zod31.z.object({
3685
+ fieldId: import_zod31.z.string(),
3686
+ value: import_zod31.z.string(),
3687
+ explanation: import_zod31.z.string().optional()
3688
+ });
3689
+ var AnswerParsingResultSchema = import_zod31.z.object({
3690
+ answers: import_zod31.z.array(ParsedAnswerSchema),
3691
+ unanswered: import_zod31.z.array(import_zod31.z.string()).describe("Field IDs that were not answered")
3692
+ });
3693
+ var LookupFillSchema = import_zod31.z.object({
3694
+ fieldId: import_zod31.z.string(),
3695
+ value: import_zod31.z.string(),
3696
+ source: import_zod31.z.string().describe("Specific citable reference, e.g. 'GL Policy #POL-12345 (Hartford)'")
3697
+ });
3698
+ var LookupFillResultSchema = import_zod31.z.object({
3699
+ fills: import_zod31.z.array(LookupFillSchema),
3700
+ unfillable: import_zod31.z.array(import_zod31.z.string()),
3701
+ explanation: import_zod31.z.string().optional()
3702
+ });
3703
+ var FlatPdfPlacementSchema = import_zod31.z.object({
3704
+ fieldId: import_zod31.z.string(),
3705
+ page: import_zod31.z.number(),
3706
+ x: import_zod31.z.number().describe("Percentage from left edge (0-100)"),
3707
+ y: import_zod31.z.number().describe("Percentage from top edge (0-100)"),
3708
+ text: import_zod31.z.string(),
3709
+ fontSize: import_zod31.z.number().optional(),
3710
+ isCheckmark: import_zod31.z.boolean().optional()
3711
+ });
3712
+ var AcroFormMappingSchema = import_zod31.z.object({
3713
+ fieldId: import_zod31.z.string(),
3714
+ acroFormName: import_zod31.z.string(),
3715
+ value: import_zod31.z.string()
3716
+ });
3717
+ var ApplicationStateSchema = import_zod31.z.object({
3718
+ id: import_zod31.z.string(),
3719
+ pdfBase64: import_zod31.z.string().optional().describe("Original PDF, omitted after extraction"),
3720
+ title: import_zod31.z.string().optional(),
3721
+ applicationType: import_zod31.z.string().nullable().optional(),
3722
+ fields: import_zod31.z.array(ApplicationFieldSchema),
3723
+ batches: import_zod31.z.array(import_zod31.z.array(import_zod31.z.string())).optional(),
3724
+ currentBatchIndex: import_zod31.z.number().default(0),
3725
+ status: import_zod31.z.enum(["classifying", "extracting", "auto_filling", "batching", "collecting", "confirming", "mapping", "complete"]),
3726
+ createdAt: import_zod31.z.number(),
3727
+ updatedAt: import_zod31.z.number()
3728
+ });
3729
+
3730
+ // src/application/agents/classifier.ts
3731
+ async function classifyApplication(pdfContent, generateObject, providerOptions) {
3732
+ const { object, usage } = await withRetry(
3733
+ () => generateObject({
3734
+ prompt: `${APPLICATION_CLASSIFY_PROMPT}
3735
+
3736
+ Analyze the following document content:
3737
+ ${pdfContent}`,
3738
+ schema: ApplicationClassifyResultSchema,
3739
+ maxTokens: 512,
3740
+ providerOptions
3741
+ })
3742
+ );
3743
+ return { result: object, usage };
3744
+ }
3745
+
3386
3746
  // src/prompts/application/field-extraction.ts
3387
3747
  function buildFieldExtractionPrompt() {
3388
3748
  return `Extract all fillable fields from this insurance application PDF as a JSON array. Be concise \u2014 use short IDs and minimal keys.
@@ -3415,6 +3775,24 @@ Example:
3415
3775
  Extract ALL fields. Respond with ONLY the JSON array, no other text.`;
3416
3776
  }
3417
3777
 
3778
+ // src/application/agents/field-extractor.ts
3779
+ async function extractFields(pdfContent, generateObject, providerOptions) {
3780
+ const prompt = `${buildFieldExtractionPrompt()}
3781
+
3782
+ Extract fields from this application:
3783
+ ${pdfContent}`;
3784
+ const { object, usage } = await withRetry(
3785
+ () => generateObject({
3786
+ prompt,
3787
+ schema: FieldExtractionResultSchema,
3788
+ maxTokens: 8192,
3789
+ providerOptions
3790
+ })
3791
+ );
3792
+ const result = object;
3793
+ return { fields: result.fields, usage };
3794
+ }
3795
+
3418
3796
  // src/prompts/application/auto-fill.ts
3419
3797
  function buildAutoFillPrompt(fields, orgContext) {
3420
3798
  const fieldList = fields.map((f) => `- ${f.id}: "${f.label}" (${f.fieldType}, section: ${f.section})`).join("\n");
@@ -3444,6 +3822,39 @@ Respond with JSON only:
3444
3822
  Only include fields you can confidently fill. Do not guess or fabricate values.`;
3445
3823
  }
3446
3824
 
3825
+ // src/application/agents/auto-filler.ts
3826
+ async function autoFillFromContext(fields, orgContext, generateObject, providerOptions) {
3827
+ const fieldSummaries = fields.map((f) => ({
3828
+ id: f.id,
3829
+ label: f.label,
3830
+ fieldType: f.fieldType,
3831
+ section: f.section
3832
+ }));
3833
+ const prompt = buildAutoFillPrompt(fieldSummaries, orgContext);
3834
+ const { object, usage } = await withRetry(
3835
+ () => generateObject({
3836
+ prompt,
3837
+ schema: AutoFillResultSchema,
3838
+ maxTokens: 4096,
3839
+ providerOptions
3840
+ })
3841
+ );
3842
+ return { result: object, usage };
3843
+ }
3844
+ async function backfillFromPriorAnswers(fields, backfillProvider) {
3845
+ const unfilled = fields.filter((f) => !f.value);
3846
+ if (unfilled.length === 0) return [];
3847
+ return backfillProvider.searchPriorAnswers(
3848
+ unfilled.map((f) => ({
3849
+ id: f.id,
3850
+ label: f.label,
3851
+ section: f.section,
3852
+ fieldType: f.fieldType
3853
+ })),
3854
+ { limit: unfilled.length * 2 }
3855
+ );
3856
+ }
3857
+
3447
3858
  // src/prompts/application/question-batch.ts
3448
3859
  function buildQuestionBatchPrompt(unfilledFields) {
3449
3860
  const fieldList = unfilledFields.map(
@@ -3478,120 +3889,27 @@ Respond with JSON only:
3478
3889
  }`;
3479
3890
  }
3480
3891
 
3481
- // src/prompts/application/answer-parsing.ts
3482
- function buildAnswerParsingPrompt(questions, emailBody) {
3483
- const questionList = questions.map(
3484
- (q, i) => `${i + 1}. ${q.id}: "${q.label ?? q.text}" (type: ${q.fieldType})`
3485
- ).join("\n");
3486
- return `You are parsing a user's email reply to extract answers for specific insurance application questions.
3487
-
3488
- QUESTIONS ASKED:
3489
- ${questionList}
3490
-
3491
- USER'S EMAIL REPLY:
3492
- ${emailBody}
3493
-
3494
- Extract answers for each question. Handle:
3495
- - Direct numbered answers (1. answer, 2. answer)
3496
- - Inline answers referencing the question
3497
- - Table data provided as lists or comma-separated values
3498
- - Yes/no answers with optional explanations
3499
- - Partial responses (some questions answered, others skipped)
3500
-
3501
- Respond with JSON only:
3502
- {
3503
- "answers": [
3504
- {
3505
- "fieldId": "company_name",
3506
- "value": "Acme Corp"
3507
- },
3508
- {
3509
- "fieldId": "prior_claims_decl",
3510
- "value": "yes",
3511
- "explanation": "One claim in 2024 for water damage, $15,000 paid"
3512
- }
3513
- ],
3514
- "unanswered": ["field_id_that_was_not_answered"]
3515
- }
3516
-
3517
- Only include answers you are confident about. If a response is ambiguous, include the field in "unanswered".`;
3518
- }
3519
-
3520
- // src/prompts/application/confirmation.ts
3521
- function buildConfirmationSummaryPrompt(fields, applicationTitle) {
3522
- const fieldList = fields.map((f) => {
3523
- const label = f.label ?? f.text ?? f.id;
3524
- const value = f.value ?? "(not provided)";
3525
- return `[${f.section}] ${label}: ${value}`;
3526
- }).join("\n");
3527
- return `Format the following insurance application answers into a clean, readable summary grouped by section. This will be sent as an email for the user to review and confirm.
3528
-
3529
- APPLICATION: ${applicationTitle}
3530
-
3531
- FIELD VALUES:
3532
- ${fieldList}
3533
-
3534
- Format as a readable summary:
3535
- - Group by section with section headers
3536
- - Show each field as "Label: Value"
3537
- - For declarations, show the question and the yes/no answer plus any explanation
3538
- - Skip fields with no value unless they are required
3539
- - End with a note asking the user to reply "Looks good" to confirm, or describe any changes needed
3540
-
3541
- Respond with the formatted summary text only (no JSON wrapper). Use markdown formatting (bold headers, bullet points).`;
3542
- }
3543
-
3544
- // src/prompts/application/batch-email.ts
3545
- function buildBatchEmailGenerationPrompt(batchFields, batchIndex, totalBatches, appTitle, totalFieldCount, filledFieldCount, previousBatchSummary, companyName) {
3546
- const nonConditionalFields = batchFields.filter((f) => !f.condition);
3547
- const conditionalFields = batchFields.filter((f) => f.condition);
3548
- const fieldList = nonConditionalFields.map((f, i) => {
3549
- let line = `${i + 1}. id="${f.id}" label="${f.label}" type=${f.fieldType}`;
3550
- if (f.options) line += ` options=[${f.options.join(", ")}]`;
3551
- return line;
3552
- }).join("\n");
3553
- const conditionalNote = conditionalFields.length > 0 ? `
3554
-
3555
- CONDITIONAL FIELDS (DO NOT include in this email \u2014 they will be asked as follow-ups in a separate email after the parent is answered):
3556
- ${conditionalFields.map((f) => `- id="${f.id}" label="${f.label}" depends on ${f.condition.dependsOn} = "${f.condition.whenValue}"`).join("\n")}` : "";
3557
- const company = companyName ?? "the company";
3558
- const remainingFields = totalFieldCount - filledFieldCount;
3559
- const estMinutes = Math.max(1, Math.round(remainingFields * 0.5));
3560
- return `You are an internal risk management assistant helping your colleague fill out an insurance application for ${company}. You work FOR ${company} \u2014 you are NOT the insurer, broker, or any external party.
3561
-
3562
- APPLICATION: ${appTitle ?? "Insurance Application"}
3563
- COMPANY: ${company}
3564
- PROGRESS: ${filledFieldCount} of ${totalFieldCount} fields done, ~${remainingFields} remaining (~${estMinutes} min of questions left)
3565
- ${previousBatchSummary ? `
3566
- PREVIOUS ANSWERS RECEIVED:
3567
- ${previousBatchSummary}
3568
- ` : ""}
3569
- FIELDS TO ASK ABOUT:
3570
- ${fieldList}${conditionalNote}
3571
-
3572
- Rules:
3573
- - ${previousBatchSummary ? 'Start by acknowledging previous answers or auto-filled data. If fields were auto-filled, list each field with its value AND cite the specific source (e.g. "from your GL Policy #ABC123", "from vercel.com", "from your business context"). If a web lookup was done, name the URL that was checked. Ask them to reply with corrections if anything is wrong.' : "Start with a one-line intro."}
3574
- - Mention progress once using estimated time remaining. Don't mention section/batch numbers or field counts.
3575
- - Use "${company}" by name when referring to the company. Also fine: "we" or "our". Never "our company" or "the company".
3576
- - Ask questions plainly. No em-dashes for dramatic effect, no filler phrases like "need to nail down" or "let's dive into". Just ask.
3577
- - For yes/no questions, ask naturally in one sentence. Don't list "Yes / No" as options. Mention what you'll need if the answer triggers a follow-up (e.g. "If not, I'll need a brief explanation.").
3578
- - For fields with 2-3 options, mention them inline. 4+ options can be a short list.
3579
- - Group related fields (address, coverage limits) into single compound questions.
3580
- - Do NOT include conditional/follow-up fields. They will be sent separately.
3581
- - Number each question.
3582
- - Note expected format where relevant: dollar amounts for currency, MM/DD/YYYY for dates, column descriptions for tables.
3583
- - End with a short closing.
3584
- - Tone: professional, brief, matter-of-fact. Write like a busy coworker, not a chatbot. No flourishes, no em-dashes between clauses, no editorializing about the questions.
3585
-
3586
- NEVER:
3587
- - Sound like a salesperson or customer service agent
3588
- - Use em-dashes for emphasis or dramatic pacing
3589
- - Editorialize ("these two should wrap up this section", "just a couple more")
3590
- - List "Yes / No / N/A" as bullet options
3591
- - Include conditional follow-up questions
3592
- - Mention section numbers, batch numbers, or field counts
3593
-
3594
- Output the email body text ONLY. No subject line, no JSON. Use markdown for numbered lists.`;
3892
+ // src/application/agents/batcher.ts
3893
+ async function batchQuestions(unfilledFields, generateObject, providerOptions) {
3894
+ const fieldSummaries = unfilledFields.map((f) => ({
3895
+ id: f.id,
3896
+ label: f.label,
3897
+ text: f.label,
3898
+ fieldType: f.fieldType,
3899
+ section: f.section,
3900
+ required: f.required,
3901
+ condition: f.condition
3902
+ }));
3903
+ const prompt = buildQuestionBatchPrompt(fieldSummaries);
3904
+ const { object, usage } = await withRetry(
3905
+ () => generateObject({
3906
+ prompt,
3907
+ schema: QuestionBatchResultSchema,
3908
+ maxTokens: 2048,
3909
+ providerOptions
3910
+ })
3911
+ );
3912
+ return { result: object, usage };
3595
3913
  }
3596
3914
 
3597
3915
  // src/prompts/application/reply-intent.ts
@@ -3630,23 +3948,78 @@ Respond with JSON only:
3630
3948
  }`;
3631
3949
  }
3632
3950
 
3633
- // src/prompts/application/field-explanation.ts
3634
- function buildFieldExplanationPrompt(field, question, policyContext) {
3635
- return `You are an internal risk management assistant helping a colleague fill out an insurance application for your company. They asked a question about a field on the form.
3951
+ // src/application/agents/reply-router.ts
3952
+ async function classifyReplyIntent(fields, replyText, generateObject, providerOptions) {
3953
+ const fieldSummaries = fields.map((f) => ({ id: f.id, label: f.label }));
3954
+ const prompt = buildReplyIntentClassificationPrompt(fieldSummaries, replyText);
3955
+ const { object, usage } = await withRetry(
3956
+ () => generateObject({
3957
+ prompt,
3958
+ schema: ReplyIntentSchema,
3959
+ maxTokens: 1024,
3960
+ providerOptions
3961
+ })
3962
+ );
3963
+ return { intent: object, usage };
3964
+ }
3636
3965
 
3637
- FIELD: "${field.label}" (type: ${field.fieldType}${field.options ? `, options: ${field.options.join(", ")}` : ""})
3966
+ // src/prompts/application/answer-parsing.ts
3967
+ function buildAnswerParsingPrompt(questions, emailBody) {
3968
+ const questionList = questions.map(
3969
+ (q, i) => `${i + 1}. ${q.id}: "${q.label ?? q.text}" (type: ${q.fieldType})`
3970
+ ).join("\n");
3971
+ return `You are parsing a user's email reply to extract answers for specific insurance application questions.
3638
3972
 
3639
- THEIR QUESTION: "${question}"
3973
+ QUESTIONS ASKED:
3974
+ ${questionList}
3640
3975
 
3641
- ${policyContext ? `RELEVANT POLICY/CONTEXT INFO:
3642
- ${policyContext}
3643
- ` : ""}
3976
+ USER'S EMAIL REPLY:
3977
+ ${emailBody}
3644
3978
 
3645
- Provide a short, helpful explanation (2-3 sentences) as a coworker would. If the field has options, briefly explain what each means if relevant. If there's policy context that helps, cite the specific source (e.g. "According to our GL Policy #ABC123 with Hartford, our current aggregate limit is $2M").
3979
+ Extract answers for each question. Handle:
3980
+ - Direct numbered answers (1. answer, 2. answer)
3981
+ - Inline answers referencing the question
3982
+ - Table data provided as lists or comma-separated values
3983
+ - Yes/no answers with optional explanations
3984
+ - Partial responses (some questions answered, others skipped)
3646
3985
 
3647
- End with: "Just reply with the answer when you're ready and I'll fill it in."
3986
+ Respond with JSON only:
3987
+ {
3988
+ "answers": [
3989
+ {
3990
+ "fieldId": "company_name",
3991
+ "value": "Acme Corp"
3992
+ },
3993
+ {
3994
+ "fieldId": "prior_claims_decl",
3995
+ "value": "yes",
3996
+ "explanation": "One claim in 2024 for water damage, $15,000 paid"
3997
+ }
3998
+ ],
3999
+ "unanswered": ["field_id_that_was_not_answered"]
4000
+ }
3648
4001
 
3649
- Respond with the explanation text only \u2014 no JSON, no field ID, no extra formatting.`;
4002
+ Only include answers you are confident about. If a response is ambiguous, include the field in "unanswered".`;
4003
+ }
4004
+
4005
+ // src/application/agents/answer-parser.ts
4006
+ async function parseAnswers(fields, replyText, generateObject, providerOptions) {
4007
+ const questions = fields.map((f) => ({
4008
+ id: f.id,
4009
+ label: f.label,
4010
+ text: f.label,
4011
+ fieldType: f.fieldType
4012
+ }));
4013
+ const prompt = buildAnswerParsingPrompt(questions, replyText);
4014
+ const { object, usage } = await withRetry(
4015
+ () => generateObject({
4016
+ prompt,
4017
+ schema: AnswerParsingResultSchema,
4018
+ maxTokens: 4096,
4019
+ providerOptions
4020
+ })
4021
+ );
4022
+ return { result: object, usage };
3650
4023
  }
3651
4024
 
3652
4025
  // src/prompts/application/pdf-mapping.ts
@@ -3753,6 +4126,1122 @@ Respond with JSON only:
3753
4126
  }`;
3754
4127
  }
3755
4128
 
4129
+ // src/application/agents/lookup-filler.ts
4130
+ async function fillFromLookup(requests, targetFields, availableData, generateObject, providerOptions) {
4131
+ const requestSummaries = requests.map((r) => ({
4132
+ type: r.type,
4133
+ description: r.description,
4134
+ targetFieldIds: r.targetFieldIds
4135
+ }));
4136
+ const fieldSummaries = targetFields.map((f) => ({
4137
+ id: f.id,
4138
+ label: f.label,
4139
+ fieldType: f.fieldType
4140
+ }));
4141
+ const prompt = buildLookupFillPrompt(requestSummaries, fieldSummaries, availableData);
4142
+ const { object, usage } = await withRetry(
4143
+ () => generateObject({
4144
+ prompt,
4145
+ schema: LookupFillResultSchema,
4146
+ maxTokens: 4096,
4147
+ providerOptions
4148
+ })
4149
+ );
4150
+ return { result: object, usage };
4151
+ }
4152
+
4153
+ // src/prompts/application/batch-email.ts
4154
+ function buildBatchEmailGenerationPrompt(batchFields, batchIndex, totalBatches, appTitle, totalFieldCount, filledFieldCount, previousBatchSummary, companyName) {
4155
+ const nonConditionalFields = batchFields.filter((f) => !f.condition);
4156
+ const conditionalFields = batchFields.filter((f) => f.condition);
4157
+ const fieldList = nonConditionalFields.map((f, i) => {
4158
+ let line = `${i + 1}. id="${f.id}" label="${f.label}" type=${f.fieldType}`;
4159
+ if (f.options) line += ` options=[${f.options.join(", ")}]`;
4160
+ return line;
4161
+ }).join("\n");
4162
+ const conditionalNote = conditionalFields.length > 0 ? `
4163
+
4164
+ CONDITIONAL FIELDS (DO NOT include in this email \u2014 they will be asked as follow-ups in a separate email after the parent is answered):
4165
+ ${conditionalFields.map((f) => `- id="${f.id}" label="${f.label}" depends on ${f.condition.dependsOn} = "${f.condition.whenValue}"`).join("\n")}` : "";
4166
+ const company = companyName ?? "the company";
4167
+ const remainingFields = totalFieldCount - filledFieldCount;
4168
+ const estMinutes = Math.max(1, Math.round(remainingFields * 0.5));
4169
+ return `You are an internal risk management assistant helping your colleague fill out an insurance application for ${company}. You work FOR ${company} \u2014 you are NOT the insurer, broker, or any external party.
4170
+
4171
+ APPLICATION: ${appTitle ?? "Insurance Application"}
4172
+ COMPANY: ${company}
4173
+ PROGRESS: ${filledFieldCount} of ${totalFieldCount} fields done, ~${remainingFields} remaining (~${estMinutes} min of questions left)
4174
+ ${previousBatchSummary ? `
4175
+ PREVIOUS ANSWERS RECEIVED:
4176
+ ${previousBatchSummary}
4177
+ ` : ""}
4178
+ FIELDS TO ASK ABOUT:
4179
+ ${fieldList}${conditionalNote}
4180
+
4181
+ Rules:
4182
+ - ${previousBatchSummary ? 'Start by acknowledging previous answers or auto-filled data. If fields were auto-filled, list each field with its value AND cite the specific source (e.g. "from your GL Policy #ABC123", "from vercel.com", "from your business context"). If a web lookup was done, name the URL that was checked. Ask them to reply with corrections if anything is wrong.' : "Start with a one-line intro."}
4183
+ - Mention progress once using estimated time remaining. Don't mention section/batch numbers or field counts.
4184
+ - Use "${company}" by name when referring to the company. Also fine: "we" or "our". Never "our company" or "the company".
4185
+ - Ask questions plainly. No em-dashes for dramatic effect, no filler phrases like "need to nail down" or "let's dive into". Just ask.
4186
+ - For yes/no questions, ask naturally in one sentence. Don't list "Yes / No" as options. Mention what you'll need if the answer triggers a follow-up (e.g. "If not, I'll need a brief explanation.").
4187
+ - For fields with 2-3 options, mention them inline. 4+ options can be a short list.
4188
+ - Group related fields (address, coverage limits) into single compound questions.
4189
+ - Do NOT include conditional/follow-up fields. They will be sent separately.
4190
+ - Number each question.
4191
+ - Note expected format where relevant: dollar amounts for currency, MM/DD/YYYY for dates, column descriptions for tables.
4192
+ - End with a short closing.
4193
+ - Tone: professional, brief, matter-of-fact. Write like a busy coworker, not a chatbot. No flourishes, no em-dashes between clauses, no editorializing about the questions.
4194
+
4195
+ NEVER:
4196
+ - Sound like a salesperson or customer service agent
4197
+ - Use em-dashes for emphasis or dramatic pacing
4198
+ - Editorialize ("these two should wrap up this section", "just a couple more")
4199
+ - List "Yes / No / N/A" as bullet options
4200
+ - Include conditional follow-up questions
4201
+ - Mention section numbers, batch numbers, or field counts
4202
+
4203
+ Output the email body text ONLY. No subject line, no JSON. Use markdown for numbered lists.`;
4204
+ }
4205
+
4206
+ // src/application/agents/email-generator.ts
4207
+ async function generateBatchEmail(batchFields, batchIndex, totalBatches, opts, generateText, providerOptions) {
4208
+ const fieldSummaries = batchFields.map((f) => ({
4209
+ id: f.id,
4210
+ label: f.label,
4211
+ fieldType: f.fieldType,
4212
+ options: f.options,
4213
+ condition: f.condition
4214
+ }));
4215
+ const prompt = buildBatchEmailGenerationPrompt(
4216
+ fieldSummaries,
4217
+ batchIndex,
4218
+ totalBatches,
4219
+ opts.appTitle,
4220
+ opts.totalFieldCount,
4221
+ opts.filledFieldCount,
4222
+ opts.previousBatchSummary,
4223
+ opts.companyName
4224
+ );
4225
+ const { text, usage } = await withRetry(
4226
+ () => generateText({
4227
+ prompt,
4228
+ maxTokens: 2048,
4229
+ providerOptions
4230
+ })
4231
+ );
4232
+ return { text, usage };
4233
+ }
4234
+
4235
+ // src/application/coordinator.ts
4236
+ function createApplicationPipeline(config) {
4237
+ const {
4238
+ generateText,
4239
+ generateObject,
4240
+ applicationStore,
4241
+ documentStore,
4242
+ memoryStore,
4243
+ backfillProvider,
4244
+ orgContext = [],
4245
+ concurrency = 4,
4246
+ onTokenUsage,
4247
+ onProgress,
4248
+ log,
4249
+ providerOptions
4250
+ } = config;
4251
+ const limit = pLimit(concurrency);
4252
+ let totalUsage = { inputTokens: 0, outputTokens: 0 };
4253
+ function trackUsage(usage) {
4254
+ if (usage) {
4255
+ totalUsage.inputTokens += usage.inputTokens;
4256
+ totalUsage.outputTokens += usage.outputTokens;
4257
+ onTokenUsage?.(usage);
4258
+ }
4259
+ }
4260
+ async function processApplication(input) {
4261
+ totalUsage = { inputTokens: 0, outputTokens: 0 };
4262
+ const { pdfBase64, context } = input;
4263
+ const id = input.applicationId ?? `app-${Date.now()}`;
4264
+ const now = Date.now();
4265
+ let state = {
4266
+ id,
4267
+ pdfBase64: void 0,
4268
+ // Don't persist the full PDF in state
4269
+ title: void 0,
4270
+ applicationType: null,
4271
+ fields: [],
4272
+ batches: void 0,
4273
+ currentBatchIndex: 0,
4274
+ status: "classifying",
4275
+ createdAt: now,
4276
+ updatedAt: now
4277
+ };
4278
+ onProgress?.("Classifying document...");
4279
+ const { result: classifyResult, usage: classifyUsage } = await classifyApplication(
4280
+ pdfBase64.slice(0, 2e3),
4281
+ // Send truncated content for classification
4282
+ generateObject,
4283
+ providerOptions
4284
+ );
4285
+ trackUsage(classifyUsage);
4286
+ if (!classifyResult.isApplication) {
4287
+ state.status = "complete";
4288
+ state.updatedAt = Date.now();
4289
+ await applicationStore?.save(state);
4290
+ return { state, tokenUsage: totalUsage };
4291
+ }
4292
+ state.applicationType = classifyResult.applicationType;
4293
+ state.status = "extracting";
4294
+ state.updatedAt = Date.now();
4295
+ onProgress?.("Extracting form fields...");
4296
+ const { fields, usage: extractUsage } = await extractFields(
4297
+ pdfBase64,
4298
+ generateObject,
4299
+ providerOptions
4300
+ );
4301
+ trackUsage(extractUsage);
4302
+ state.fields = fields;
4303
+ state.title = classifyResult.applicationType ?? void 0;
4304
+ state.status = "auto_filling";
4305
+ state.updatedAt = Date.now();
4306
+ await applicationStore?.save(state);
4307
+ onProgress?.(`Auto-filling ${fields.length} fields...`);
4308
+ const fillTasks = [];
4309
+ if (backfillProvider) {
4310
+ fillTasks.push(
4311
+ (async () => {
4312
+ try {
4313
+ const priorAnswers = await backfillFromPriorAnswers(fields, backfillProvider);
4314
+ for (const pa of priorAnswers) {
4315
+ const field = state.fields.find((f) => f.id === pa.fieldId);
4316
+ if (field && !field.value && pa.relevance > 0.8) {
4317
+ field.value = pa.value;
4318
+ field.source = `backfill: ${pa.source}`;
4319
+ field.confidence = "high";
4320
+ }
4321
+ }
4322
+ } catch (e) {
4323
+ await log?.(`Backfill failed: ${e}`);
4324
+ }
4325
+ })()
4326
+ );
4327
+ }
4328
+ if (orgContext.length > 0) {
4329
+ fillTasks.push(
4330
+ limit(async () => {
4331
+ const unfilledFields2 = state.fields.filter((f) => !f.value);
4332
+ if (unfilledFields2.length === 0) return;
4333
+ const { result: autoFillResult, usage: afUsage } = await autoFillFromContext(
4334
+ unfilledFields2,
4335
+ orgContext,
4336
+ generateObject,
4337
+ providerOptions
4338
+ );
4339
+ trackUsage(afUsage);
4340
+ for (const match of autoFillResult.matches) {
4341
+ const field = state.fields.find((f) => f.id === match.fieldId);
4342
+ if (field && !field.value) {
4343
+ field.value = match.value;
4344
+ field.source = `auto-fill: ${match.contextKey}`;
4345
+ field.confidence = match.confidence;
4346
+ }
4347
+ }
4348
+ })
4349
+ );
4350
+ }
4351
+ if (documentStore && memoryStore) {
4352
+ fillTasks.push(
4353
+ (async () => {
4354
+ try {
4355
+ const unfilledFields2 = state.fields.filter((f) => !f.value);
4356
+ const searchPromises = unfilledFields2.slice(0, 10).map(
4357
+ (f) => limit(async () => {
4358
+ const chunks = await memoryStore.search(f.label, { limit: 3 });
4359
+ for (const chunk of chunks) {
4360
+ if (!state.fields.find((sf) => sf.id === f.id)?.value) {
4361
+ }
4362
+ }
4363
+ })
4364
+ );
4365
+ await Promise.all(searchPromises);
4366
+ } catch (e) {
4367
+ await log?.(`Document backfill search failed: ${e}`);
4368
+ }
4369
+ })()
4370
+ );
4371
+ }
4372
+ await Promise.all(fillTasks);
4373
+ state.updatedAt = Date.now();
4374
+ await applicationStore?.save(state);
4375
+ const unfilledFields = state.fields.filter((f) => !f.value);
4376
+ if (unfilledFields.length > 0) {
4377
+ onProgress?.(`Batching ${unfilledFields.length} remaining questions...`);
4378
+ state.status = "batching";
4379
+ const { result: batchResult, usage: batchUsage } = await batchQuestions(
4380
+ unfilledFields,
4381
+ generateObject,
4382
+ providerOptions
4383
+ );
4384
+ trackUsage(batchUsage);
4385
+ state.batches = batchResult.batches;
4386
+ state.currentBatchIndex = 0;
4387
+ state.status = "collecting";
4388
+ } else {
4389
+ state.status = "confirming";
4390
+ }
4391
+ state.updatedAt = Date.now();
4392
+ await applicationStore?.save(state);
4393
+ const filledCount = state.fields.filter((f) => f.value).length;
4394
+ onProgress?.(`Application processed: ${filledCount}/${state.fields.length} fields filled, ${state.batches?.length ?? 0} batches to collect.`);
4395
+ return { state, tokenUsage: totalUsage };
4396
+ }
4397
+ async function processReply(input) {
4398
+ totalUsage = { inputTokens: 0, outputTokens: 0 };
4399
+ const { applicationId, replyText, context } = input;
4400
+ let state = null;
4401
+ if (applicationStore) {
4402
+ state = await applicationStore.get(applicationId);
4403
+ }
4404
+ if (!state) {
4405
+ throw new Error(`Application ${applicationId} not found`);
4406
+ }
4407
+ const currentBatchFieldIds = state.batches?.[state.currentBatchIndex] ?? [];
4408
+ const currentBatchFields = state.fields.filter(
4409
+ (f) => currentBatchFieldIds.includes(f.id)
4410
+ );
4411
+ onProgress?.("Classifying reply...");
4412
+ const { intent, usage: intentUsage } = await classifyReplyIntent(
4413
+ currentBatchFields,
4414
+ replyText,
4415
+ generateObject,
4416
+ providerOptions
4417
+ );
4418
+ trackUsage(intentUsage);
4419
+ let fieldsFilled = 0;
4420
+ let responseText;
4421
+ if (intent.hasAnswers) {
4422
+ onProgress?.("Parsing answers...");
4423
+ const { result: parseResult, usage: parseUsage } = await parseAnswers(
4424
+ currentBatchFields,
4425
+ replyText,
4426
+ generateObject,
4427
+ providerOptions
4428
+ );
4429
+ trackUsage(parseUsage);
4430
+ for (const answer of parseResult.answers) {
4431
+ const field = state.fields.find((f) => f.id === answer.fieldId);
4432
+ if (field) {
4433
+ field.value = answer.value;
4434
+ field.source = "user";
4435
+ field.confidence = "confirmed";
4436
+ fieldsFilled++;
4437
+ }
4438
+ }
4439
+ }
4440
+ if (intent.lookupRequests?.length) {
4441
+ onProgress?.("Processing lookup requests...");
4442
+ let availableData = "";
4443
+ if (documentStore) {
4444
+ try {
4445
+ const docs = await documentStore.query({});
4446
+ availableData = docs.map((d) => {
4447
+ const doc = d;
4448
+ return `Document ${doc.id}: ${doc.type} - ${doc.carrier ?? "unknown carrier"} - ${doc.insuredName ?? ""}`;
4449
+ }).join("\n");
4450
+ } catch (e) {
4451
+ await log?.(`Document query for lookup failed: ${e}`);
4452
+ }
4453
+ }
4454
+ if (availableData) {
4455
+ const targetFields = state.fields.filter(
4456
+ (f) => intent.lookupRequests.some((lr) => lr.targetFieldIds.includes(f.id))
4457
+ );
4458
+ const { result: lookupResult, usage: lookupUsage } = await fillFromLookup(
4459
+ intent.lookupRequests,
4460
+ targetFields,
4461
+ availableData,
4462
+ generateObject,
4463
+ providerOptions
4464
+ );
4465
+ trackUsage(lookupUsage);
4466
+ for (const fill of lookupResult.fills) {
4467
+ const field = state.fields.find((f) => f.id === fill.fieldId);
4468
+ if (field) {
4469
+ field.value = fill.value;
4470
+ field.source = `lookup: ${fill.source}`;
4471
+ field.confidence = "high";
4472
+ fieldsFilled++;
4473
+ }
4474
+ }
4475
+ }
4476
+ }
4477
+ if (intent.primaryIntent === "question" || intent.primaryIntent === "mixed") {
4478
+ if (intent.questionText) {
4479
+ const { text, usage } = await generateText({
4480
+ prompt: `The user is filling out an insurance application and asked: "${intent.questionText}"
4481
+
4482
+ Provide a brief, helpful explanation (2-3 sentences). End with "Just reply with the answer when you're ready and I'll fill it in."`,
4483
+ maxTokens: 512,
4484
+ providerOptions
4485
+ });
4486
+ trackUsage(usage);
4487
+ responseText = text;
4488
+ }
4489
+ }
4490
+ const currentBatchComplete = currentBatchFieldIds.every(
4491
+ (fid) => state.fields.find((f) => f.id === fid)?.value
4492
+ );
4493
+ if (currentBatchComplete && state.batches) {
4494
+ if (state.currentBatchIndex < state.batches.length - 1) {
4495
+ state.currentBatchIndex++;
4496
+ const nextBatchFieldIds = state.batches[state.currentBatchIndex];
4497
+ const nextBatchFields = state.fields.filter(
4498
+ (f) => nextBatchFieldIds.includes(f.id)
4499
+ );
4500
+ const filledCount = state.fields.filter((f) => f.value).length;
4501
+ const { text: emailText, usage: emailUsage } = await generateBatchEmail(
4502
+ nextBatchFields,
4503
+ state.currentBatchIndex,
4504
+ state.batches.length,
4505
+ {
4506
+ appTitle: state.title,
4507
+ totalFieldCount: state.fields.length,
4508
+ filledFieldCount: filledCount,
4509
+ companyName: context?.companyName
4510
+ },
4511
+ generateText,
4512
+ providerOptions
4513
+ );
4514
+ trackUsage(emailUsage);
4515
+ if (!responseText) {
4516
+ responseText = emailText;
4517
+ } else {
4518
+ responseText += `
4519
+
4520
+ ${emailText}`;
4521
+ }
4522
+ } else {
4523
+ state.status = "confirming";
4524
+ }
4525
+ }
4526
+ state.updatedAt = Date.now();
4527
+ await applicationStore?.save(state);
4528
+ return {
4529
+ state,
4530
+ intent: intent.primaryIntent,
4531
+ fieldsFilled,
4532
+ responseText,
4533
+ tokenUsage: totalUsage
4534
+ };
4535
+ }
4536
+ async function generateCurrentBatchEmail(applicationId, opts) {
4537
+ totalUsage = { inputTokens: 0, outputTokens: 0 };
4538
+ const state = await applicationStore?.get(applicationId);
4539
+ if (!state) throw new Error(`Application ${applicationId} not found`);
4540
+ if (!state.batches?.length) throw new Error("No batches available");
4541
+ const batchFieldIds = state.batches[state.currentBatchIndex];
4542
+ const batchFields = state.fields.filter((f) => batchFieldIds.includes(f.id));
4543
+ const filledCount = state.fields.filter((f) => f.value).length;
4544
+ const { text, usage } = await generateBatchEmail(
4545
+ batchFields,
4546
+ state.currentBatchIndex,
4547
+ state.batches.length,
4548
+ {
4549
+ appTitle: state.title,
4550
+ totalFieldCount: state.fields.length,
4551
+ filledFieldCount: filledCount,
4552
+ companyName: opts?.companyName,
4553
+ previousBatchSummary: opts?.previousBatchSummary
4554
+ },
4555
+ generateText,
4556
+ providerOptions
4557
+ );
4558
+ trackUsage(usage);
4559
+ return { text, tokenUsage: totalUsage };
4560
+ }
4561
+ async function getConfirmationSummary(applicationId) {
4562
+ totalUsage = { inputTokens: 0, outputTokens: 0 };
4563
+ const state = await applicationStore?.get(applicationId);
4564
+ if (!state) throw new Error(`Application ${applicationId} not found`);
4565
+ const filledFields = state.fields.filter((f) => f.value);
4566
+ const fieldSummary = filledFields.map((f) => `${f.section} > ${f.label}: ${f.value} (source: ${f.source ?? "unknown"})`).join("\n");
4567
+ const { text, usage } = await generateText({
4568
+ prompt: `Format these filled insurance application fields as a clean confirmation summary for the user to review. Group by section, show each field as "Label: Value". End with a note asking them to confirm or request changes.
4569
+
4570
+ Application: ${state.title ?? "Insurance Application"}
4571
+
4572
+ Fields:
4573
+ ${fieldSummary}`,
4574
+ maxTokens: 4096,
4575
+ providerOptions
4576
+ });
4577
+ trackUsage(usage);
4578
+ return { text, tokenUsage: totalUsage };
4579
+ }
4580
+ return {
4581
+ processApplication,
4582
+ processReply,
4583
+ generateCurrentBatchEmail,
4584
+ getConfirmationSummary
4585
+ };
4586
+ }
4587
+
4588
+ // src/prompts/application/confirmation.ts
4589
+ function buildConfirmationSummaryPrompt(fields, applicationTitle) {
4590
+ const fieldList = fields.map((f) => {
4591
+ const label = f.label ?? f.text ?? f.id;
4592
+ const value = f.value ?? "(not provided)";
4593
+ return `[${f.section}] ${label}: ${value}`;
4594
+ }).join("\n");
4595
+ return `Format the following insurance application answers into a clean, readable summary grouped by section. This will be sent as an email for the user to review and confirm.
4596
+
4597
+ APPLICATION: ${applicationTitle}
4598
+
4599
+ FIELD VALUES:
4600
+ ${fieldList}
4601
+
4602
+ Format as a readable summary:
4603
+ - Group by section with section headers
4604
+ - Show each field as "Label: Value"
4605
+ - For declarations, show the question and the yes/no answer plus any explanation
4606
+ - Skip fields with no value unless they are required
4607
+ - End with a note asking the user to reply "Looks good" to confirm, or describe any changes needed
4608
+
4609
+ Respond with the formatted summary text only (no JSON wrapper). Use markdown formatting (bold headers, bullet points).`;
4610
+ }
4611
+
4612
+ // src/prompts/application/field-explanation.ts
4613
+ function buildFieldExplanationPrompt(field, question, policyContext) {
4614
+ return `You are an internal risk management assistant helping a colleague fill out an insurance application for your company. They asked a question about a field on the form.
4615
+
4616
+ FIELD: "${field.label}" (type: ${field.fieldType}${field.options ? `, options: ${field.options.join(", ")}` : ""})
4617
+
4618
+ THEIR QUESTION: "${question}"
4619
+
4620
+ ${policyContext ? `RELEVANT POLICY/CONTEXT INFO:
4621
+ ${policyContext}
4622
+ ` : ""}
4623
+
4624
+ Provide a short, helpful explanation (2-3 sentences) as a coworker would. If the field has options, briefly explain what each means if relevant. If there's policy context that helps, cite the specific source (e.g. "According to our GL Policy #ABC123 with Hartford, our current aggregate limit is $2M").
4625
+
4626
+ End with: "Just reply with the answer when you're ready and I'll fill it in."
4627
+
4628
+ Respond with the explanation text only \u2014 no JSON, no field ID, no extra formatting.`;
4629
+ }
4630
+
4631
+ // src/prompts/query/classify.ts
4632
+ function buildQueryClassifyPrompt(question, conversationContext) {
4633
+ return `You are a query classifier for an insurance document intelligence system.
4634
+
4635
+ Analyze the user's question and produce a structured classification.
4636
+
4637
+ USER QUESTION:
4638
+ ${question}
4639
+ ${conversationContext ? `
4640
+ CONVERSATION CONTEXT:
4641
+ ${conversationContext}` : ""}
4642
+
4643
+ INSTRUCTIONS:
4644
+
4645
+ 1. Determine the primary intent:
4646
+ - "policy_question": questions about specific coverage, limits, deductibles, endorsements, conditions
4647
+ - "coverage_comparison": comparing coverages across multiple documents or policies
4648
+ - "document_search": looking for a specific document by carrier, policy number, insured name
4649
+ - "claims_inquiry": questions about claims history, loss runs, experience modification
4650
+ - "general_knowledge": insurance concepts not tied to a specific document
4651
+
4652
+ 2. Decompose into atomic sub-questions:
4653
+ - Each sub-question should be answerable from a single retrieval pass
4654
+ - Simple questions produce exactly one sub-question (the question itself)
4655
+ - Complex questions (comparisons, multi-policy, multi-field) decompose into 2-5 sub-questions
4656
+ - Each sub-question should specify which chunk types are most relevant
4657
+
4658
+ 3. Determine which storage backends are needed:
4659
+ - requiresDocumentLookup: true if a specific document needs to be fetched by ID/number/carrier
4660
+ - requiresChunkSearch: true if semantic search over document chunks is needed
4661
+ - requiresConversationHistory: true if the question references prior conversation
4662
+
4663
+ CHUNK TYPES (for chunkTypes filter):
4664
+ carrier_info, named_insured, coverage, endorsement, exclusion, condition, section, declaration, loss_history, premium, supplementary
4665
+
4666
+ Respond with the structured classification.`;
4667
+ }
4668
+
4669
+ // src/prompts/query/respond.ts
4670
+ function buildRespondPrompt(originalQuestion, subAnswersJson, platform) {
4671
+ const formatGuidance = platform === "email" ? "Format as a professional email response. Use plain text, no markdown." : platform === "sms" ? "Keep the response concise and conversational. No markdown." : "Format as clear, well-structured text. Use markdown for lists and emphasis where helpful.";
4672
+ return `You are composing a final answer to an insurance question. You have verified sub-answers with citations that you need to merge into a single, natural response.
4673
+
4674
+ ORIGINAL QUESTION:
4675
+ ${originalQuestion}
4676
+
4677
+ VERIFIED SUB-ANSWERS:
4678
+ ${subAnswersJson}
4679
+
4680
+ FORMATTING:
4681
+ ${formatGuidance}
4682
+
4683
+ INSTRUCTIONS:
4684
+ 1. Write a natural, direct answer to the original question.
4685
+ 2. Embed inline citation numbers [1], [2], etc. after each factual claim. These reference the citation objects from the sub-answers \u2014 preserve the original citation index numbers.
4686
+ 3. If any sub-answer had low confidence or noted missing context, mention what information was unavailable rather than omitting silently.
4687
+ 4. If the answer naturally leads to a follow-up question the user might want to ask, suggest it in the followUp field.
4688
+ 5. Merge overlapping citations \u2014 if two sub-answers cite the same chunk, use one citation number.
4689
+ 6. Keep the tone helpful and professional.
4690
+
4691
+ Respond with the final answer, deduplicated citations array, overall confidence (weighted average of sub-answer confidences), and an optional follow-up suggestion.`;
4692
+ }
4693
+
4694
+ // src/schemas/query.ts
4695
+ var import_zod32 = require("zod");
4696
+ var QueryIntentSchema = import_zod32.z.enum([
4697
+ "policy_question",
4698
+ "coverage_comparison",
4699
+ "document_search",
4700
+ "claims_inquiry",
4701
+ "general_knowledge"
4702
+ ]);
4703
+ var SubQuestionSchema = import_zod32.z.object({
4704
+ question: import_zod32.z.string().describe("Atomic sub-question to retrieve and answer independently"),
4705
+ intent: QueryIntentSchema,
4706
+ chunkTypes: import_zod32.z.array(import_zod32.z.string()).optional().describe("Chunk types to filter retrieval (e.g. coverage, endorsement, declaration)"),
4707
+ documentFilters: import_zod32.z.object({
4708
+ type: import_zod32.z.enum(["policy", "quote"]).optional(),
4709
+ carrier: import_zod32.z.string().optional(),
4710
+ insuredName: import_zod32.z.string().optional(),
4711
+ policyNumber: import_zod32.z.string().optional(),
4712
+ quoteNumber: import_zod32.z.string().optional()
4713
+ }).optional().describe("Structured filters to narrow document lookup")
4714
+ });
4715
+ var QueryClassifyResultSchema = import_zod32.z.object({
4716
+ intent: QueryIntentSchema,
4717
+ subQuestions: import_zod32.z.array(SubQuestionSchema).min(1).describe("Decomposed atomic sub-questions"),
4718
+ requiresDocumentLookup: import_zod32.z.boolean().describe("Whether structured document lookup is needed"),
4719
+ requiresChunkSearch: import_zod32.z.boolean().describe("Whether semantic chunk search is needed"),
4720
+ requiresConversationHistory: import_zod32.z.boolean().describe("Whether conversation history is relevant")
4721
+ });
4722
+ var EvidenceItemSchema = import_zod32.z.object({
4723
+ source: import_zod32.z.enum(["chunk", "document", "conversation"]),
4724
+ chunkId: import_zod32.z.string().optional(),
4725
+ documentId: import_zod32.z.string().optional(),
4726
+ turnId: import_zod32.z.string().optional(),
4727
+ text: import_zod32.z.string().describe("Text excerpt from the source"),
4728
+ relevance: import_zod32.z.number().min(0).max(1),
4729
+ metadata: import_zod32.z.record(import_zod32.z.string(), import_zod32.z.string()).optional()
4730
+ });
4731
+ var RetrievalResultSchema = import_zod32.z.object({
4732
+ subQuestion: import_zod32.z.string(),
4733
+ evidence: import_zod32.z.array(EvidenceItemSchema)
4734
+ });
4735
+ var CitationSchema = import_zod32.z.object({
4736
+ index: import_zod32.z.number().describe("Citation number [1], [2], etc."),
4737
+ chunkId: import_zod32.z.string().describe("Source chunk ID, e.g. doc-123:coverage:2"),
4738
+ documentId: import_zod32.z.string(),
4739
+ documentType: import_zod32.z.enum(["policy", "quote"]).optional(),
4740
+ field: import_zod32.z.string().optional().describe("Specific field path, e.g. coverages[0].deductible"),
4741
+ quote: import_zod32.z.string().describe("Exact text from source that supports the claim"),
4742
+ relevance: import_zod32.z.number().min(0).max(1)
4743
+ });
4744
+ var SubAnswerSchema = import_zod32.z.object({
4745
+ subQuestion: import_zod32.z.string(),
4746
+ answer: import_zod32.z.string(),
4747
+ citations: import_zod32.z.array(CitationSchema),
4748
+ confidence: import_zod32.z.number().min(0).max(1),
4749
+ needsMoreContext: import_zod32.z.boolean().describe("True if evidence was insufficient to answer fully")
4750
+ });
4751
+ var VerifyResultSchema = import_zod32.z.object({
4752
+ approved: import_zod32.z.boolean().describe("Whether all sub-answers are adequately grounded"),
4753
+ issues: import_zod32.z.array(import_zod32.z.string()).describe("Specific grounding or consistency issues found"),
4754
+ retrySubQuestions: import_zod32.z.array(import_zod32.z.string()).optional().describe("Sub-questions that need additional retrieval or re-reasoning")
4755
+ });
4756
+ var QueryResultSchema = import_zod32.z.object({
4757
+ answer: import_zod32.z.string(),
4758
+ citations: import_zod32.z.array(CitationSchema),
4759
+ intent: QueryIntentSchema,
4760
+ confidence: import_zod32.z.number().min(0).max(1),
4761
+ followUp: import_zod32.z.string().optional().describe("Suggested follow-up question if applicable")
4762
+ });
4763
+
4764
+ // src/query/retriever.ts
4765
+ async function retrieve(subQuestion, conversationId, config) {
4766
+ const { documentStore, memoryStore, retrievalLimit, log } = config;
4767
+ const evidence = [];
4768
+ const tasks = [];
4769
+ tasks.push(
4770
+ (async () => {
4771
+ try {
4772
+ const filter = {};
4773
+ if (subQuestion.chunkTypes?.length) {
4774
+ const chunkResults = await Promise.all(
4775
+ subQuestion.chunkTypes.map(
4776
+ (type) => memoryStore.search(subQuestion.question, {
4777
+ limit: Math.ceil(retrievalLimit / subQuestion.chunkTypes.length),
4778
+ filter: { ...filter, type }
4779
+ })
4780
+ )
4781
+ );
4782
+ for (const chunks of chunkResults) {
4783
+ for (const chunk of chunks) {
4784
+ evidence.push({
4785
+ source: "chunk",
4786
+ chunkId: chunk.id,
4787
+ documentId: chunk.documentId,
4788
+ text: chunk.text,
4789
+ relevance: 0.8,
4790
+ // Default — store doesn't expose scores directly
4791
+ metadata: chunk.metadata
4792
+ });
4793
+ }
4794
+ }
4795
+ } else {
4796
+ const chunks = await memoryStore.search(subQuestion.question, {
4797
+ limit: retrievalLimit
4798
+ });
4799
+ for (const chunk of chunks) {
4800
+ evidence.push({
4801
+ source: "chunk",
4802
+ chunkId: chunk.id,
4803
+ documentId: chunk.documentId,
4804
+ text: chunk.text,
4805
+ relevance: 0.8,
4806
+ metadata: chunk.metadata
4807
+ });
4808
+ }
4809
+ }
4810
+ } catch (e) {
4811
+ await log?.(`Chunk search failed for "${subQuestion.question}": ${e}`);
4812
+ }
4813
+ })()
4814
+ );
4815
+ if (subQuestion.documentFilters) {
4816
+ tasks.push(
4817
+ (async () => {
4818
+ try {
4819
+ const filters = {};
4820
+ if (subQuestion.documentFilters?.type) filters.type = subQuestion.documentFilters.type;
4821
+ if (subQuestion.documentFilters?.carrier) filters.carrier = subQuestion.documentFilters.carrier;
4822
+ if (subQuestion.documentFilters?.insuredName) filters.insuredName = subQuestion.documentFilters.insuredName;
4823
+ if (subQuestion.documentFilters?.policyNumber) filters.policyNumber = subQuestion.documentFilters.policyNumber;
4824
+ if (subQuestion.documentFilters?.quoteNumber) filters.quoteNumber = subQuestion.documentFilters.quoteNumber;
4825
+ const docs = await documentStore.query(filters);
4826
+ for (const doc of docs) {
4827
+ const summary = buildDocumentSummary(doc);
4828
+ evidence.push({
4829
+ source: "document",
4830
+ documentId: doc.id,
4831
+ text: summary,
4832
+ relevance: 0.9,
4833
+ // Direct lookup is high relevance
4834
+ metadata: {
4835
+ type: doc.type,
4836
+ carrier: doc.carrier ?? "",
4837
+ insuredName: doc.insuredName ?? ""
4838
+ }
4839
+ });
4840
+ }
4841
+ } catch (e) {
4842
+ await log?.(`Document lookup failed: ${e}`);
4843
+ }
4844
+ })()
4845
+ );
4846
+ }
4847
+ if (conversationId) {
4848
+ tasks.push(
4849
+ (async () => {
4850
+ try {
4851
+ const turns = await memoryStore.searchHistory(
4852
+ subQuestion.question,
4853
+ conversationId
4854
+ );
4855
+ for (const turn of turns.slice(0, 5)) {
4856
+ evidence.push({
4857
+ source: "conversation",
4858
+ turnId: turn.id,
4859
+ text: `[${turn.role}]: ${turn.content}`,
4860
+ relevance: 0.6
4861
+ // Conversation context is lower relevance than documents
4862
+ });
4863
+ }
4864
+ } catch (e) {
4865
+ await log?.(`Conversation history search failed: ${e}`);
4866
+ }
4867
+ })()
4868
+ );
4869
+ }
4870
+ await Promise.all(tasks);
4871
+ evidence.sort((a, b) => b.relevance - a.relevance);
4872
+ return {
4873
+ subQuestion: subQuestion.question,
4874
+ evidence: evidence.slice(0, retrievalLimit)
4875
+ };
4876
+ }
4877
+ function buildDocumentSummary(doc) {
4878
+ const parts = [];
4879
+ const type = doc.type;
4880
+ parts.push(`Document type: ${type}`);
4881
+ if (doc.carrier) parts.push(`Carrier: ${doc.carrier}`);
4882
+ if (doc.insuredName) parts.push(`Insured: ${doc.insuredName}`);
4883
+ if (type === "policy") {
4884
+ if (doc.policyNumber) parts.push(`Policy #: ${doc.policyNumber}`);
4885
+ if (doc.effectiveDate) parts.push(`Effective: ${doc.effectiveDate}`);
4886
+ if (doc.expirationDate) parts.push(`Expiration: ${doc.expirationDate}`);
4887
+ } else if (type === "quote") {
4888
+ if (doc.quoteNumber) parts.push(`Quote #: ${doc.quoteNumber}`);
4889
+ if (doc.proposedEffectiveDate) parts.push(`Proposed effective: ${doc.proposedEffectiveDate}`);
4890
+ }
4891
+ if (doc.premium) parts.push(`Premium: ${doc.premium}`);
4892
+ const coverages = doc.coverages;
4893
+ if (coverages?.length) {
4894
+ parts.push(`Coverages (${coverages.length}):`);
4895
+ for (const cov of coverages.slice(0, 10)) {
4896
+ const line = [cov.name, cov.limit ? `Limit: ${cov.limit}` : null, cov.deductible ? `Ded: ${cov.deductible}` : null].filter(Boolean).join(" | ");
4897
+ parts.push(` - ${line}`);
4898
+ }
4899
+ }
4900
+ return parts.join("\n");
4901
+ }
4902
+
4903
+ // src/prompts/query/reason.ts
4904
+ var INTENT_INSTRUCTIONS = {
4905
+ policy_question: `You are answering a question about a specific insurance policy or quote.
4906
+
4907
+ RULES:
4908
+ - Answer ONLY from the evidence provided. Do not use general knowledge.
4909
+ - When citing limits, deductibles, or amounts, use the exact values from the source.
4910
+ - If the evidence mentions an endorsement that modifies coverage, include that context.
4911
+ - If the evidence is insufficient, say what is missing rather than guessing.
4912
+ - Reference specific coverage names, form numbers, and endorsement titles when available.`,
4913
+ coverage_comparison: `You are comparing coverages across insurance documents.
4914
+
4915
+ RULES:
4916
+ - Answer ONLY from the evidence provided.
4917
+ - Structure your comparison around specific coverage attributes: limits, deductibles, forms, triggers.
4918
+ - Note differences clearly: "Policy A has X, while Policy B has Y."
4919
+ - Flag where one document has coverage the other lacks entirely.
4920
+ - If evidence for one side of the comparison is missing, state that explicitly.`,
4921
+ document_search: `You are helping locate a specific insurance document.
4922
+
4923
+ RULES:
4924
+ - Answer ONLY from the evidence provided.
4925
+ - Identify the document by carrier, policy/quote number, insured name, and effective dates.
4926
+ - If multiple documents match, list them with distinguishing details.
4927
+ - If no documents match, say so clearly.`,
4928
+ claims_inquiry: `You are answering a question about claims history or loss experience.
4929
+
4930
+ RULES:
4931
+ - Answer ONLY from the evidence provided.
4932
+ - Reference specific claim dates, amounts, descriptions, and statuses.
4933
+ - Include experience modification factors if available.
4934
+ - Be precise with dollar amounts and dates \u2014 do not approximate.
4935
+ - If the evidence shows no claims, state that explicitly.`,
4936
+ general_knowledge: `You are answering a general insurance question using available document context.
4937
+
4938
+ RULES:
4939
+ - You may use general insurance knowledge to frame your answer.
4940
+ - If the question can be answered from the evidence, prefer that over general knowledge.
4941
+ - When mixing general knowledge with document-specific data, make the distinction clear.
4942
+ - Still cite evidence when referencing specific documents.`
4943
+ };
4944
+ function buildReasonPrompt(subQuestion, intent, evidence) {
4945
+ return `${INTENT_INSTRUCTIONS[intent]}
4946
+
4947
+ SUB-QUESTION:
4948
+ ${subQuestion}
4949
+
4950
+ EVIDENCE:
4951
+ ${evidence}
4952
+
4953
+ Answer the sub-question based on the evidence above. For every factual claim, include a citation referencing the source evidence item by its chunkId or documentId. Rate your confidence from 0 to 1 based on how well the evidence supports your answer. Set needsMoreContext to true if the evidence was insufficient.`;
4954
+ }
4955
+
4956
+ // src/query/reasoner.ts
4957
+ async function reason(subQuestion, intent, evidence, config) {
4958
+ const { generateObject, providerOptions } = config;
4959
+ const evidenceText = evidence.map((e, i) => {
4960
+ const sourceLabel = e.source === "chunk" ? `[chunk:${e.chunkId}]` : e.source === "document" ? `[doc:${e.documentId}]` : `[turn:${e.turnId}]`;
4961
+ return `Evidence ${i + 1} ${sourceLabel} (relevance: ${e.relevance.toFixed(2)}):
4962
+ ${e.text}`;
4963
+ }).join("\n\n");
4964
+ const prompt = buildReasonPrompt(subQuestion, intent, evidenceText);
4965
+ const { object, usage } = await withRetry(
4966
+ () => generateObject({
4967
+ prompt,
4968
+ schema: SubAnswerSchema,
4969
+ maxTokens: 4096,
4970
+ providerOptions
4971
+ })
4972
+ );
4973
+ return { subAnswer: object, usage };
4974
+ }
4975
+
4976
+ // src/prompts/query/verify.ts
4977
+ function buildVerifyPrompt(originalQuestion, subAnswersJson, evidenceJson) {
4978
+ return `You are a verification agent for an insurance document intelligence system. Your job is to check that answers are accurate, grounded, and complete.
4979
+
4980
+ ORIGINAL QUESTION:
4981
+ ${originalQuestion}
4982
+
4983
+ SUB-ANSWERS:
4984
+ ${subAnswersJson}
4985
+
4986
+ AVAILABLE EVIDENCE:
4987
+ ${evidenceJson}
4988
+
4989
+ CHECK EACH SUB-ANSWER FOR:
4990
+
4991
+ 1. GROUNDING: Every factual claim must be supported by a citation that references actual evidence. Flag any claim that:
4992
+ - Has no citation
4993
+ - Cites a source that doesn't actually contain the claimed information
4994
+ - Extrapolates beyond what the evidence states
4995
+
4996
+ 2. CONSISTENCY: Sub-answers should not contradict each other. Flag any contradictions, noting which sub-answers conflict and what the discrepancy is.
4997
+
4998
+ 3. COMPLETENESS: Did each sub-question get an adequate answer? Flag any sub-question where:
4999
+ - The answer is vague or hedged when the evidence supports a specific answer
5000
+ - Important details from the evidence were omitted
5001
+ - The confidence rating seems miscalibrated (high confidence with weak evidence, or low confidence with strong evidence)
5002
+
5003
+ RESPOND WITH:
5004
+ - approved: true only if ALL sub-answers pass all three checks
5005
+ - issues: list every specific issue found (empty array if approved)
5006
+ - retrySubQuestions: sub-questions that need re-retrieval or re-reasoning (only if not approved)`;
5007
+ }
5008
+
5009
+ // src/query/verifier.ts
5010
+ async function verify(originalQuestion, subAnswers, allEvidence, config) {
5011
+ const { generateObject, providerOptions } = config;
5012
+ const subAnswersJson = JSON.stringify(
5013
+ subAnswers.map((sa) => ({
5014
+ subQuestion: sa.subQuestion,
5015
+ answer: sa.answer,
5016
+ citations: sa.citations,
5017
+ confidence: sa.confidence,
5018
+ needsMoreContext: sa.needsMoreContext
5019
+ })),
5020
+ null,
5021
+ 2
5022
+ );
5023
+ const evidenceJson = JSON.stringify(
5024
+ allEvidence.map((e) => ({
5025
+ source: e.source,
5026
+ id: e.chunkId ?? e.documentId ?? e.turnId,
5027
+ text: e.text.slice(0, 500),
5028
+ // Truncate for context efficiency
5029
+ relevance: e.relevance
5030
+ })),
5031
+ null,
5032
+ 2
5033
+ );
5034
+ const prompt = buildVerifyPrompt(originalQuestion, subAnswersJson, evidenceJson);
5035
+ const { object, usage } = await withRetry(
5036
+ () => generateObject({
5037
+ prompt,
5038
+ schema: VerifyResultSchema,
5039
+ maxTokens: 2048,
5040
+ providerOptions
5041
+ })
5042
+ );
5043
+ return { result: object, usage };
5044
+ }
5045
+
5046
+ // src/query/coordinator.ts
5047
+ function createQueryAgent(config) {
5048
+ const {
5049
+ generateText,
5050
+ generateObject,
5051
+ documentStore,
5052
+ memoryStore,
5053
+ concurrency = 3,
5054
+ maxVerifyRounds = 1,
5055
+ retrievalLimit = 10,
5056
+ onTokenUsage,
5057
+ onProgress,
5058
+ log,
5059
+ providerOptions
5060
+ } = config;
5061
+ const limit = pLimit(concurrency);
5062
+ let totalUsage = { inputTokens: 0, outputTokens: 0 };
5063
+ function trackUsage(usage) {
5064
+ if (usage) {
5065
+ totalUsage.inputTokens += usage.inputTokens;
5066
+ totalUsage.outputTokens += usage.outputTokens;
5067
+ onTokenUsage?.(usage);
5068
+ }
5069
+ }
5070
+ async function query(input) {
5071
+ totalUsage = { inputTokens: 0, outputTokens: 0 };
5072
+ const { question, conversationId, context } = input;
5073
+ onProgress?.("Classifying query...");
5074
+ const classification = await classify(question, conversationId);
5075
+ onProgress?.(`Retrieving evidence for ${classification.subQuestions.length} sub-question(s)...`);
5076
+ const retrieverConfig = {
5077
+ documentStore,
5078
+ memoryStore,
5079
+ retrievalLimit,
5080
+ log
5081
+ };
5082
+ const retrievalResults = await Promise.all(
5083
+ classification.subQuestions.map(
5084
+ (sq) => limit(() => retrieve(sq, conversationId, retrieverConfig))
5085
+ )
5086
+ );
5087
+ const allEvidence = retrievalResults.flatMap((r) => r.evidence);
5088
+ onProgress?.("Reasoning over evidence...");
5089
+ const reasonerConfig = { generateObject, providerOptions };
5090
+ let subAnswers = await Promise.all(
5091
+ classification.subQuestions.map(
5092
+ (sq, i) => limit(async () => {
5093
+ const { subAnswer, usage } = await reason(
5094
+ sq.question,
5095
+ sq.intent,
5096
+ retrievalResults[i].evidence,
5097
+ reasonerConfig
5098
+ );
5099
+ trackUsage(usage);
5100
+ return subAnswer;
5101
+ })
5102
+ )
5103
+ );
5104
+ onProgress?.("Verifying answer grounding...");
5105
+ const verifierConfig = { generateObject, providerOptions };
5106
+ for (let round = 0; round < maxVerifyRounds; round++) {
5107
+ const { result: verifyResult, usage } = await verify(
5108
+ question,
5109
+ subAnswers,
5110
+ allEvidence,
5111
+ verifierConfig
5112
+ );
5113
+ trackUsage(usage);
5114
+ if (verifyResult.approved) {
5115
+ onProgress?.("Verification passed.");
5116
+ break;
5117
+ }
5118
+ onProgress?.(`Verification found ${verifyResult.issues.length} issue(s), round ${round + 1}/${maxVerifyRounds}`);
5119
+ await log?.(`Verify issues: ${verifyResult.issues.join("; ")}`);
5120
+ if (verifyResult.retrySubQuestions?.length) {
5121
+ const retryQuestions = classification.subQuestions.filter(
5122
+ (sq) => verifyResult.retrySubQuestions.includes(sq.question)
5123
+ );
5124
+ if (retryQuestions.length > 0) {
5125
+ const retryRetrievals = await Promise.all(
5126
+ retryQuestions.map(
5127
+ (sq) => limit(
5128
+ () => retrieve(sq, conversationId, {
5129
+ ...retrieverConfig,
5130
+ retrievalLimit: retrievalLimit * 2
5131
+ // Broader retrieval on retry
5132
+ })
5133
+ )
5134
+ )
5135
+ );
5136
+ for (const r of retryRetrievals) {
5137
+ allEvidence.push(...r.evidence);
5138
+ }
5139
+ const retrySubAnswers = await Promise.all(
5140
+ retryQuestions.map(
5141
+ (sq, i) => limit(async () => {
5142
+ const { subAnswer, usage: u } = await reason(
5143
+ sq.question,
5144
+ sq.intent,
5145
+ retryRetrievals[i].evidence,
5146
+ reasonerConfig
5147
+ );
5148
+ trackUsage(u);
5149
+ return subAnswer;
5150
+ })
5151
+ )
5152
+ );
5153
+ const retryQSet = new Set(retryQuestions.map((sq) => sq.question));
5154
+ subAnswers = subAnswers.map((sa) => {
5155
+ if (retryQSet.has(sa.subQuestion)) {
5156
+ const replacement = retrySubAnswers.find((r) => r.subQuestion === sa.subQuestion);
5157
+ return replacement ?? sa;
5158
+ }
5159
+ return sa;
5160
+ });
5161
+ }
5162
+ }
5163
+ }
5164
+ onProgress?.("Composing final answer...");
5165
+ const queryResult = await respond(
5166
+ question,
5167
+ subAnswers,
5168
+ classification,
5169
+ context?.platform
5170
+ );
5171
+ if (conversationId) {
5172
+ try {
5173
+ await memoryStore.addTurn({
5174
+ id: `turn-${Date.now()}-q`,
5175
+ conversationId,
5176
+ role: "user",
5177
+ content: question,
5178
+ timestamp: Date.now()
5179
+ });
5180
+ await memoryStore.addTurn({
5181
+ id: `turn-${Date.now()}-a`,
5182
+ conversationId,
5183
+ role: "assistant",
5184
+ content: queryResult.answer,
5185
+ timestamp: Date.now()
5186
+ });
5187
+ } catch (e) {
5188
+ await log?.(`Failed to store conversation turn: ${e}`);
5189
+ }
5190
+ }
5191
+ return { ...queryResult, tokenUsage: totalUsage };
5192
+ }
5193
+ async function classify(question, conversationId) {
5194
+ let conversationContext;
5195
+ if (conversationId) {
5196
+ try {
5197
+ const history = await memoryStore.getHistory(conversationId, { limit: 5 });
5198
+ if (history.length > 0) {
5199
+ conversationContext = history.map((t) => `[${t.role}]: ${t.content}`).join("\n");
5200
+ }
5201
+ } catch {
5202
+ }
5203
+ }
5204
+ const prompt = buildQueryClassifyPrompt(question, conversationContext);
5205
+ const { object, usage } = await withRetry(
5206
+ () => generateObject({
5207
+ prompt,
5208
+ schema: QueryClassifyResultSchema,
5209
+ maxTokens: 2048,
5210
+ providerOptions
5211
+ })
5212
+ );
5213
+ trackUsage(usage);
5214
+ return object;
5215
+ }
5216
+ async function respond(originalQuestion, subAnswers, classification, platform) {
5217
+ const subAnswersJson = JSON.stringify(
5218
+ subAnswers.map((sa) => ({
5219
+ subQuestion: sa.subQuestion,
5220
+ answer: sa.answer,
5221
+ citations: sa.citations,
5222
+ confidence: sa.confidence,
5223
+ needsMoreContext: sa.needsMoreContext
5224
+ })),
5225
+ null,
5226
+ 2
5227
+ );
5228
+ const prompt = buildRespondPrompt(originalQuestion, subAnswersJson, platform);
5229
+ const { object, usage } = await withRetry(
5230
+ () => generateObject({
5231
+ prompt,
5232
+ schema: QueryResultSchema,
5233
+ maxTokens: 4096,
5234
+ providerOptions
5235
+ })
5236
+ );
5237
+ trackUsage(usage);
5238
+ const result = object;
5239
+ result.intent = classification.intent;
5240
+ return result;
5241
+ }
5242
+ return { query };
5243
+ }
5244
+
3756
5245
  // src/prompts/intent.ts
3757
5246
  function buildClassifyMessagePrompt(platform) {
3758
5247
  const platformFields = {
@@ -3879,9 +5368,16 @@ var AGENT_TOOLS = [
3879
5368
  AGENT_TOOLS,
3880
5369
  APPLICATION_CLASSIFY_PROMPT,
3881
5370
  AUDIT_TYPES,
5371
+ AcroFormMappingSchema,
3882
5372
  AddressSchema,
3883
5373
  AdmittedStatusSchema,
5374
+ AnswerParsingResultSchema,
5375
+ ApplicationClassifyResultSchema,
5376
+ ApplicationFieldSchema,
5377
+ ApplicationStateSchema,
3884
5378
  AuditTypeSchema,
5379
+ AutoFillMatchSchema,
5380
+ AutoFillResultSchema,
3885
5381
  BOAT_TYPES,
3886
5382
  BindingAuthoritySchema,
3887
5383
  BoatTypeSchema,
@@ -3895,6 +5391,7 @@ var AGENT_TOOLS = [
3895
5391
  COVERAGE_FORMS,
3896
5392
  COVERAGE_TRIGGERS,
3897
5393
  ChunkTypeSchema,
5394
+ CitationSchema,
3898
5395
  ClaimRecordSchema,
3899
5396
  ClaimStatusSchema,
3900
5397
  ClassificationCodeSchema,
@@ -3937,12 +5434,16 @@ var AGENT_TOOLS = [
3937
5434
  EnrichedSubjectivitySchema,
3938
5435
  EnrichedUnderwritingConditionSchema,
3939
5436
  EntityTypeSchema,
5437
+ EvidenceItemSchema,
3940
5438
  ExclusionSchema,
3941
5439
  ExperienceModSchema,
3942
5440
  ExtendedReportingPeriodSchema,
3943
5441
  FLOOD_ZONES,
3944
5442
  FOUNDATION_TYPES,
3945
5443
  FarmRanchDeclarationsSchema,
5444
+ FieldExtractionResultSchema,
5445
+ FieldTypeSchema,
5446
+ FlatPdfPlacementSchema,
3946
5447
  FloodDeclarationsSchema,
3947
5448
  FloodZoneSchema,
3948
5449
  FormReferenceSchema,
@@ -3961,6 +5462,9 @@ var AGENT_TOOLS = [
3961
5462
  LimitScheduleSchema,
3962
5463
  LimitTypeSchema,
3963
5464
  LocationPremiumSchema,
5465
+ LookupFillResultSchema,
5466
+ LookupFillSchema,
5467
+ LookupRequestSchema,
3964
5468
  LossSettlementSchema,
3965
5469
  LossSummarySchema,
3966
5470
  NamedInsuredSchema,
@@ -3970,6 +5474,7 @@ var AGENT_TOOLS = [
3970
5474
  POLICY_SECTION_TYPES,
3971
5475
  POLICY_TERM_TYPES,
3972
5476
  POLICY_TYPES,
5477
+ ParsedAnswerSchema,
3973
5478
  PaymentInstallmentSchema,
3974
5479
  PaymentPlanSchema,
3975
5480
  PersonalArticlesDeclarationsSchema,
@@ -3989,6 +5494,10 @@ var AGENT_TOOLS = [
3989
5494
  ProducerInfoSchema,
3990
5495
  ProfessionalLiabilityDeclarationsSchema,
3991
5496
  QUOTE_SECTION_TYPES,
5497
+ QueryClassifyResultSchema,
5498
+ QueryIntentSchema,
5499
+ QueryResultSchema,
5500
+ QuestionBatchResultSchema,
3992
5501
  QuoteDocumentSchema,
3993
5502
  QuoteSectionTypeSchema,
3994
5503
  RATING_BASIS_TYPES,
@@ -3998,12 +5507,16 @@ var AGENT_TOOLS = [
3998
5507
  RatingBasisSchema,
3999
5508
  RatingBasisTypeSchema,
4000
5509
  RecreationalVehicleDeclarationsSchema,
5510
+ ReplyIntentSchema,
5511
+ RetrievalResultSchema,
4001
5512
  RoofTypeSchema,
4002
5513
  SCHEDULED_ITEM_CATEGORIES,
4003
5514
  SUBJECTIVITY_CATEGORIES,
4004
5515
  ScheduledItemCategorySchema,
4005
5516
  SectionSchema,
4006
5517
  SharedLimitSchema,
5518
+ SubAnswerSchema,
5519
+ SubQuestionSchema,
4007
5520
  SubjectivityCategorySchema,
4008
5521
  SubjectivitySchema,
4009
5522
  SublimitSchema,
@@ -4020,6 +5533,7 @@ var AGENT_TOOLS = [
4020
5533
  ValuationMethodSchema,
4021
5534
  VehicleCoverageSchema,
4022
5535
  VehicleCoverageTypeSchema,
5536
+ VerifyResultSchema,
4023
5537
  WatercraftDeclarationsSchema,
4024
5538
  WorkersCompDeclarationsSchema,
4025
5539
  buildAcroFormMappingPrompt,
@@ -4039,12 +5553,18 @@ var AGENT_TOOLS = [
4039
5553
  buildIdentityPrompt,
4040
5554
  buildIntentPrompt,
4041
5555
  buildLookupFillPrompt,
5556
+ buildQueryClassifyPrompt,
4042
5557
  buildQuestionBatchPrompt,
4043
5558
  buildQuotesPoliciesPrompt,
5559
+ buildReasonPrompt,
4044
5560
  buildReplyIntentClassificationPrompt,
5561
+ buildRespondPrompt,
4045
5562
  buildSafetyPrompt,
5563
+ buildVerifyPrompt,
4046
5564
  chunkDocument,
5565
+ createApplicationPipeline,
4047
5566
  createExtractor,
5567
+ createQueryAgent,
4048
5568
  extractPageRange,
4049
5569
  fillAcroForm,
4050
5570
  getAcroFormFields,