| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { nanoid } from "nanoid"; |
| import { |
| CommitmentContract, |
| AutonomyClass, |
| ProcessedEmailRecord, |
| RoleType, |
| } from "@/types"; |
|
|
| |
|
|
| |
| const REQUEST_KEYWORDS: readonly string[] = [ |
| "need", |
| "please prepare", |
| "by friday", |
| "by monday", |
| "send me", |
| "review", |
| "update", |
| "analyze", |
| "report", |
| "summarize", |
| "follow up", |
| "schedule", |
| "complete", |
| "draft", |
| "create", |
| ]; |
|
|
| |
| const AUTHORITY_SIGNALS: readonly string[] = [ |
| "manager", |
| "director", |
| "vp", |
| "vice president", |
| "head of", |
| "lead", |
| "supervisor", |
| "principal", |
| ]; |
|
|
| |
| const DEADLINE_PHRASES: ReadonlyArray<{ phrase: RegExp; offsetDays: number | (() => number); label: string }> = [ |
| { phrase: /\basap\b/i, offsetDays: 1, label: "ASAP" }, |
| { phrase: /\burgent\b/i, offsetDays: 1, label: "urgent" }, |
| { phrase: /\btomorrow\b/i, offsetDays: 1, label: "tomorrow" }, |
| { phrase: /\bend of (the )?week\b/i, offsetDays: 5, label: "end of week" }, |
| { phrase: /\bby friday\b/i, offsetDays: () => nextWeekdayOffset(5), label: "by Friday" }, |
| { phrase: /\bby monday\b/i, offsetDays: () => nextWeekdayOffset(1), label: "by Monday" }, |
| { phrase: /\bby tuesday\b/i, offsetDays: () => nextWeekdayOffset(2), label: "by Tuesday" }, |
| { phrase: /\bby wednesday\b/i, offsetDays: () => nextWeekdayOffset(3), label: "by Wednesday" }, |
| { phrase: /\bby thursday\b/i, offsetDays: () => nextWeekdayOffset(4), label: "by Thursday" }, |
| ]; |
|
|
| |
| const ROLE_TOOLS: Record<RoleType, string[]> = { |
| field_representative: [ |
| "crm_lookup", |
| "territory_map", |
| "route_planner", |
| "call_planner", |
| "internal_report_writer", |
| ], |
| regional_manager: [ |
| "crm_lookup", |
| "performance_dashboard", |
| "team_report_writer", |
| "forecast_model", |
| "internal_email_drafter", |
| ], |
| medical_affairs: [ |
| "evidence_search", |
| "medical_information_writer", |
| "literature_index", |
| "internal_email_drafter", |
| ], |
| market_access: [ |
| "formulary_database", |
| "payer_analyzer", |
| "reimbursement_calculator", |
| "internal_report_writer", |
| ], |
| compliance: [ |
| "policy_search", |
| "audit_log_reader", |
| "compliance_checklist_writer", |
| "internal_email_drafter", |
| ], |
| }; |
|
|
| |
|
|
| |
| |
| |
| |
| |
| function nextWeekdayOffset(targetWeekday: number): number { |
| const today = new Date(); |
| const todayWeekday = today.getDay(); |
| let offset = targetWeekday - todayWeekday; |
| if (offset <= 0) offset += 7; |
| return offset; |
| } |
|
|
| |
| function toIsoDate(date: Date): string { |
| return date.toISOString().slice(0, 10); |
| } |
|
|
| |
| function daysFromNow(offsetDays: number): string { |
| const d = new Date(); |
| d.setDate(d.getDate() + offsetDays); |
| return toIsoDate(d); |
| } |
|
|
| |
| function contains(text: string, phrase: string): boolean { |
| return text.toLowerCase().includes(phrase.toLowerCase()); |
| } |
|
|
| |
| function containsAny(text: string, phrases: readonly string[]): boolean { |
| const lower = text.toLowerCase(); |
| return phrases.some((p) => lower.includes(p.toLowerCase())); |
| } |
|
|
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function inferRole(subject: string, body: string): RoleType { |
| const text = `${subject} ${body}`.toLowerCase(); |
|
|
| if (contains(text, "compliance") && contains(text, "review")) { |
| return "compliance"; |
| } |
| if (contains(text, "medical information") && contains(text, "evidence")) { |
| return "medical_affairs"; |
| } |
| if (contains(text, "formulary") && contains(text, "payer")) { |
| return "market_access"; |
| } |
| if (contains(text, "team") && contains(text, "performance")) { |
| return "regional_manager"; |
| } |
| if (contains(text, "territory")) { |
| return "field_representative"; |
| } |
|
|
| |
| return "field_representative"; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function extractDeadline(text: string): string { |
| if (!text) return ""; |
|
|
| |
| for (const { phrase, offsetDays } of DEADLINE_PHRASES) { |
| if (phrase.test(text)) { |
| const resolved = typeof offsetDays === "function" ? offsetDays() : offsetDays; |
| return daysFromNow(resolved); |
| } |
| } |
|
|
| |
| const isoMatch = text.match(/\b(\d{4})-(\d{2})-(\d{2})\b/); |
| if (isoMatch) { |
| const [, y, m, d] = isoMatch; |
| return `${y}-${m}-${d}`; |
| } |
|
|
| |
| const numericMatch = text.match(/\b(\d{1,2})\/(\d{1,2})\b/); |
| if (numericMatch) { |
| const [, monthStr, dayStr] = numericMatch; |
| const month = monthStr.padStart(2, "0"); |
| const day = dayStr.padStart(2, "0"); |
| const year = new Date().getFullYear(); |
| return `${year}-${month}-${day}`; |
| } |
|
|
| |
| const monthNameMatch = text.match( |
| /\b(Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)\s+(\d{1,2})\b/i, |
| ); |
| if (monthNameMatch) { |
| const monthMap: Record<string, string> = { |
| jan: "01", feb: "02", mar: "03", apr: "04", may: "05", jun: "06", |
| jul: "07", aug: "08", sep: "09", oct: "10", nov: "11", dec: "12", |
| }; |
| const monthKey = monthNameMatch[1].toLowerCase().slice(0, 3); |
| const month = monthMap[monthKey]; |
| const day = monthNameMatch[2].padStart(2, "0"); |
| const year = new Date().getFullYear(); |
| return `${year}-${month}-${day}`; |
| } |
|
|
| return ""; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function classifyAutonomy( |
| requestedOutcome: string, |
| externalSend: boolean, |
| ): AutonomyClass { |
| const text = requestedOutcome.toLowerCase(); |
|
|
| |
| const prohibited = [ |
| "off-label", |
| "bribe", |
| "kickback", |
| "prescribe", |
| "diagnose", |
| "fabricate", |
| "falsify", |
| ]; |
| if (containsAny(text, prohibited)) { |
| return 4; |
| } |
|
|
| |
| const class3 = [ |
| "clinical", |
| "scientific", |
| "efficacy", |
| "safety", |
| "promotional claim", |
| "claim", |
| "financial authorization", |
| "authorize payment", |
| "budget approval", |
| "medical judgment", |
| ]; |
| if (containsAny(text, class3)) { |
| return 3; |
| } |
|
|
| |
| if (externalSend) { |
| return 2; |
| } |
| const class2 = [ |
| "external email", |
| "send to hcp", |
| "contact hcp", |
| "crm", |
| "schedule meeting", |
| "book meeting", |
| "calendar invite", |
| "modify crm", |
| ]; |
| if (containsAny(text, class2)) { |
| return 2; |
| } |
|
|
| |
| const class1 = [ |
| "report", |
| "summary", |
| "summarize", |
| "reconcile", |
| "reconciliation", |
| "route", |
| "route calculation", |
| "internal", |
| "dashboard", |
| "analysis", |
| "analyze", |
| ]; |
| if (containsAny(text, class1)) { |
| return 1; |
| } |
|
|
| |
| if (externalSend) return 2; |
|
|
| |
| return 1; |
| } |
|
|
| |
| |
| |
| |
| function isAuthoritySender(sender: string, subject: string): boolean { |
| return containsAny(sender, AUTHORITY_SIGNALS) || containsAny(subject, AUTHORITY_SIGNALS); |
| } |
|
|
| |
| |
| |
| |
| |
| function extractRequestedOutcome(subject: string, body: string): string { |
| const subjectTrimmed = subject.trim(); |
| if (subjectTrimmed.length > 0 && containsAny(subjectTrimmed, REQUEST_KEYWORDS)) { |
| return subjectTrimmed; |
| } |
|
|
| const firstSentence = body.split(/[.\n!]/)[0]?.trim() ?? ""; |
| if (firstSentence.length > 0) { |
| return firstSentence.slice(0, 240); |
| } |
|
|
| return subjectTrimmed || "Unspecified work request"; |
| } |
|
|
| function extractEmailAddress(sender: string): string { |
| if (!sender) return ""; |
| const match = sender.match(/<([^>]+)>/); |
| return match?.[1]?.trim() ?? ""; |
| } |
|
|
| |
| |
| |
| |
| function inferMandatoryOutputs(text: string): string[] { |
| const lower = text.toLowerCase(); |
| const outputs: string[] = []; |
|
|
| if (contains(lower, "report")) outputs.push("Written report"); |
| if (contains(lower, "summary") || contains(lower, "summarize")) outputs.push("Executive summary"); |
| if (contains(lower, "analysis") || contains(lower, "analyze")) outputs.push("Analysis document"); |
| if (contains(lower, "brief") || contains(lower, "pre-call")) outputs.push("Pre-call brief"); |
| if (contains(lower, "draft")) outputs.push("Draft document"); |
| if (contains(lower, "schedule")) outputs.push("Scheduled meeting invite"); |
| if (contains(lower, "review")) outputs.push("Review checklist"); |
| if (contains(lower, "update")) outputs.push("Updated record/document"); |
| if (contains(lower, "route") || contains(lower, "plan")) outputs.push("Route/plan document"); |
| if (contains(lower, "formulary")) outputs.push("Formulary analysis"); |
| if (contains(lower, "compliance")) outputs.push("Compliance review memo"); |
|
|
| if (outputs.length === 0) outputs.push("Response to request"); |
| return outputs; |
| } |
|
|
| |
| |
| |
| |
| function inferAdditionalOutputs(text: string, role: RoleType): string[] { |
| const lower = text.toLowerCase(); |
| const extras: string[] = []; |
|
|
| if (role === "field_representative") { |
| extras.push("Recommended next-best-action for territory"); |
| if (contains(lower, "territory")) extras.push("Territory coverage map"); |
| } |
| if (role === "regional_manager") { |
| extras.push("Team performance snapshot"); |
| } |
| if (role === "medical_affairs") { |
| extras.push("Evidence quality assessment"); |
| } |
| if (role === "market_access") { |
| extras.push("Payer landscape summary"); |
| } |
| if (role === "compliance") { |
| extras.push("Risk mitigation recommendations"); |
| } |
|
|
| if (contains(lower, "deadline") || contains(lower, "by ")) { |
| extras.push("Timeline with milestones"); |
| } |
|
|
| return extras; |
| } |
|
|
| |
| |
| |
| function inferDependencies(text: string): { description: string; blocksProbability: number }[] { |
| const lower = text.toLowerCase(); |
| const deps: { description: string; blocksProbability: number }[] = []; |
|
|
| if (contains(lower, "attachment") || contains(lower, "attached")) { |
| deps.push({ description: "Attachment must be parsed and validated", blocksProbability: 0.2 }); |
| } |
| if (contains(lower, "approval") || contains(lower, "approve")) { |
| deps.push({ description: "Manager approval required before execution", blocksProbability: 0.35 }); |
| } |
| if (contains(lower, "data") || contains(lower, "crm")) { |
| deps.push({ description: "CRM/data source availability", blocksProbability: 0.15 }); |
| } |
| if (contains(lower, "compliance")) { |
| deps.push({ description: "Compliance review gate", blocksProbability: 0.3 }); |
| } |
|
|
| return deps; |
| } |
|
|
| |
| |
| |
| function estimateCompletionProbability(text: string, autonomyClass: AutonomyClass): number { |
| const lower = text.toLowerCase(); |
| let probability = 0.9; |
|
|
| |
| if (contains(lower, "complex") || contains(lower, "detailed")) probability -= 0.08; |
| if (contains(lower, "multiple") || contains(lower, "all")) probability -= 0.05; |
| if (contains(lower, "approval") || contains(lower, "approve")) probability -= 0.07; |
| if (contains(lower, "compliance")) probability -= 0.06; |
|
|
| |
| if (autonomyClass >= 3) probability -= 0.05; |
| if (autonomyClass === 4) probability -= 0.05; |
|
|
| |
| return Math.max(0.7, Math.min(0.95, Number(probability.toFixed(2)))); |
| } |
|
|
| |
| |
| |
| |
| export function detectCommitments(records: ProcessedEmailRecord[]): CommitmentContract[] { |
| const commitments: CommitmentContract[] = []; |
|
|
| for (const record of records) { |
| const subject = record.subject ?? ""; |
| const body = record.extractedData?.summary ?? ""; |
| const combinedText = `${subject} ${body}`; |
|
|
| |
| if (!containsAny(combinedText, REQUEST_KEYWORDS)) { |
| continue; |
| } |
|
|
| const role = inferRole(subject, body); |
| const outcome = extractRequestedOutcome(subject, body); |
| const externalSend = false; |
| const autonomyClass = classifyAutonomy(outcome, externalSend); |
| const deadline = extractDeadline(combinedText) || daysFromNow(3); |
| const mandatoryOutputs = inferMandatoryOutputs(combinedText); |
| const inferredOutputs = inferAdditionalOutputs(combinedText, role); |
| const dependencies = inferDependencies(combinedText); |
| const completionProbability = estimateCompletionProbability(combinedText, autonomyClass); |
| const authority = isAuthoritySender(record.sender, subject); |
|
|
| const now = new Date().toISOString(); |
|
|
| const contract: CommitmentContract = { |
| id: nanoid(), |
| emailId: record.emailId, |
| emailSubject: subject, |
| requester: record.sender, |
| requesterEmail: record.senderEmail || extractEmailAddress(record.sender) || "", |
| recipientRole: role, |
| authorityVerified: authority, |
| requestedOutcome: outcome, |
| deadline, |
| mandatoryOutputs, |
| inferredOutputs, |
| permittedTools: ROLE_TOOLS[role], |
| externalSendAllowed: externalSend, |
| autonomyClass, |
| completionProbability, |
| dependencies, |
| assumptions: [ |
| "Request is directed at the inferred recipient role.", |
| "Standard content library and policy package apply.", |
| "No off-label or promotional claims are permitted.", |
| ], |
| status: "detected", |
| auditEvents: [ |
| { |
| timestamp: now, |
| event: "detected", |
| detail: `Commitment detected from email "${subject}" with autonomy class ${autonomyClass}.`, |
| }, |
| ], |
| detectedAt: now, |
| }; |
|
|
| commitments.push(contract); |
| } |
|
|
| return commitments; |
| } |
|
|
| |
|
|
| |
| |
| |
| |
| |
| |
| export function generateSampleCommitments(): CommitmentContract[] { |
| const now = new Date().toISOString(); |
| const baseAssumptions = [ |
| "Request is directed at the inferred recipient role.", |
| "Standard content library and policy package apply.", |
| "No off-label or promotional claims are permitted.", |
| ]; |
|
|
| const makeAudit = (subject: string, autonomyClass: AutonomyClass) => [ |
| { |
| timestamp: now, |
| event: "detected", |
| detail: `Sample commitment generated for "${subject}" with autonomy class ${autonomyClass}.`, |
| }, |
| ]; |
|
|
| |
| const territoryOutcome = "Prepare territory review report for Northern California this week"; |
| const territoryRole: RoleType = "field_representative"; |
| const territoryAutonomy = classifyAutonomy(territoryOutcome, false); |
|
|
| |
| const briefOutcome = "Draft pre-call brief for Dr. Chen cardiology visit"; |
| const briefRole: RoleType = "field_representative"; |
| const briefAutonomy = classifyAutonomy(briefOutcome, false); |
|
|
| |
| const formularyOutcome = "Analyze formulary status for Biktarvy across top regional payers"; |
| const formularyRole: RoleType = "market_access"; |
| const formularyAutonomy = classifyAutonomy(formularyOutcome, false); |
|
|
| |
| const medInfoOutcome = "Prepare medical information response with evidence summary on Descovy"; |
| const medInfoRole: RoleType = "medical_affairs"; |
| const medInfoAutonomy = classifyAutonomy(medInfoOutcome, false); |
|
|
| |
| const complianceOutcome = "Compliance review of proposed HCP engagement materials"; |
| const complianceRole: RoleType = "compliance"; |
| const complianceAutonomy = classifyAutonomy(complianceOutcome, false); |
|
|
| const samples: Array<{ |
| subject: string; |
| sender: string; |
| outcome: string; |
| role: RoleType; |
| autonomy: AutonomyClass; |
| deadline: string; |
| mandatory: string[]; |
| inferred: string[]; |
| dependencies: { description: string; blocksProbability: number }[]; |
| probability: number; |
| }> = [ |
| { |
| subject: "Territory review needed by Friday", |
| sender: "Sarah Martinez, Regional Manager", |
| outcome: territoryOutcome, |
| role: territoryRole, |
| autonomy: territoryAutonomy, |
| deadline: daysFromNow(nextWeekdayOffset(5)), |
| mandatory: ["Written report", "Territory coverage map"], |
| inferred: ["Recommended next-best-action for territory", "Timeline with milestones"], |
| dependencies: [ |
| { description: "CRM/data source availability", blocksProbability: 0.15 }, |
| ], |
| probability: 0.9, |
| }, |
| { |
| subject: "Please prepare pre-call brief for Dr. Chen", |
| sender: "James Park, Field Lead", |
| outcome: briefOutcome, |
| role: briefRole, |
| autonomy: briefAutonomy, |
| deadline: daysFromNow(2), |
| mandatory: ["Pre-call brief"], |
| inferred: ["Recommended next-best-action for territory"], |
| dependencies: [], |
| probability: 0.92, |
| }, |
| { |
| subject: "Formulary analysis request - Biktarvy payer coverage", |
| sender: "Dana Whitfield, Market Access Director", |
| outcome: formularyOutcome, |
| role: formularyRole, |
| autonomy: formularyAutonomy, |
| deadline: daysFromNow(5), |
| mandatory: ["Formulary analysis", "Analysis document"], |
| inferred: ["Payer landscape summary"], |
| dependencies: [ |
| { description: "Formulary database availability", blocksProbability: 0.2 }, |
| ], |
| probability: 0.85, |
| }, |
| { |
| subject: "Medical information response needed - Descovy evidence", |
| sender: "Dr. Priya Nair, Medical Affairs Lead", |
| outcome: medInfoOutcome, |
| role: medInfoRole, |
| autonomy: medInfoAutonomy, |
| deadline: daysFromNow(3), |
| mandatory: ["Medical information response", "Evidence quality assessment"], |
| inferred: ["Evidence quality assessment"], |
| dependencies: [ |
| { description: "Literature index availability", blocksProbability: 0.15 }, |
| ], |
| probability: 0.88, |
| }, |
| { |
| subject: "Compliance review of HCP engagement materials", |
| sender: "Robert Osei, Compliance Director", |
| outcome: complianceOutcome, |
| role: complianceRole, |
| autonomy: complianceAutonomy, |
| deadline: daysFromNow(4), |
| mandatory: ["Compliance review memo", "Review checklist"], |
| inferred: ["Risk mitigation recommendations"], |
| dependencies: [ |
| { description: "Compliance review gate", blocksProbability: 0.3 }, |
| ], |
| probability: 0.82, |
| }, |
| ]; |
|
|
| return samples.map((s) => ({ |
| id: nanoid(), |
| emailId: nanoid(), |
| emailSubject: s.subject, |
| requester: s.sender, |
| requesterEmail: s.sender, |
| recipientRole: s.role, |
| authorityVerified: isAuthoritySender(s.sender, s.subject), |
| requestedOutcome: s.outcome, |
| deadline: s.deadline, |
| mandatoryOutputs: s.mandatory, |
| inferredOutputs: s.inferred, |
| permittedTools: ROLE_TOOLS[s.role], |
| externalSendAllowed: false, |
| autonomyClass: s.autonomy, |
| completionProbability: s.probability, |
| dependencies: s.dependencies, |
| assumptions: baseAssumptions, |
| status: "detected" as const, |
| auditEvents: makeAudit(s.subject, s.autonomy), |
| detectedAt: now, |
| })); |
| } |
|
|