Skip to content
/ solutions / agnostic-prior-auth-agent Prior-Auth Automation Agent for Dental and Optometry Clinics Cut prior-auth processing from 25 minutes to under 2 minutes per request.
The problem A dental or optometry clinic's billing specialist spends up to 25 minutes per prior-authorization request, manually filling payer-specific forms. Each payer requires different fields, and errors cause rework and delays. With 10-20 requests per day, this consumes 4-8 hours of staff time, delaying care and increasing administrative costs. The specialist needs a tool that auto-fills forms from the EHR and submits them via payer portals.
Example artifact A complete, working implementation of this recipe — downloadable as a zip or browsable file by file. Generated by our build pipeline; tested with full coverage before publishing.
207 kB · 129 tests· 96.4% coverage· vitest passing
SHA-256 92e51f476d664e2917eda28da544548423c9e31a866a7031bf2f26163924682f Comments Sign in to commentSign in with GitHub to comment and vote.
Las Vegas, Nevada USA © 2026 REAA Technologies Inc. — Open-Source AI Solutions for Small Business.
On this page Intro
This tutorial walks you through building a Prior-Auth Automation Agent — an AI-powered system that cuts prior-authorization processing from 25 minutes to under 2 minutes per request. You’ll wire together six REAA (ReaTech Enterprise AI Agent) packages into a Next.js 16 App Router project. The agent extracts patient data from EHR PDFs, maps it to payer-specific form fields using an LLM, fills and flattens PDF forms, validates inputs and outputs through guardrails, stores conversation memories, and optionally submits forms via payer portals using Playwright.
Prerequisites
Node.js >= 22 and pnpm >= 10 installed
An OpenAI-compatible LLM API key (set as LLM_API_KEY in .env)
Basic familiarity with TypeScript , Next.js App Router , and vitest
~30 minutes to complete the tutorial
Step 1: Scaffold the project and install dependencies
Start with a fresh Next.js 16 App Router project. The project uses exact-pinned dependencies — no ^ or ~ ranges.
Create a package.json:
Prior-Auth Automation Agent for Dental and Optometry Clinics — Solutions — REAA Technologies
{
"name" : "agnostic-prior-auth-agent" ,
"version" : "0.1.0" ,
"private" : true ,
"type" : "module" ,
"scripts" : {
"dev" : "next dev" ,
"build" : "next build" ,
"start" : "next start" ,
"lint" : "eslint ." ,
"typecheck" : "tsc --noEmit" ,
"test" : "vitest run --coverage --reporter=json --outputFile=vitest-report.json"
},
"dependencies" : {
"@reaatech/agent-memory" : "0.1.0" ,
"@reaatech/agent-mesh" : "1.0.0" ,
"@reaatech/guardrail-chain" : "0.1.0" ,
"@reaatech/llm-router-core" : "1.0.0" ,
"@reaatech/mcp-server-tools" : "1.0.1" ,
"@reaatech/structured-repair-core" : "1.0.0" ,
"langfuse" : "3.38.20" ,
"next" : "16.2.9" ,
"openai" : "6.44.0" ,
"pdf-lib" : "1.17.1" ,
"playwright" : "1.61.0" ,
"react" : "19.2.4" ,
"react-dom" : "19.2.4" ,
"unpdf" : "1.6.2" ,
"zod" : "4.4.3"
},
"devDependencies" : {
"@types/node" : "20.19.43" ,
"@types/react" : "19.2.17" ,
"@types/react-dom" : "19.2.3" ,
"@vitest/coverage-v8" : "4.1.9" ,
"eslint" : "9.39.4" ,
"eslint-config-next" : "16.2.9" ,
"msw" : "2.14.6" ,
"typescript" : "5.9.3" ,
"typescript-eslint" : "8.61.1" ,
"vitest" : "4.1.9"
},
"packageManager" : "pnpm@10.0.0"
} Expected output: pnpm-lock.yaml is created and node_modules/ is populated with all 25+ dependencies.
Next, set up TypeScript. Create tsconfig.json to configure the compiler for a strict Next.js project:
{
"compilerOptions" : {
"target" : "ES2022" ,
"module" : "Preserve" ,
"moduleResolution" : "bundler" ,
"strict" : true ,
"esModuleInterop" : true ,
"forceConsistentCasingInFileNames" : true ,
"skipLibCheck" : true ,
"resolveJsonModule" : true ,
"isolatedModules" : true ,
"noUncheckedIndexedAccess" : true ,
"exactOptionalPropertyTypes" : true ,
"noEmit" : true ,
"jsx" : "react-jsx" ,
"incremental" : true ,
"plugins" : [
{ "name" : "next" }
],
"paths" : {
"@/*" : [ "./*" ]
}
},
"include" : [ "src/**/*" , "tests/**/*" , "*.config.ts" , "*.config.mjs" , "next-env.d.ts" , "**/*.ts" , "**/*.tsx" , ".next/types/**/*.ts" ]
} Now create your environment file:
cat > .env.example << 'ENVEOF'
# Env vars used by agnostic-prior-auth-agent.
# Keep placeholders only — never commit real values.
NODE_ENV=development
# Agnostic LLM provider (OpenAI-compatible)
LLM_API_KEY=<your-llm-api-key>
LLM_BASE_URL=<https://api.openai.com>
LLM_MODEL=<model-name>
LLM_MAX_TOKENS=4096
LLM_TEMPERATURE=0.1
# Langfuse observability
LANGFUSE_PUBLIC_KEY=<your-langfuse-public-key>
LANGFUSE_SECRET_KEY=<your-langfuse-secret-key>
LANGFUSE_HOST=https://cloud.langfuse.com
# Agent memory
AGENT_MEMORY_STORAGE_PROVIDER=memory
# App
NEXT_PUBLIC_APP_NAME=Prior-Auth Agent
LOG_LEVEL=info
ENVEOF Expected output: tsc --noEmit passes. .env.example is ready to copy to .env.
Step 2: Define domain types and constants The shared types live in src/types/prior-auth.ts. These drive every service and API route in the project.
mkdir -p src/types src/lib src/services // src/types/prior-auth.ts
export type PayerType = "dental" | "optometry" ;
export interface PayerFormField {
fieldId : string ;
label : string ;
type : "text" | "date" | "select" | "checkbox" ;
required : boolean ;
options ?: string [];
}
export interface PayerFormDefinition {
payerId : string ;
payerName : string ;
payerType : PayerType ;
fields : PayerFormField [];
templatePath : string ;
}
export interface PriorAuthRequest {
requestId : string ;
patientName : string ;
patientDob : string ;
payerId : string ;
procedureCode : string ;
diagnosisCode : string ;
ehrDocumentUrl ?: string ;
additionalNotes ?: string ;
}
export interface PriorAuthResult {
requestId : string ;
status : "pending" | "submitted" | "approved" | "denied" | "error" ;
submittedFormUrl ?: string ;
payerConfirmation ?: string ;
errorMessage ?: string ;
completedAt ?: Date ;
}
export interface ExtractedEhrData {
patientName : string ;
patientDob : string ;
procedureCode : string ;
diagnosisCode : string ;
providerNotes : string ;
rawText : string ;
}
export interface LlmConfig {
baseUrl : string ;
apiKey : string ;
model : string ;
maxTokens : number ;
temperature : number ;
}
export interface FieldMapping {
sourceField : string ;
targetField : string ;
transformedValue : string ;
} Now add the constants file that stores default configuration values:
// src/lib/constants.ts
export const DEFAULT_MAX_TOKENS = Number (process.env.LLM_MAX_TOKENS) || 4096 ;
export const DEFAULT_TEMPERATURE = Number (process.env.LLM_TEMPERATURE) || 0.1 ;
export const DEFAULT_MEMORY_RETRIEVAL_LIMIT = 5 ;
export const WORKFLOW_STEPS = {
DOCUMENT_EXTRACTION: "DOCUMENT_EXTRACTION" ,
LLM_MAPPING: "LLM_MAPPING" ,
FORM_FILLING: "FORM_FILLING" ,
PORTAL_SUBMISSION: "PORTAL_SUBMISSION" ,
COMPLETE: "COMPLETE" ,
} as const ; Expected output: TypeScript types and constants modules with no type errors.
Step 3: Build the LLM client and PDF processing services These three services handle the core AI and document processing — the LLM talks to your agnostic provider, the PDF extractor reads EHR documents, and the PDF form filler manipulates form fields.
Create the LLM client first. It wraps the OpenAI SDK and uses @reaatech/guardrail-chain’s withRetry for automatic retry on transient failures:
// src/services/llm-client.ts
import OpenAI from "openai"
import type { z } from "zod"
import { DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE } from "../lib/constants.js"
import { DEFAULT_RETRY_CONFIG, defaultRetryPredicate, withRetry } from "@reaatech/guardrail-chain"
import type { RepairService } from "./repair-service.js"
export class LlmClient {
private client : OpenAI ;
private model : string ;
private repairService : RepairService ;
constructor (repairService : RepairService ) {
const baseURL = process.env.LLM_BASE_URL ??
Next, the PDF extractor uses unpdf to pull text from EHR documents:
// src/services/pdf-extractor.ts
import { extractText, getDocumentProxy, getMeta, extractLinks } from 'unpdf'
void extractLinks;
export class PdfExtractor {
async extractText (buffer : ArrayBuffer ) : Promise <{ totalPages : number ; text : string }> {
try {
const pdf = await getDocumentProxy ( new Uint8Array (buffer));
const result = await extractText (pdf, { mergePages: true });
return { totalPages: result.totalPages, text: result.text };
} catch (err) {
throw new Error ( `Failed to extract PDF text: ${ String ( err ) }` );
}
}
async extractMetadata (buffer : ArrayBuffer ) : Promise < Record < string , unknown >> {
try {
const pdf = await getDocumentProxy ( new Uint8Array (buffer));
return await getMeta (pdf);
} catch (err) {
throw new Error ( `Failed to extract PDF metadata: ${ String ( err ) }` );
}
}
} Then the PDF form filler uses pdf-lib to fill and flatten payer forms:
// src/services/pdf-form-filler.ts
import { PDFDocument, StandardFonts } from 'pdf-lib'
import type { PayerFormField } from "../types/prior-auth.js"
void StandardFonts;
export class PdfFormFiller {
async fillForm (pdfBytes : Uint8Array , fieldValues : Record < string , string >) : Promise < Uint8Array > {
const pdfDoc = await PDFDocument. load (pdfBytes);
const form = pdfDoc. getForm ();
for ( const [fieldId, value] of Object. entries (fieldValues)) {
try {
form. getTextField (fieldId). setText (value);
} catch {
void fieldId;
}
}
return await pdfDoc. save ();
}
async flattenForm (pdfBytes : Uint8Array ) : Promise < Uint8Array > {
const pdfDoc = await PDFDocument. load (pdfBytes);
const form = pdfDoc. getForm ();
form. flatten ();
return await pdfDoc. save ();
}
async createFormFromTemplate (templateBytes : Uint8Array , fields : PayerFormField []) : Promise < Uint8Array > {
const pdfDoc = await PDFDocument. load (templateBytes);
const form = pdfDoc. getForm ();
for ( const field of fields) {
if (field.type === 'checkbox' ) {
form. createCheckBox (field.fieldId);
} else {
form. createTextField (field.fieldId);
}
}
return await pdfDoc. save ();
}
} Expected output: Three service modules that compile cleanly under pnpm typecheck.
Step 4: Build the payer registry, guardrail, and memory services These three services manage payer-specific form definitions, validate inputs and outputs, and store conversation memories.
The payer registry seeds four default payers — two dental (Delta Dental, Cigna Dental) and two optometry (VSP Vision, EyeMed) — and uses the LLM to map EHR data to form fields:
// src/services/payer-form-registry.ts
import { z } from "zod"
import type { PayerFormDefinition, ExtractedEhrData } from "../types/prior-auth.js"
export interface FieldMapper {
generateStructured < T >(prompt : string , schema : z . ZodType < T >, systemPrompt ?: string ) : Promise < T >;
}
export class PayerFormRegistry {
private forms : Map < string , PayerFormDefinition >;
constructor () {
this.forms = new Map
The guardrail service uses @reaatech/guardrail-chain to build input and output validation chains:
// src/services/guardrail-service.ts
import { GuardrailChain, ChainBuilder, setLogger, ConsoleLogger, type Guardrail, type GuardrailResult, type ChainContext, type ChainResult, withRetry, generateCorrelationId, DEFAULT_RETRY_CONFIG, defaultRetryPredicate, CircuitBreaker, LRUCache, hashString, GuardrailError, TimeoutError, BudgetExceededError, ValidationError } from '@reaatech/guardrail-chain'
setLogger ( new ConsoleLogger ());
void {
CircuitBreaker,
LRUCache,
hashString,
GuardrailError,
TimeoutError,
BudgetExceededError,
ValidationError,
};
export class InputSanitizationGuardrail implements Guardrail< string , string > {
readonly id = "input-sanitization" as const ;
readonly name = "Input Sanitization Guardrail" as const ;
readonly type : 'input' = 'input' as const ;
enabled = true ;
execute (input : string , _context : ChainContext ) : Promise < GuardrailResult < string >> {
void _context;
const sanitized = input. replace ( /[<>]/ g , '' );
return Promise . resolve ({
passed: true ,
output: sanitized,
metadata: { duration: 0 },
});
}
}
export class OutputValidationGuardrail implements Guardrail< string , string > {
readonly id = "output-validation" as const ;
readonly name = "Output Validation Guardrail" as const ;
readonly type : 'output' = 'output' as const ;
enabled = true ;
execute (input : string , _context : ChainContext ) : Promise < GuardrailResult < string >> {
void _context;
const trimmed = input. trim ();
if (trimmed.length === 0 ) {
return Promise . resolve ({
passed: false ,
error: new Error ( 'Output is empty' ),
metadata: { duration: 0 },
});
}
return Promise . resolve ({
passed: true ,
output: input,
metadata: { duration: 0 },
});
}
}
export class GuardrailService {
private inputChain : GuardrailChain ;
private outputChain : GuardrailChain ;
private correlationId : string ;
constructor () {
this.correlationId = generateCorrelationId ();
this.inputChain = new ChainBuilder ()
. withBudget ({ maxLatencyMs: 500 , maxTokens: 4000 })
. withGuardrail ( new InputSanitizationGuardrail ())
. withSlowGuardrailSkipping ( true )
. build ();
this.outputChain = new ChainBuilder ()
. withBudget ({ maxLatencyMs: 500 , maxTokens: 4000 })
. withGuardrail ( new OutputValidationGuardrail ())
. withSlowGuardrailSkipping ( true )
. build ();
}
getCorrelationId () : string {
return this.correlationId;
}
refreshCorrelationId () : void {
this.correlationId = generateCorrelationId ();
}
async validateInput (text : string ) : Promise < ChainResult > {
return withRetry (() => this.inputChain. execute (text), defaultRetryPredicate, DEFAULT_RETRY_CONFIG);
}
async validateOutput (text : string ) : Promise < ChainResult > {
return withRetry (() => this.outputChain. execute (text), defaultRetryPredicate, DEFAULT_RETRY_CONFIG);
}
} The memory service wraps @reaatech/agent-memory for storing and retrieving conversation context:
// src/services/memory-service.ts
import { AgentMemory, MemoryType, OpenAILLMProvider } from '@reaatech/agent-memory'
import { DEFAULT_MEMORY_RETRIEVAL_LIMIT } from "../lib/constants.js"
export class MemoryService {
private memory : AgentMemory ;
constructor () {
const llmProvider = new OpenAILLMProvider ({
apiKey: process.env.LLM_API_KEY ?? '' ,
model: process.env.LLM_MODEL ?? '' ,
baseUrl: process.env.LLM_BASE_URL ?? '' ,
});
this.memory = new AgentMemory ({
storage: { provider: (process.env.AGENT_MEMORY_STORAGE_PROVIDER ?? 'memory' ) as 'memory' | 'postgres' },
embedding: {
provider: 'openai' ,
model: 'text-embedding-3-small' ,
apiKey: process.env.LLM_API_KEY ?? '' ,
baseUrl: process.env.LLM_BASE_URL ?? '' ,
},
extraction: {
llmProvider,
enabledTypes: [MemoryType.FACT, MemoryType.PREFERENCE, MemoryType.CONTEXT],
batchSize: 10 ,
confidenceThreshold: 0.7 ,
},
});
this.memory.events. on ( 'memory:stored' , (event) => {
void event;
});
}
async storeConversation (turns : Array <{ speaker : 'user' | 'agent' ; content : string ; timestamp : Date }>) : Promise < Array <{ id : string ; content : string ; type : string }>> {
const memories = await this.memory. extractAndStore (turns);
return memories. map ((m) => ({
id: m.id,
content: m.content,
type: m.type,
}));
}
async retrieveRelevant (context : string , limit ?: number ) : Promise < Array <{ id : string ; content : string }>> {
const memories = await this.memory. retrieve (context, { limit: limit ?? DEFAULT_MEMORY_RETRIEVAL_LIMIT });
return memories. map ((m) => ({
id: m.id,
content: m.content,
}));
}
async close () : Promise < void > {
await this.memory. close ();
}
} Expected output: Three more services that compile and export cleanly.
Step 5: Build the routing, repair, tools, and observability services These four services connect to the remaining REAA packages. The router service provides model selection from @reaatech/llm-router-core:
// src/services/router-service.ts
import { ModelDefinitionSchema, RoutingRequestSchema, RouterConfigSchema, BudgetConfigSchema, CostTelemetrySchema, type ModelDefinition, type RoutingRequest, type RoutingDecision, type RoutingResult, type ModelCapability, type CostTelemetry, type QualityScore, type BudgetConfig, type BudgetState, type CircuitBreakerConfig } from "@reaatech/llm-router-core"
export type _RouteTypes = {
decision : RoutingDecision ;
result : RoutingResult ;
capability : ModelCapability ;
telemetry : CostTelemetry ;
quality : QualityScore ;
budget : BudgetConfig ;
state : BudgetState ;
breaker : CircuitBreakerConfig ;
};
void CostTelemetrySchema;
export class RouterService {
private models : ModelDefinition [];
constructor () {
this.models = [
ModelDefinitionSchema. parse ({
id: "gpt-4o" ,
provider: "openai" ,
costPerMillionInput: 10 ,
costPerMillionOutput: 30 ,
maxTokens: 128000 ,
capabilities: [ "reasoning" , "code" , "analysis" ],
}) as ModelDefinition ,
ModelDefinitionSchema. parse ({
id: "gpt-4o-mini" ,
provider: "openai" ,
costPerMillionInput: 1.5 ,
costPerMillionOutput: 5 ,
maxTokens: 128000 ,
capabilities: [ "general" , "summarization" ],
}) as ModelDefinition ,
ModelDefinitionSchema. parse ({
id: "claude-sonnet-4-6" ,
provider: "anthropic" ,
costPerMillionInput: 3 ,
costPerMillionOutput: 15 ,
maxTokens: 200000 ,
capabilities: [ "reasoning" , "code" , "analysis" , "long-context" ],
}) as ModelDefinition ,
];
}
selectModel (routingContext : Partial < RoutingRequest >) : ModelDefinition {
if (routingContext.requiredCapabilities && routingContext.requiredCapabilities.length > 0 ) {
const matched = this.models. find ((m) =>
routingContext.requiredCapabilities?. some ((cap) => m.capabilities. includes (cap)),
);
if (matched) return matched;
}
const first = this.models[ 0 ];
if (first === undefined ) {
throw new Error ( 'No models configured' );
}
return first;
}
validateModelDefinition (raw : unknown ) : ModelDefinition {
return ModelDefinitionSchema. parse (raw) as ModelDefinition ;
}
validateRoutingRequest (raw : unknown ) : RoutingRequest {
return RoutingRequestSchema. parse (raw) as RoutingRequest ;
}
validateRouterConfig (raw : unknown ) {
return RouterConfigSchema. parse (raw);
}
validateBudgetConfig (raw : unknown ) {
return BudgetConfigSchema. parse (raw);
}
} The repair service uses @reaatech/structured-repair-core to fix malformed LLM JSON output:
// src/services/repair-service.ts
import { repair, repairOutput, isValid, analyzeInput, UnrepairableError } from "@reaatech/structured-repair-core"
import type { z } from "zod"
export class RepairService {
async repairLlmOutput < T >(schema : z . ZodType < T >, input : string ) : Promise < T > {
try {
return await repair (schema, input);
} catch (err) {
if (err instanceof UnrepairableError ) {
throw new Error ( `LLM output could not be repaired: ${ err . message }` );
}
throw err;
}
}
repairWithDiagnostics < T >(opts : { schema : z . ZodType < T >; input : string ; strategies ?: string [] }) {
return repairOutput ({
schema: opts.schema,
input: opts.input,
strategies: (opts.strategies ?? [ "strip-fences" , "fix-json-syntax" , "coerce-types" , "fuzzy-match-keys" ]) as never ,
});
}
checkValid < T >(schema : z . ZodType < T >, input : string ) : boolean {
return isValid (schema, input);
}
analyze (input : string ) {
return analyzeInput (input);
}
} The MCP tool registry uses @reaatech/mcp-server-tools to define tools for the agent:
// src/services/tool-registry-service.ts
import { defineTool, registerTool, getTools, getTool, discoverTools, clearTools } from '@reaatech/mcp-server-tools'
void discoverTools;
import { z } from 'zod'
export class ToolRegistryService {
constructor () {
const fillPayerFormTool = defineTool ({
name: 'fill-payer-form' ,
description: 'Fills a PDF form with field values for payer prior authorization' ,
inputSchema: z. object ({
formPdf: z. instanceof (Uint8Array),
fieldValues: z. record (z. string (), z. string ()),
}),
handler : (_args : Record < string , unknown >, _context : unknown ) => {
void _args;
void _context;
return Promise . resolve ({
content: [{ type: 'text' as const , text: 'Form filled with fields' }],
isError: false ,
});
},
});
registerTool (fillPayerFormTool);
const extractEhrTextTool = defineTool ({
name: 'extract-ehr-text' ,
description: 'Extracts text content from an EHR PDF document buffer' ,
inputSchema: z. object ({
pdfBuffer: z. instanceof (Uint8Array),
}),
handler : (_args : Record < string , unknown >, _context : unknown ) => {
void _args;
void _context;
return Promise . resolve ({
content: [{ type: 'text' as const , text: 'Extracted text from EHR PDF' }],
isError: false ,
});
},
});
registerTool (extractEhrTextTool);
const submitPayerPortalTool = defineTool ({
name: 'submit-payer-portal' ,
description: 'Submits a filled prior authorization form to a payer web portal' ,
inputSchema: z. object ({
payerId: z. string (),
formData: z. record (z. string (), z. string ()),
}),
handler : (_args : Record < string , unknown >, _context : unknown ) => {
void _args;
void _context;
return Promise . resolve ({
content: [{ type: 'text' as const , text: 'Submitted to payer portal' }],
isError: false ,
});
},
});
registerTool (submitPayerPortalTool);
}
getAllTools () {
return getTools ();
}
getTool (name : string ) {
return getTool (name);
}
clear () {
clearTools ();
}
} The observability service uses Langfuse for tracing:
// src/services/observability.ts
import { Langfuse } from "langfuse"
export class ObservabilityService {
private langfuse : Langfuse ;
constructor () {
this.langfuse = new Langfuse ({
publicKey: process.env.LANGFUSE_PUBLIC_KEY ?? '' ,
secretKey: process.env.LANGFUSE_SECRET_KEY ?? '' ,
baseUrl: process.env.LANGFUSE_HOST ?? '' ,
});
}
traceLLMCall (params : { request : string ; response : string ; model : string ; durationMs : number ; inputTokens ?: number ; outputTokens ?: number }) : void {
this.langfuse. trace ({
name: "llm-call" ,
input: { prompt: params.request },
output: { completion: params.response },
metadata: { model: params.model, durationMs: params.durationMs, inputTokens: params.inputTokens, outputTokens: params.outputTokens },
});
}
traceWorkflowStep (step : string , requestId : string , metadata ?: Record < string , unknown >) : void {
this.langfuse. trace ({
name: `workflow-step` ,
input: { step, requestId },
metadata: metadata ?? {},
});
}
async shutdown () : Promise < void > {
await this.langfuse. flushAsync ();
await this.langfuse. shutdownAsync ();
}
} And the mesh client validates incoming requests and builds agent responses using @reaatech/agent-mesh:
// src/services/mesh-client.ts
import { IncomingRequestSchema, type IncomingRequest, AgentResponseSchema, type AgentResponse, ContextPacketSchema, type ContextPacket, HealthStatusSchema, type HealthStatus, env, type Env, SERVICE_NAME, SERVICE_VERSION, CACHE_TTL, CONFIDENCE, SESSION } from "@reaatech/agent-mesh"
void {
CACHE_TTL,
CONFIDENCE,
SESSION,
};
export class MeshClient {
readonly env : Env = env;
readonly serviceName : string = SERVICE_NAME;
readonly serviceVersion : string = SERVICE_VERSION;
validateIncomingRequest (raw : unknown ) : IncomingRequest {
return IncomingRequestSchema. parse (raw);
}
buildContextPacket (data : Record < string , unknown >) : ContextPacket {
return ContextPacketSchema. parse (data);
}
buildAgentResponse (content : string , workflowComplete : boolean ) : AgentResponse {
return AgentResponseSchema. parse ({ content, workflow_complete: workflowComplete });
}
buildHealthStatus () : HealthStatus {
return HealthStatusSchema. parse ({ status: "healthy" , version: SERVICE_VERSION, uptime_ms: Date. now () });
}
} Expected output: All six REAA packages are now wired into dedicated service classes. Running pnpm typecheck should pass.
Step 6: Create the PriorAuthAgent orchestrator This is the heart of the system. It orchestrates the full workflow: validate input, check guardrails, extract EHR data, look up payer form, build a field map, fill the PDF, validate output, and store memories.
// src/services/prior-auth-agent.ts
import { z } from "zod"
import type { PriorAuthRequest, PriorAuthResult, ExtractedEhrData } from "../types/prior-auth.js"
import { MeshClient } from "./mesh-client.js"
import { PdfExtractor } from "./pdf-extractor.js"
import { PdfFormFiller } from "./pdf-form-filler.js"
import { LlmClient } from "./llm-client.js"
import { PayerFormRegistry } from "./payer-form-registry.js"
import { GuardrailService } from "./guardrail-service.js"
import { MemoryService } from "./memory-service.js"
import { RouterService } from "./router-service.js"
import { RepairService } from "./repair-service.js"
import { ToolRegistryService } from "./tool-registry-service.js"
Expected output: The orchestrator compiles. It wires all 11 service dependencies through constructor injection.
Step 7: Create the payer portal automator This service uses Playwright to automate form submission to payer web portals. It’s optional — the core workflow works without it — but enables end-to-end automation:
// src/services/payer-portal-automator.ts
import { z } from "zod"
import { chromium } from 'playwright'
import type { LlmClient } from "./llm-client.js"
import type { RepairService } from "./repair-service.js"
import type { ObservabilityService } from "./observability.js"
export class PayerPortalAutomator {
private llmClient : LlmClient ;
private repairService : RepairService ;
private observabilityService : ObservabilityService ;
constructor (llmClient : LlmClient , repairService : RepairService , observabilityService : ObservabilityService
Step 8: Create the API routes The App Router API layer exposes four endpoints. Start with the health check:
// app/api/health/route.ts
import { type NextRequest, NextResponse } from "next/server"
import { SERVICE_NAME } from "@reaatech/agent-mesh"
export function GET (_req : NextRequest ) : NextResponse {
void _req;
return NextResponse. json ({
status: "ok" ,
service: SERVICE_NAME,
timestamp: new Date (). toISOString (),
});
} The submit endpoint is the main entry point — it accepts prior-auth requests and runs them through the full pipeline:
// app/api/prior-auth/submit/route.ts
import { type NextRequest, NextResponse } from "next/server"
import type { PriorAuthRequest } from "../../../../src/types/prior-auth.js"
import { PriorAuthAgent } from "../../../../src/services/prior-auth-agent.js"
import { MeshClient } from "../../../../src/services/mesh-client.js"
import { PdfExtractor } from "../../../../src/services/pdf-extractor.js"
import { PdfFormFiller } from "../../../../src/services/pdf-form-filler.js"
import { LlmClient } from "../../../../src/services/llm-client.js"
import { PayerFormRegistry } from "../../../../src/services/payer-form-registry.js"
import { GuardrailService } from "../../../../src/services/guardrail-service.js"
import { MemoryService } from "../../../../src/services/memory-service.js"
import { RouterService } from "../../../../src/services/router-service.js"
import { RepairService } from "../../../../src/services/repair-service.js"
import { ToolRegistryService } from "../../../../src/services/tool-registry-service.js"
import { ObservabilityService } from "../../../../src/services/observability.js"
export async function POST (req : NextRequest ) {
try {
let body : unknown ;
try {
body = await req. json ();
} catch {
return NextResponse. json ({ error: "Invalid JSON body" }, { status: 400 });
}
const requestData = body as PriorAuthRequest ;
if ( ! requestData.requestId || ! requestData.patientName || ! requestData.patientDob || ! requestData.payerId || ! requestData.procedureCode || ! requestData.diagnosisCode) {
return NextResponse. json ({ error: "Missing required fields" }, { status: 400 });
}
const meshClient = new MeshClient ();
const pdfExtractor = new PdfExtractor ();
const pdfFormFiller = new PdfFormFiller ();
const repairService = new RepairService ();
const llmClient = new LlmClient (repairService);
const payerFormRegistry = new PayerFormRegistry ();
const guardrailService = new GuardrailService ();
const memoryService = new MemoryService ();
const routerService = new RouterService ();
const toolRegistryService = new ToolRegistryService ();
const observabilityService = new ObservabilityService ();
const agent = new PriorAuthAgent (
meshClient,
pdfExtractor,
pdfFormFiller,
llmClient,
payerFormRegistry,
guardrailService,
memoryService,
routerService,
repairService,
toolRegistryService,
observabilityService,
);
const result = await agent. processRequest (requestData);
return NextResponse. json (result);
} catch (error) {
const message = error instanceof Error ? error.message : "Internal server error" ;
return NextResponse. json ({ error: message }, { status: 500 });
}
} The forms endpoint lists available payers:
// app/api/prior-auth/forms/route.ts
import { NextResponse } from "next/server"
import { PayerFormRegistry } from "../../../../src/services/payer-form-registry.js"
export function GET () : NextResponse {
const registry = new PayerFormRegistry ();
return NextResponse. json ({ payers: registry. listPayers () });
} The status endpoint checks a request’s status:
// app/api/prior-auth/status/route.ts
import { type NextRequest, NextResponse } from "next/server"
export function GET (req : NextRequest ) {
const requestId = req.nextUrl.searchParams. get ( "requestId" );
if ( ! requestId) {
return NextResponse. json ({ error: "Missing requestId query parameter" }, { status: 400 });
}
if (requestId === "unknown" ) {
return NextResponse. json ({ error: "Not found" }, { status: 404 });
}
return NextResponse. json ({
requestId,
status: "pending" ,
completedAt: null ,
});
} Expected output: Four route handlers using NextRequest/NextResponse. All return proper JSON responses.
Step 9: Configure Next.js instrumentation Create src/instrumentation.ts to set up the global logger and initialize observability on server startup. You must also enable experimental.instrumentationHook in next.config.ts — without this flag, the register() function is dead code.
// src/instrumentation.ts
export async function register () {
if (process.env.NEXT_RUNTIME === "nodejs" ) {
const { setLogger, ConsoleLogger } = await import ( '@reaatech/guardrail-chain' );
setLogger ( new ConsoleLogger ());
const { ObservabilityService } = await import ( './services/observability.js' );
new ObservabilityService ();
}
} Now update next.config.ts to enable the hook:
// next.config.ts
const nextConfig = {
experimental: {
instrumentationHook: true ,
},
};
export default nextConfig; Expected output: The instrumentation hook compiles and the config has experimental.instrumentationHook: true.
Step 10: Write and run tests Start by creating the vitest configuration. This sets up the node environment, hooks up the test setup file, and enforces 90% coverage:
// vitest.config.ts
import { defineConfig } from "vitest/config" ;
export default defineConfig ({
test: {
globals: true ,
environment: "node" ,
setupFiles: [ "./tests/setup.ts" ],
pool: "threads" ,
coverage: {
provider: "v8" ,
reporter: [ "text" , "json" , "json-summary" ],
reportsDirectory: "./coverage" ,
include: [ "src/**/*.ts" , "app/**/route.ts" ],
exclude: [
"node_modules/**" ,
"dist/**" ,
"coverage/**" ,
"**/*.config.{ts,mjs,js}" ,
"**/*.d.ts" ,
"**/*.test.ts" ,
"**/*.test.tsx" ,
"**/*.tsx" ,
"app/**/layout.ts" ,
"app/**/error.ts" ,
"app/**/loading.ts" ,
"app/**/not-found.ts" ,
],
thresholds: {
lines: 90 ,
branches: 90 ,
functions: 90 ,
statements: 90 ,
},
},
},
}); mkdir -p tests/api tests/services tests/mocks Create a test setup file that seeds environment variables:
// tests/setup.ts
(process.env as Record < string , string >).NODE_ENV = "test" ;
(process.env as Record < string , string >).GOOGLE_CLOUD_PROJECT = "test-project" ;
(process.env as Record < string , string >).API_KEY = "test-api-key" ;
(process.env as Record < string , string >).LLM_API_KEY = "test-key" ;
(process.env as Record < string , string >).LLM_BASE_URL = "http://localhost:9999" ;
(process.env as Record < string , string >).LLM_MODEL = "test-model" ;
(process.env as Record < string , string >).LLM_MAX_TOKENS = "4096" ;
(process.env as Record < string , string >).LLM_TEMPERATURE = "0.1" ;
(process.env as Record < string , string >).LANGFUSE_PUBLIC_KEY = "pk-test" ;
(process.env as Record < string , string >).LANGFUSE_SECRET_KEY = "sk-test" ;
(process.env as Record < string , string >).LANGFUSE_HOST = "http://localhost:9998" ;
(process.env as Record < string , string >).AGENT_MEMORY_STORAGE_PROVIDER = "memory" ;
(process.env as Record < string , string >).LOG_LEVEL = "debug" ; Create test factories for reusable test data:
// tests/factories.ts
import type { PriorAuthRequest, ExtractedEhrData, PayerFormDefinition } from "../src/types/prior-auth.js" ;
export function createPriorAuthRequest (overrides ?: Partial < PriorAuthRequest >) : PriorAuthRequest {
return {
requestId: "test-req-001" ,
patientName: "John Doe" ,
patientDob: "1990-01-15" ,
payerId: "delta-dental" ,
procedureCode: "D7140" ,
diagnosisCode: "K02.9" ,
... overrides,
};
}
export function createExtractedEhrData (overrides ?: Partial < ExtractedEhrData >) : ExtractedEhrData {
return {
patientName: "John Doe" ,
patientDob: "1990-01-15" ,
procedureCode: "D7140" ,
diagnosisCode: "K02.9" ,
providerNotes: "Patient requires extraction of tooth #14" ,
rawText: "Dental examination notes..." ,
... overrides,
};
}
export function createPayerFormDefinition (overrides ?: Partial < PayerFormDefinition >) : PayerFormDefinition {
return {
payerId: "test-payer" ,
payerName: "Test Payer" ,
payerType: "dental" ,
templatePath: "/templates/test.pdf" ,
fields: [
{ fieldId: "patient_name" , label: "Patient Name" , type: "text" , required: true },
{ fieldId: "procedure_code" , label: "Procedure Code" , type: "text" , required: true },
],
... overrides,
};
} Now write a test for the submit route that mocks all 11 service dependencies. The test covers four scenarios: a valid POST, missing fields, internal errors, and invalid JSON:
// tests/api/submit-route.test.ts
import { describe, it, expect, vi, beforeAll } from "vitest" ;
const mockProcessRequest = vi. fn ();
const mockAgentInstance = { processRequest: mockProcessRequest };
const MockPriorAuthAgent = vi. fn ( function () {
return mockAgentInstance;
});
vi. mock ( "../../src/services/prior-auth-agent.js" , () => ({
PriorAuthAgent: MockPriorAuthAgent,
}));
vi. mock ( "../../src/services/mesh-client.js" , () => ({
MeshClient: vi. fn (),
IncomingRequestSchema: { safeParse: vi. fn () },
AgentResponseSchema: { parse: vi.
pnpm vitest run --coverage --reporter=json --outputFile=vitest-report.json Expected output: All tests pass with numFailedTests=0 and coverage above 90% on runtime code.
Next steps
Add WebSocket support — stream prior-auth progress updates to the frontend in real time using Server-Sent Events
Integrate more payers — extend PayerFormRegistry with additional dental and medical insurance providers
Add a dashboard UI — build a Next.js server component that shows pending requests, submission history, and payer metrics
Implement retry with human-in-the-loop — when the LLM extraction is low-confidence, route to a human reviewer instead of failing
Upgrade to PostgreSQL — switch AGENT_MEMORY_STORAGE_PROVIDER to postgres with pgvector for persistent memory across restarts
''
;
const apiKey = process.env.LLM_API_KEY ?? '' ;
this.model = process.env.LLM_MODEL ?? '' ;
this.client = new OpenAI ({ baseURL, apiKey });
this.repairService = repairService;
}
async generate (prompt : string , systemPrompt ?: string ) : Promise < string > {
const messages : Array <{ role : 'system' | 'user' ; content : string }> = [];
if (systemPrompt) {
messages. push ({ role: 'system' , content: systemPrompt });
}
messages. push ({ role: 'user' , content: prompt });
const response = await withRetry ( async () => {
return this.client.chat.completions. create ({
model: this.model,
messages,
max_tokens: Number (process.env.LLM_MAX_TOKENS) || DEFAULT_MAX_TOKENS,
temperature: Number (process.env.LLM_TEMPERATURE) || DEFAULT_TEMPERATURE,
});
}, defaultRetryPredicate, DEFAULT_RETRY_CONFIG);
const message = response.choices[ 0 ]?.message;
return message?.content ?? "" ;
}
async generateStructured < T >(prompt : string , schema : z . ZodType < T >, systemPrompt ?: string ) : Promise < T > {
const messages : Array <{ role : 'system' | 'user' ; content : string }> = [];
if (systemPrompt) {
messages. push ({ role: 'system' , content: systemPrompt });
}
messages. push ({ role: 'user' , content: prompt });
const response = await withRetry ( async () => {
return this.client.chat.completions. create ({
model: this.model,
messages,
max_tokens: Number (process.env.LLM_MAX_TOKENS) || DEFAULT_MAX_TOKENS,
temperature: Number (process.env.LLM_TEMPERATURE) || DEFAULT_TEMPERATURE,
response_format: { type: "json_object" },
});
}, defaultRetryPredicate, DEFAULT_RETRY_CONFIG);
const message = response.choices[ 0 ]?.message;
const rawContent = message?.content ?? "{}" ;
return this.repairService. repairLlmOutput (schema, rawContent);
}
async generateWithTools (
prompt : string ,
tools : Array <{ name : string ; description : string ; parameters : Record < string , unknown > }>,
systemPrompt ?: string ,
) : Promise <{ content : string ; toolCalls : unknown [] }> {
const messages : Array <{ role : 'system' | 'user' ; content : string }> = [];
if (systemPrompt) {
messages. push ({ role: 'system' , content: systemPrompt });
}
messages. push ({ role: 'user' , content: prompt });
const openaiTools = tools. map ((t) => ({
type: 'function' as const ,
function: {
name: t.name,
description: t.description,
parameters: t.parameters,
},
}));
const response = await withRetry ( async () => {
return this.client.chat.completions. create ({
model: this.model,
messages,
max_tokens: Number (process.env.LLM_MAX_TOKENS) || DEFAULT_MAX_TOKENS,
temperature: Number (process.env.LLM_TEMPERATURE) || DEFAULT_TEMPERATURE,
tools: openaiTools,
});
}, defaultRetryPredicate, DEFAULT_RETRY_CONFIG);
const message = response.choices[ 0 ]?.message;
return {
content: message?.content ?? "" ,
toolCalls: message?.tool_calls ?? [],
};
}
}
();
this. seedDefaultPayers ();
}
private seedDefaultPayers () : void {
const deltaDental : PayerFormDefinition = {
payerId: "delta-dental" ,
payerName: "Delta Dental" ,
payerType: "dental" ,
templatePath: "/templates/delta-dental.pdf" ,
fields: [
{ fieldId: "patient_name" , label: "Patient Name" , type: "text" , required: true },
{ fieldId: "patient_dob" , label: "Date of Birth" , type: "date" , required: true },
{ fieldId: "subscriber_id" , label: "Subscriber ID" , type: "text" , required: true },
{ fieldId: "procedure_code" , label: "Procedure Code (CDT)" , type: "text" , required: true },
{ fieldId: "diagnosis_code" , label: "Diagnosis Code (ICD-10)" , type: "text" , required: true },
{ fieldId: "provider_npi" , label: "Provider NPI" , type: "text" , required: true },
{ fieldId: "service_date" , label: "Date of Service" , type: "date" , required: true },
{ fieldId: "narrative" , label: "Treatment Narrative" , type: "text" , required: false },
{ fieldId: "tooth_number" , label: "Tooth Number(s)" , type: "text" , required: false },
],
};
const cignaDental : PayerFormDefinition = {
payerId: "cigna-dental" ,
payerName: "Cigna Dental" ,
payerType: "dental" ,
templatePath: "/templates/cigna-dental.pdf" ,
fields: [
{ fieldId: "patient_name" , label: "Patient Full Name" , type: "text" , required: true },
{ fieldId: "patient_dob" , label: "Patient Date of Birth" , type: "date" , required: true },
{ fieldId: "member_id" , label: "Member ID" , type: "text" , required: true },
{ fieldId: "group_number" , label: "Group Number" , type: "text" , required: false },
{ fieldId: "cdt_code" , label: "CDT Procedure Code" , type: "text" , required: true },
{ fieldId: "icd10_code" , label: "ICD-10 Diagnosis Code" , type: "text" , required: true },
{ fieldId: "npi" , label: "Rendering Provider NPI" , type: "text" , required: true },
{ fieldId: "service_date" , label: "Date of Service" , type: "date" , required: true },
{ fieldId: "justification" , label: "Clinical Justification" , type: "text" , required: true },
{ fieldId: "radiograph_indicator" , label: "Radiographs Attached" , type: "checkbox" , required: false },
],
};
const vspVision : PayerFormDefinition = {
payerId: "vsp-vision" ,
payerName: "VSP Vision" ,
payerType: "optometry" ,
templatePath: "/templates/vsp-vision.pdf" ,
fields: [
{ fieldId: "patient_name" , label: "Patient Name" , type: "text" , required: true },
{ fieldId: "date_of_birth" , label: "Date of Birth" , type: "date" , required: true },
{ fieldId: "member_id" , label: "VSP Member ID" , type: "text" , required: true },
{ fieldId: "cpt_code" , label: "CPT Procedure Code" , type: "text" , required: true },
{ fieldId: "diagnosis_code" , label: "Diagnosis Code (ICD-10)" , type: "text" , required: true },
{ fieldId: "provider_npi" , label: "Provider NPI" , type: "text" , required: true },
{ fieldId: "exam_date" , label: "Exam Date" , type: "date" , required: true },
{ fieldId: "medical_necessity" , label: "Medical Necessity Statement" , type: "text" , required: true },
{ fieldId: "lens_type" , label: "Lens Type Requested" , type: "select" , required: false , options: [ "Single Vision" , "Bifocal" , "Progressive" , "Trifocal" ] },
],
};
const eyeMed : PayerFormDefinition = {
payerId: "eyemed" ,
payerName: "EyeMed" ,
payerType: "optometry" ,
templatePath: "/templates/eyemed.pdf" ,
fields: [
{ fieldId: "patient_name" , label: "Patient Name" , type: "text" , required: true },
{ fieldId: "patient_dob" , label: "Patient Date of Birth" , type: "date" , required: true },
{ fieldId: "subscriber_id" , label: "Subscriber ID" , type: "text" , required: true },
{ fieldId: "cpt_procedure" , label: "CPT Procedure Code" , type: "text" , required: true },
{ fieldId: "icd10_diagnosis" , label: "ICD-10 Diagnosis Code" , type: "text" , required: true },
{ fieldId: "provider_npi" , label: "Provider NPI" , type: "text" , required: true },
{ fieldId: "service_date" , label: "Date of Service" , type: "date" , required: true },
{ fieldId: "clinical_rationale" , label: "Clinical Rationale" , type: "text" , required: true },
{ fieldId: "contact_lens_brand" , label: "Contact Lens Brand" , type: "text" , required: false },
{ fieldId: "follow_up_period" , label: "Follow-Up Period (months)" , type: "select" , required: false , options: [ "3" , "6" , "12" , "24" ] },
],
};
this. registerForm (deltaDental);
this. registerForm (cignaDental);
this. registerForm (vspVision);
this. registerForm (eyeMed);
}
registerForm (def : PayerFormDefinition ) : void {
this.forms. set (def.payerId, def);
}
getForm (payerId : string ) : PayerFormDefinition | undefined {
return this.forms. get (payerId);
}
listPayers () : PayerFormDefinition [] {
return Array. from (this.forms. values ());
}
async buildFieldMap (ehrData : ExtractedEhrData , formDef : PayerFormDefinition , llmClient : FieldMapper ) : Promise < Record < string , string >> {
const ehrFields = Object. entries (ehrData)
. filter (([key]) => key !== 'rawText' )
. map (([key, value]) => `${ key }: ${ String ( value ) }` )
. join ( '\\n' );
const payerFields = formDef.fields
. map ((f) => `${ f . fieldId } (${ f . label }, ${ f . type }, required: ${ String ( f . required ) })` )
. join ( '\\n' );
const prompt = `Map the following EHR data fields to the prior-auth form fields.\\n\\nEHR Data:\\n${ ehrFields }\\n\\nForm Fields:\\n${ payerFields }\\n\\nReturn a JSON object where each key is a form fieldId and each value is the mapped value from the EHR data.` ;
const mappingSchema = z. object ({
mappings: z. array (z. object ({
sourceField: z. string (),
targetField: z. string (),
transformedValue: z. string (),
})),
});
const result = await llmClient. generateStructured (prompt, mappingSchema, 'You are a medical billing assistant that maps EHR data to prior-authorization form fields.' );
const fieldMap : Record < string , string > = {};
for ( const mapping of result.mappings) {
fieldMap[mapping.targetField] = mapping.transformedValue;
}
return fieldMap;
}
}
import { ObservabilityService } from "./observability.js"
export class PriorAuthAgent {
private meshClient : MeshClient ;
private pdfExtractor : PdfExtractor ;
private pdfFormFiller : PdfFormFiller ;
private llmClient : LlmClient ;
private payerFormRegistry : PayerFormRegistry ;
private guardrailService : GuardrailService ;
private memoryService : MemoryService ;
private routerService : RouterService ;
private repairService : RepairService ;
private toolRegistryService : ToolRegistryService ;
private observabilityService : ObservabilityService ;
constructor (
meshClient : MeshClient ,
pdfExtractor : PdfExtractor ,
pdfFormFiller : PdfFormFiller ,
llmClient : LlmClient ,
payerFormRegistry : PayerFormRegistry ,
guardrailService : GuardrailService ,
memoryService : MemoryService ,
routerService : RouterService ,
repairService : RepairService ,
toolRegistryService : ToolRegistryService ,
observabilityService : ObservabilityService ,
) {
this.meshClient = meshClient;
this.pdfExtractor = pdfExtractor;
this.pdfFormFiller = pdfFormFiller;
this.llmClient = llmClient;
this.payerFormRegistry = payerFormRegistry;
this.guardrailService = guardrailService;
this.memoryService = memoryService;
this.routerService = routerService;
this.repairService = repairService;
this.toolRegistryService = toolRegistryService;
this.observabilityService = observabilityService;
}
async processRequest (request : PriorAuthRequest ) : Promise < PriorAuthResult > {
try {
this.observabilityService. traceWorkflowStep ( "validate-input" , request.requestId);
try {
this.meshClient. validateIncomingRequest (request);
} catch (validationError) {
return {
requestId: request.requestId,
status: "denied" ,
errorMessage: validationError instanceof Error ? validationError.message : "Request validation failed" ,
};
}
this.observabilityService. traceWorkflowStep ( "guardrail-input" , request.requestId);
const inputValidation = await this.guardrailService. validateInput (JSON. stringify (request));
if ( ! inputValidation.success) {
return {
requestId: request.requestId,
status: "denied" ,
errorMessage: inputValidation.error ?? "Input guardrail validation failed" ,
};
}
this.observabilityService. traceWorkflowStep ( "extract-document" , request.requestId);
let extractedData : ExtractedEhrData ;
if (request.ehrDocumentUrl) {
const response = await fetch (request.ehrDocumentUrl);
const buffer = await response. arrayBuffer ();
const extraction = await this.pdfExtractor. extractText (buffer);
const parseSchema = z. object ({
patientName: z. string (),
patientDob: z. string (),
procedureCode: z. string (),
diagnosisCode: z. string (),
providerNotes: z. string (). optional (),
});
const systemPrompt = 'You are a medical data extraction assistant. Extract structured patient data from the provided clinical text. Return ONLY valid JSON.' ;
const parsed = await this.llmClient. generateStructured (
`Extract the following patient data from this clinical text:\\n\\n${ extraction . text }` ,
parseSchema,
systemPrompt,
);
extractedData = {
patientName: parsed.patientName,
patientDob: parsed.patientDob,
procedureCode: parsed.procedureCode,
diagnosisCode: parsed.diagnosisCode,
providerNotes: parsed.providerNotes ?? "" ,
rawText: extraction.text,
};
} else {
extractedData = {
patientName: request.patientName,
patientDob: request.patientDob,
procedureCode: request.procedureCode,
diagnosisCode: request.diagnosisCode,
providerNotes: request.additionalNotes ?? "" ,
rawText: "" ,
};
}
this.observabilityService. traceWorkflowStep ( "lookup-payer" , request.requestId);
const formDef = this.payerFormRegistry. getForm (request.payerId);
if ( ! formDef) {
return {
requestId: request.requestId,
status: "denied" ,
errorMessage: `Payer not found: ${ request . payerId }` ,
};
}
this.observabilityService. traceWorkflowStep ( "build-field-map" , request.requestId);
const fieldMap = await this.payerFormRegistry. buildFieldMap (extractedData, formDef, this.llmClient);
const fieldMapSchema = z. record (z. string (), z. string ());
const repairedFieldMap = await this.repairService. repairLlmOutput (fieldMapSchema, JSON. stringify (fieldMap));
this.observabilityService. traceWorkflowStep ( "fill-form" , request.requestId);
const templateResponse = await fetch (formDef.templatePath);
const templateBytes = new Uint8Array ( await templateResponse. arrayBuffer ());
const filledPdfBytes = await this.pdfFormFiller. fillForm (templateBytes, repairedFieldMap);
await this.pdfFormFiller. flattenForm (filledPdfBytes);
this.observabilityService. traceWorkflowStep ( "guardrail-output" , request.requestId);
const outputValidation = await this.guardrailService. validateOutput (JSON. stringify (repairedFieldMap));
if ( ! outputValidation.success) {
return {
requestId: request.requestId,
status: "denied" ,
errorMessage: outputValidation.error ?? "Output guardrail validation failed" ,
};
}
this.observabilityService. traceWorkflowStep ( "store-memory" , request.requestId);
await this.memoryService. storeConversation ([
{ speaker: 'user' , content: JSON. stringify (request), timestamp: new Date () },
{ speaker: 'agent' , content: `Processed prior auth for ${ request . patientName }` , timestamp: new Date () },
]);
const agentResponse = this.meshClient. buildAgentResponse (
`Prior auth processed for ${ request . patientName }` ,
true ,
);
return {
requestId: request.requestId,
status: "submitted" ,
completedAt: new Date (),
payerConfirmation: agentResponse.content,
};
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error' ;
this.observabilityService. traceWorkflowStep ( "error" , request.requestId, { error: message });
return {
requestId: request.requestId,
status: "error" ,
errorMessage: message,
};
}
}
}
) {
this.llmClient = llmClient;
this.repairService = repairService;
this.observabilityService = observabilityService;
}
async submitToPortal (payerId : string , formData : Record < string , string >, requestId ?: string ) : Promise <{ confirmation : string ; screenshotPath ?: string }> {
const controller = new AbortController ();
const timeoutId = setTimeout (() => { controller. abort (); }, 60000 );
const browser = await chromium. launch ({ headless: true });
try {
const page = await browser. newPage ();
const portalUrl = this. getPortalUrl (payerId);
await page. goto (portalUrl, { timeout: 30000 , waitUntil: 'networkidle' });
const pageContent = await page. content ();
const fieldPrompt = `Given this HTML page content, identify the form field selectors for the following fields: ${ Object . keys ( formData ). join ( ', ' ) }. Return a JSON object mapping each field name to its CSS selector.\\n\\nPage HTML (truncated):\\n${ pageContent . slice ( 0 , 3000 ) }` ;
const selectorSchema = z. record (z. string (), z. string ());
const fieldSelectors = await this.llmClient. generateStructured (fieldPrompt, selectorSchema, 'You are a web automation assistant. Return ONLY valid JSON.' );
for ( const [fieldId, value] of Object. entries (formData)) {
const selector = fieldSelectors[fieldId] ?? `[name="${ fieldId }"], #${ fieldId }` ;
try {
await page. fill (selector, value, { timeout: 5000 });
} catch {
void fieldId;
}
}
this.observabilityService. traceWorkflowStep ( 'portal-submission' , payerId, { fieldCount: Object. keys (formData).length });
await page. click ( 'button[type="submit"], input[type="submit"]' , { timeout: 10000 });
await page. waitForTimeout ( 2000 );
const confirmationEl = await page. waitForSelector ( '.confirmation, .success, [class*="success"], [class*="confirmation"]' , { timeout: 30000 });
const text = await confirmationEl. textContent ();
const confirmation = text ?? 'Submission completed' ;
const screenshotPath = requestId ? `/tmp/prior-auth-screenshot-${ requestId }.png` : undefined ;
if (screenshotPath) {
await page. screenshot ({ path: screenshotPath, fullPage: true });
}
return { confirmation, ... (screenshotPath ? { screenshotPath } : {}) };
} catch (err) {
if (controller.signal.aborted) {
return { confirmation: 'Timeout: portal submission exceeded 60 seconds' };
}
throw err;
} finally {
clearTimeout (timeoutId);
await browser. close ();
}
}
private getPortalUrl (payerId : string ) : string {
const urls : Record < string , string > = {
'delta-dental' : 'https://provider.deltadental.com' ,
'cigna-dental' : 'https://cignaforhcp.cigna.com' ,
'vsp-vision' : 'https://vsp.com/providers' ,
'eyemed' : 'https://providers.eyemed.com' ,
};
return urls[payerId] ?? 'https://example.com/portal' ;
}
}
fn
() },
}));
vi. mock ( "../../src/services/pdf-extractor.js" , () => ({ PdfExtractor: vi. fn () }));
vi. mock ( "../../src/services/pdf-form-filler.js" , () => ({ PdfFormFiller: vi. fn () }));
vi. mock ( "../../src/services/llm-client.js" , () => ({ LlmClient: vi. fn () }));
vi. mock ( "../../src/services/payer-form-registry.js" , () => ({ PayerFormRegistry: vi. fn () }));
vi. mock ( "../../src/services/guardrail-service.js" , () => ({ GuardrailService: vi. fn () }));
vi. mock ( "../../src/services/memory-service.js" , () => ({ MemoryService: vi. fn () }));
vi. mock ( "../../src/services/router-service.js" , () => ({ RouterService: vi. fn () }));
vi. mock ( "../../src/services/repair-service.js" , () => ({ RepairService: vi. fn () }));
vi. mock ( "../../src/services/tool-registry-service.js" , () => ({ ToolRegistryService: vi. fn () }));
vi. mock ( "../../src/services/observability.js" , () => ({ ObservabilityService: vi. fn () }));
vi. mock ( "../../src/types/prior-auth.js" , () => ({}));
describe ( "submit route" , () => {
beforeAll (() => {
mockProcessRequest. mockReset ();
});
it ( "POST with valid body returns 200" , async () => {
mockProcessRequest. mockResolvedValue ({ requestId: "req-1" , status: "submitted" });
const { POST } = await import ( "../../app/api/prior-auth/submit/route.js" );
const { NextRequest } = await import ( "next/server.js" );
const init = {
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: JSON. stringify ({
requestId: "req-1" ,
patientName: "John Doe" ,
patientDob: "1990-01-01" ,
payerId: "delta-dental" ,
procedureCode: "D7140" ,
diagnosisCode: "K02.9" ,
}),
duplex: "half" as const ,
};
const req = new NextRequest ( "http://localhost/api/prior-auth/submit" , init);
const response = await POST (req);
expect (response.status). toBe ( 200 );
});
it ( "POST with missing fields returns 400" , async () => {
const { POST } = await import ( "../../app/api/prior-auth/submit/route.js" );
const { NextRequest } = await import ( "next/server.js" );
const init = {
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: JSON. stringify ({ requestId: "req-1" }),
duplex: "half" as const ,
};
const req = new NextRequest ( "http://localhost/api/prior-auth/submit" , init);
const response = await POST (req);
expect (response.status). toBe ( 400 );
});
it ( "POST handles Error rejections with 500 and uses error message" , async () => {
mockProcessRequest. mockRejectedValue ( new Error ( "Something broke" ));
const { POST } = await import ( "../../app/api/prior-auth/submit/route.js" );
const { NextRequest } = await import ( "next/server.js" );
const init = {
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: JSON. stringify ({
requestId: "req-1" ,
patientName: "John Doe" ,
patientDob: "1990-01-01" ,
payerId: "delta-dental" ,
procedureCode: "D7140" ,
diagnosisCode: "K02.9" ,
}),
duplex: "half" as const ,
};
const req = new NextRequest ( "http://localhost/api/prior-auth/submit" , init);
const response = await POST (req);
expect (response.status). toBe ( 500 );
});
it ( "POST handles non-Error rejections with 500" , async () => {
mockProcessRequest. mockRejectedValue ( "string error" );
const { POST } = await import ( "../../app/api/prior-auth/submit/route.js" );
const { NextRequest } = await import ( "next/server.js" );
const init = {
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: JSON. stringify ({
requestId: "req-1" ,
patientName: "John Doe" ,
patientDob: "1990-01-01" ,
payerId: "delta-dental" ,
procedureCode: "D7140" ,
diagnosisCode: "K02.9" ,
}),
duplex: "half" as const ,
};
const req = new NextRequest ( "http://localhost/api/prior-auth/submit" , init);
const response = await POST (req);
expect (response.status). toBe ( 500 );
});
it ( "POST with invalid JSON returns 400" , async () => {
const { POST } = await import ( "../../app/api/prior-auth/submit/route.js" );
const { NextRequest } = await import ( "next/server.js" );
const invalidBody = new ReadableStream ({
start (controller) {
controller. enqueue ( new TextEncoder (). encode ( "not json" ));
controller. close ();
},
});
const init = {
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: invalidBody,
duplex: "half" as const ,
};
const req = new NextRequest ( "http://localhost/api/prior-auth/submit" , init);
const response = await POST (req);
expect (response.status). toBe ( 400 );
});
});