// ============================================================================= // TOM Generator Document Analyzer // AI-powered analysis of uploaded evidence documents // ============================================================================= import { EvidenceDocument, AIDocumentAnalysis, ExtractedClause, DocumentType, } from '../types' import { getDocumentAnalysisPrompt, getDocumentTypeDetectionPrompt, DocumentAnalysisPromptContext, } from './prompts' import { getAllControls } from '../controls/loader' // ============================================================================= // TYPES // ============================================================================= export interface AnalysisResult { success: boolean analysis: AIDocumentAnalysis | null error?: string } export interface DocumentTypeDetectionResult { documentType: DocumentType confidence: number reasoning: string } // ============================================================================= // DOCUMENT ANALYZER CLASS // ============================================================================= export class TOMDocumentAnalyzer { private apiEndpoint: string private apiKey: string | null constructor(options?: { apiEndpoint?: string; apiKey?: string }) { this.apiEndpoint = options?.apiEndpoint || '/api/sdk/v1/tom-generator/evidence/analyze' this.apiKey = options?.apiKey || null } /** * Analyze a document and extract relevant TOM information */ async analyzeDocument( document: EvidenceDocument, documentText: string, language: 'de' | 'en' = 'de' ): Promise { try { // Get all control IDs for context const allControls = getAllControls() const controlIds = allControls.map((c) => c.id) // Build the prompt context const promptContext: DocumentAnalysisPromptContext = { documentType: document.documentType, documentText, controlIds, language, } const prompt = getDocumentAnalysisPrompt(promptContext) // Call the AI API const response = await this.callAI(prompt) if (!response.success || !response.data) { return { success: false, analysis: null, error: response.error || 'Failed to analyze document', } } // Parse the AI response const parsedResponse = this.parseAnalysisResponse(response.data) const analysis: AIDocumentAnalysis = { summary: parsedResponse.summary, extractedClauses: parsedResponse.extractedClauses, applicableControls: parsedResponse.applicableControls, gaps: parsedResponse.gaps, confidence: parsedResponse.confidence, analyzedAt: new Date(), } return { success: true, analysis, } } catch (error) { return { success: false, analysis: null, error: error instanceof Error ? error.message : 'Unknown error', } } } /** * Detect the document type from content */ async detectDocumentType( documentText: string, filename: string ): Promise { try { const prompt = getDocumentTypeDetectionPrompt(documentText, filename) const response = await this.callAI(prompt) if (!response.success || !response.data) { return { documentType: 'OTHER', confidence: 0, reasoning: 'Could not detect document type', } } const parsed = this.parseJSONResponse(response.data) return { documentType: this.mapDocumentType(String(parsed.documentType || 'OTHER')), confidence: typeof parsed.confidence === 'number' ? parsed.confidence : 0, reasoning: typeof parsed.reasoning === 'string' ? parsed.reasoning : '', } } catch (error) { return { documentType: 'OTHER', confidence: 0, reasoning: error instanceof Error ? error.message : 'Detection failed', } } } /** * Link document to applicable controls based on analysis */ async suggestControlLinks( analysis: AIDocumentAnalysis ): Promise { // Use the applicable controls from the analysis const suggestedControls = [...analysis.applicableControls] // Also check extracted clauses for related controls for (const clause of analysis.extractedClauses) { if (clause.relatedControlId && !suggestedControls.includes(clause.relatedControlId)) { suggestedControls.push(clause.relatedControlId) } } return suggestedControls } /** * Calculate evidence coverage for a control */ calculateEvidenceCoverage( controlId: string, documents: EvidenceDocument[] ): { coverage: number linkedDocuments: string[] missingEvidence: string[] } { const control = getAllControls().find((c) => c.id === controlId) if (!control) { return { coverage: 0, linkedDocuments: [], missingEvidence: [] } } const linkedDocuments: string[] = [] const coveredRequirements = new Set() for (const doc of documents) { // Check if document is explicitly linked if (doc.linkedControlIds.includes(controlId)) { linkedDocuments.push(doc.id) } // Check if AI analysis suggests this document covers the control if (doc.aiAnalysis?.applicableControls.includes(controlId)) { if (!linkedDocuments.includes(doc.id)) { linkedDocuments.push(doc.id) } } // Check which evidence requirements are covered if (doc.aiAnalysis) { for (const requirement of control.evidenceRequirements) { const reqLower = requirement.toLowerCase() if ( doc.aiAnalysis.summary.toLowerCase().includes(reqLower) || doc.aiAnalysis.extractedClauses.some((c) => c.text.toLowerCase().includes(reqLower) ) ) { coveredRequirements.add(requirement) } } } } const missingEvidence = control.evidenceRequirements.filter( (req) => !coveredRequirements.has(req) ) const coverage = control.evidenceRequirements.length > 0 ? Math.round( (coveredRequirements.size / control.evidenceRequirements.length) * 100 ) : 100 return { coverage, linkedDocuments, missingEvidence, } } /** * Call the AI API */ private async callAI( prompt: string ): Promise<{ success: boolean; data?: string; error?: string }> { try { const headers: Record = { 'Content-Type': 'application/json', } if (this.apiKey) { headers['Authorization'] = `Bearer ${this.apiKey}` } const response = await fetch(this.apiEndpoint, { method: 'POST', headers, body: JSON.stringify({ prompt }), }) if (!response.ok) { return { success: false, error: `API error: ${response.status} ${response.statusText}`, } } const data = await response.json() return { success: true, data: data.response || data.content || JSON.stringify(data), } } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'API call failed', } } } /** * Parse the AI analysis response */ private parseAnalysisResponse(response: string): { summary: string extractedClauses: ExtractedClause[] applicableControls: string[] gaps: string[] confidence: number } { const parsed = this.parseJSONResponse(response) return { summary: typeof parsed.summary === 'string' ? parsed.summary : '', extractedClauses: (Array.isArray(parsed.extractedClauses) ? parsed.extractedClauses : []).map( (clause: Record) => ({ id: String(clause.id || ''), text: String(clause.text || ''), type: String(clause.type || ''), relatedControlId: clause.relatedControlId ? String(clause.relatedControlId) : null, }) ), applicableControls: Array.isArray(parsed.applicableControls) ? parsed.applicableControls.map(String) : [], gaps: Array.isArray(parsed.gaps) ? parsed.gaps.map(String) : [], confidence: typeof parsed.confidence === 'number' ? parsed.confidence : 0, } } /** * Parse JSON from AI response (handles markdown code blocks) */ private parseJSONResponse(response: string): Record { let jsonStr = response.trim() // Remove markdown code blocks if present if (jsonStr.startsWith('```json')) { jsonStr = jsonStr.slice(7) } else if (jsonStr.startsWith('```')) { jsonStr = jsonStr.slice(3) } if (jsonStr.endsWith('```')) { jsonStr = jsonStr.slice(0, -3) } jsonStr = jsonStr.trim() try { return JSON.parse(jsonStr) } catch { // Try to extract JSON from the response const jsonMatch = jsonStr.match(/\{[\s\S]*\}/) if (jsonMatch) { try { return JSON.parse(jsonMatch[0]) } catch { return {} } } return {} } } /** * Map string to DocumentType */ private mapDocumentType(type: string): DocumentType { const typeMap: Record = { AVV: 'AVV', DPA: 'DPA', SLA: 'SLA', NDA: 'NDA', POLICY: 'POLICY', CERTIFICATE: 'CERTIFICATE', AUDIT_REPORT: 'AUDIT_REPORT', OTHER: 'OTHER', } return typeMap[type.toUpperCase()] || 'OTHER' } } // ============================================================================= // SINGLETON INSTANCE // ============================================================================= let analyzerInstance: TOMDocumentAnalyzer | null = null export function getDocumentAnalyzer( options?: { apiEndpoint?: string; apiKey?: string } ): TOMDocumentAnalyzer { if (!analyzerInstance) { analyzerInstance = new TOMDocumentAnalyzer(options) } return analyzerInstance } // ============================================================================= // HELPER FUNCTIONS // ============================================================================= /** * Quick document analysis */ export async function analyzeEvidenceDocument( document: EvidenceDocument, documentText: string, language: 'de' | 'en' = 'de' ): Promise { return getDocumentAnalyzer().analyzeDocument(document, documentText, language) } /** * Quick document type detection */ export async function detectEvidenceDocumentType( documentText: string, filename: string ): Promise { return getDocumentAnalyzer().detectDocumentType(documentText, filename) } /** * Get evidence gaps for all controls */ export function getEvidenceGapsForAllControls( documents: EvidenceDocument[] ): Map { const analyzer = getDocumentAnalyzer() const allControls = getAllControls() const gaps = new Map() for (const control of allControls) { const result = analyzer.calculateEvidenceCoverage(control.id, documents) gaps.set(control.id, { coverage: result.coverage, missing: result.missingEvidence, }) } return gaps }