Fix CIM template data linkage issues - update field mapping to use proper nested paths

This commit is contained in:
Jon
2025-07-29 00:25:04 -04:00
parent d794e64a02
commit 4ce430b531
2 changed files with 230 additions and 87 deletions

View File

@@ -461,9 +461,13 @@ export class OptimizedAgenticRAGProcessor {
// Use the existing LLM service to generate CIM review
const result = await llmService.processCIMDocument(text, 'BPCP CIM Review Template');
// Generate a comprehensive summary from the analysis data
const analysisData = result.jsonOutput || {} as CIMReview;
const summary = this.generateSummaryFromAnalysis(analysisData);
return {
summary: 'Document processed with optimized agentic RAG',
analysisData: result.jsonOutput || {} as CIMReview
summary,
analysisData
};
} catch (error) {
logger.error(`Failed to generate LLM analysis for document: ${documentId}`, error);
@@ -474,6 +478,114 @@ export class OptimizedAgenticRAGProcessor {
};
}
}
/**
* Generate a comprehensive summary from analysis data
*/
private generateSummaryFromAnalysis(analysisData: CIMReview): string {
let summary = '# CIM Review Summary\n\n';
// Add deal overview
if (analysisData.dealOverview?.targetCompanyName) {
summary += `## Deal Overview\n\n`;
summary += `**Target Company:** ${analysisData.dealOverview.targetCompanyName}\n\n`;
if (analysisData.dealOverview.industrySector) {
summary += `**Industry:** ${analysisData.dealOverview.industrySector}\n\n`;
}
if (analysisData.dealOverview.transactionType) {
summary += `**Transaction Type:** ${analysisData.dealOverview.transactionType}\n\n`;
}
if (analysisData.dealOverview.geography) {
summary += `**Geography:** ${analysisData.dealOverview.geography}\n\n`;
}
}
// Add financial summary
if (analysisData.financialSummary?.financials) {
summary += `## Financial Summary\n\n`;
const financials = analysisData.financialSummary.financials;
if (financials.fy3) {
summary += `### FY3 (Latest)\n\n`;
if (financials.fy3.revenue) summary += `- **Revenue:** ${financials.fy3.revenue}\n`;
if (financials.fy3.ebitda) summary += `- **EBITDA:** ${financials.fy3.ebitda}\n`;
if (financials.fy3.ebitdaMargin) summary += `- **EBITDA Margin:** ${financials.fy3.ebitdaMargin}\n`;
if (financials.fy3.revenueGrowth) summary += `- **Revenue Growth:** ${financials.fy3.revenueGrowth}\n\n`;
}
if (financials.fy2) {
summary += `### FY2\n\n`;
if (financials.fy2.revenue) summary += `- **Revenue:** ${financials.fy2.revenue}\n`;
if (financials.fy2.ebitda) summary += `- **EBITDA:** ${financials.fy2.ebitda}\n`;
if (financials.fy2.ebitdaMargin) summary += `- **EBITDA Margin:** ${financials.fy2.ebitdaMargin}\n`;
if (financials.fy2.revenueGrowth) summary += `- **Revenue Growth:** ${financials.fy2.revenueGrowth}\n\n`;
}
if (financials.fy1) {
summary += `### FY1\n\n`;
if (financials.fy1.revenue) summary += `- **Revenue:** ${financials.fy1.revenue}\n`;
if (financials.fy1.ebitda) summary += `- **EBITDA:** ${financials.fy1.ebitda}\n`;
if (financials.fy1.ebitdaMargin) summary += `- **EBITDA Margin:** ${financials.fy1.ebitdaMargin}\n`;
if (financials.fy1.revenueGrowth) summary += `- **Revenue Growth:** ${financials.fy1.revenueGrowth}\n\n`;
}
}
// Add business description
if (analysisData.businessDescription?.coreOperationsSummary) {
summary += `## Business Description\n\n`;
summary += `**Core Operations:** ${analysisData.businessDescription.coreOperationsSummary}\n\n`;
if (analysisData.businessDescription.keyProductsServices) {
summary += `**Key Products/Services:** ${analysisData.businessDescription.keyProductsServices}\n\n`;
}
if (analysisData.businessDescription.uniqueValueProposition) {
summary += `**Unique Value Proposition:** ${analysisData.businessDescription.uniqueValueProposition}\n\n`;
}
}
// Add key questions and next steps
if (analysisData.keyQuestionsNextSteps?.criticalQuestions) {
summary += `## Key Questions & Next Steps\n\n`;
summary += `**Critical Questions:** ${analysisData.keyQuestionsNextSteps.criticalQuestions}\n\n`;
if (analysisData.keyQuestionsNextSteps.preliminaryRecommendation) {
summary += `**Preliminary Recommendation:** ${analysisData.keyQuestionsNextSteps.preliminaryRecommendation}\n\n`;
}
}
// Add management team
if (analysisData.managementTeamOverview?.keyLeaders) {
summary += `## Management Team\n\n`;
summary += `**Key Leaders:** ${analysisData.managementTeamOverview.keyLeaders}\n\n`;
if (analysisData.managementTeamOverview.managementQualityAssessment) {
summary += `**Quality Assessment:** ${analysisData.managementTeamOverview.managementQualityAssessment}\n\n`;
}
}
// Add market analysis
if (analysisData.marketIndustryAnalysis?.estimatedMarketSize) {
summary += `## Market & Industry Analysis\n\n`;
summary += `**Market Size:** ${analysisData.marketIndustryAnalysis.estimatedMarketSize}\n\n`;
if (analysisData.marketIndustryAnalysis.keyIndustryTrends) {
summary += `**Industry Trends:** ${analysisData.marketIndustryAnalysis.keyIndustryTrends}\n\n`;
}
}
// Add investment thesis
if (analysisData.preliminaryInvestmentThesis?.keyAttractions) {
summary += `## Investment Thesis\n\n`;
summary += `**Key Attractions:** ${analysisData.preliminaryInvestmentThesis.keyAttractions}\n\n`;
if (analysisData.preliminaryInvestmentThesis.potentialRisks) {
summary += `**Potential Risks:** ${analysisData.preliminaryInvestmentThesis.potentialRisks}\n\n`;
}
}
return summary;
}
}
export const optimizedAgenticRAGProcessor = new OptimizedAgenticRAGProcessor();

View File

@@ -239,62 +239,93 @@ const CIMReviewTemplate: React.FC<CIMReviewTemplateProps> = ({
const renderField = (
label: string,
field: keyof CIMReviewData,
fieldPath: string,
type: 'text' | 'textarea' | 'date' = 'text',
placeholder?: string,
rows?: number
) => (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{label}
</label>
{type === 'textarea' ? (
<textarea
value={getFieldValue(data, field) || ''}
onChange={(e) => updateData(field, e.target.value)}
placeholder={placeholder}
rows={rows || 3}
disabled={readOnly}
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm disabled:bg-gray-50 disabled:text-gray-500"
/>
) : type === 'date' ? (
<input
type="date"
value={getFieldValue(data, field) || ''}
onChange={(e) => updateData(field, e.target.value)}
disabled={readOnly}
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm disabled:bg-gray-50 disabled:text-gray-500"
/>
) : (
<input
type="text"
value={getFieldValue(data, field) || ''}
onChange={(e) => updateData(field, e.target.value)}
placeholder={placeholder}
disabled={readOnly}
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm disabled:bg-gray-50 disabled:text-gray-500"
/>
)}
</div>
);
) => {
const path = fieldPath.split('.');
const value = getNestedFieldValue(data, path);
// Helper function to safely get field values
const updateNestedField = (newValue: string) => {
setData(prev => {
const newData = { ...prev };
let current = newData;
for (let i = 0; i < path.length - 1; i++) {
if (!current[path[i]]) {
current[path[i]] = {};
}
current = current[path[i]];
}
current[path[path.length - 1]] = newValue;
return newData;
});
};
return (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{label}
</label>
{type === 'textarea' ? (
<textarea
value={value || ''}
onChange={(e) => updateNestedField(e.target.value)}
placeholder={placeholder}
rows={rows || 3}
disabled={readOnly}
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm disabled:bg-gray-50 disabled:text-gray-500"
/>
) : type === 'date' ? (
<input
type="date"
value={value || ''}
onChange={(e) => updateNestedField(e.target.value)}
disabled={readOnly}
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm disabled:bg-gray-50 disabled:text-gray-500"
/>
) : (
<input
type="text"
value={value || ''}
onChange={(e) => updateNestedField(e.target.value)}
placeholder={placeholder}
disabled={readOnly}
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm disabled:bg-gray-50 disabled:text-gray-500"
/>
)}
</div>
);
};
// Helper function to safely get field values with proper nested structure handling
const getFieldValue = (obj: any, field: keyof CIMReviewData): string => {
const value = obj[field];
if (typeof value === 'string') {
return value;
}
if (typeof value === 'object' && value !== null) {
// For nested objects, try to find a string value
for (const key in value) {
if (typeof value[key] === 'string') {
return value[key];
}
}
// For nested objects, we need to handle them specifically
// This function should only be called for top-level fields that are strings
// For nested objects, we should use specific accessors
return '';
}
return '';
};
// Helper function to get nested field values
const getNestedFieldValue = (obj: any, path: string[]): string => {
let current = obj;
for (const key of path) {
if (current && typeof current === 'object' && key in current) {
current = current[key];
} else {
return '';
}
}
return typeof current === 'string' ? current : '';
};
const renderFinancialTable = () => (
<div className="space-y-4">
<h4 className="text-lg font-medium text-gray-900">Key Historical Financials</h4>
@@ -400,39 +431,39 @@ const CIMReviewTemplate: React.FC<CIMReviewTemplateProps> = ({
return (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{renderField('Target Company Name', 'dealOverview')}
{renderField('Industry/Sector', 'dealOverview')}
{renderField('Geography (HQ & Key Operations)', 'dealOverview')}
{renderField('Deal Source', 'dealOverview')}
{renderField('Transaction Type', 'dealOverview')}
{renderField('Date CIM Received', 'dealOverview', 'date')}
{renderField('Date Reviewed', 'dealOverview', 'date')}
{renderField('Reviewer(s)', 'dealOverview')}
{renderField('CIM Page Count', 'dealOverview')}
{renderField('Target Company Name', 'dealOverview.targetCompanyName')}
{renderField('Industry/Sector', 'dealOverview.industrySector')}
{renderField('Geography (HQ & Key Operations)', 'dealOverview.geography')}
{renderField('Deal Source', 'dealOverview.dealSource')}
{renderField('Transaction Type', 'dealOverview.transactionType')}
{renderField('Date CIM Received', 'dealOverview.dateCIMReceived', 'date')}
{renderField('Date Reviewed', 'dealOverview.dateReviewed', 'date')}
{renderField('Reviewer(s)', 'dealOverview.reviewers')}
{renderField('CIM Page Count', 'dealOverview.cimPageCount')}
</div>
{renderField('Stated Reason for Sale (if provided)', 'dealOverview', 'textarea', 'Enter the stated reason for sale...', 4)}
{renderField('Stated Reason for Sale (if provided)', 'dealOverview.statedReasonForSale', 'textarea', 'Enter the stated reason for sale...', 4)}
</div>
);
case 'business-description':
return (
<div className="space-y-6">
{renderField('Core Operations Summary (3-5 sentences)', 'businessDescription', 'textarea', 'Describe the core operations...', 4)}
{renderField('Key Products/Services & Revenue Mix (Est. % if available)', 'businessDescription', 'textarea', 'List key products/services and revenue mix...', 4)}
{renderField('Unique Value Proposition (UVP) / Why Customers Buy', 'businessDescription', 'textarea', 'Describe the unique value proposition...', 4)}
{renderField('Core Operations Summary (3-5 sentences)', 'businessDescription.coreOperationsSummary', 'textarea', 'Describe the core operations...', 4)}
{renderField('Key Products/Services & Revenue Mix (Est. % if available)', 'businessDescription.keyProductsServices', 'textarea', 'List key products/services and revenue mix...', 4)}
{renderField('Unique Value Proposition (UVP) / Why Customers Buy', 'businessDescription.uniqueValueProposition', 'textarea', 'Describe the unique value proposition...', 4)}
<div className="space-y-4">
<h4 className="text-lg font-medium text-gray-900">Customer Base Overview</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{renderField('Key Customer Segments/Types', 'businessDescription')}
{renderField('Customer Concentration Risk (Top 5 and/or Top 10 Customers as % Revenue)', 'businessDescription')}
{renderField('Typical Contract Length / Recurring Revenue %', 'businessDescription')}
{renderField('Key Customer Segments/Types', 'businessDescription.customerBaseOverview.keyCustomerSegments')}
{renderField('Customer Concentration Risk (Top 5 and/or Top 10 Customers as % Revenue)', 'businessDescription.customerBaseOverview.customerConcentrationRisk')}
{renderField('Typical Contract Length / Recurring Revenue %', 'businessDescription.customerBaseOverview.typicalContractLength')}
</div>
</div>
<div className="space-y-4">
<h4 className="text-lg font-medium text-gray-900">Key Supplier Overview (if critical & mentioned)</h4>
{renderField('Dependence/Concentration Risk', 'businessDescription', 'textarea', 'Describe supplier dependencies...', 3)}
{renderField('Dependence/Concentration Risk', 'businessDescription.keySupplierOverview.dependenceConcentrationRisk', 'textarea', 'Describe supplier dependencies...', 3)}
</div>
</div>
);
@@ -441,21 +472,21 @@ const CIMReviewTemplate: React.FC<CIMReviewTemplateProps> = ({
return (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{renderField('Estimated Market Size (TAM/SAM - if provided)', 'marketIndustryAnalysis')}
{renderField('Estimated Market Growth Rate (% CAGR - Historical & Projected)', 'marketIndustryAnalysis')}
{renderField('Estimated Market Size (TAM/SAM - if provided)', 'marketIndustryAnalysis.estimatedMarketSize')}
{renderField('Estimated Market Growth Rate (% CAGR - Historical & Projected)', 'marketIndustryAnalysis.estimatedMarketGrowthRate')}
</div>
{renderField('Key Industry Trends & Drivers (Tailwinds/Headwinds)', 'marketIndustryAnalysis', 'textarea', 'Describe key industry trends...', 4)}
{renderField('Key Industry Trends & Drivers (Tailwinds/Headwinds)', 'marketIndustryAnalysis.keyIndustryTrends', 'textarea', 'Describe key industry trends...', 4)}
<div className="space-y-4">
<h4 className="text-lg font-medium text-gray-900">Competitive Landscape</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{renderField('Key Competitors Identified', 'marketIndustryAnalysis')}
{renderField('Target\'s Stated Market Position/Rank', 'marketIndustryAnalysis')}
{renderField('Basis of Competition', 'marketIndustryAnalysis')}
{renderField('Key Competitors Identified', 'marketIndustryAnalysis.competitiveLandscape.keyCompetitors')}
{renderField('Target\'s Stated Market Position/Rank', 'marketIndustryAnalysis.competitiveLandscape.targetMarketPosition')}
{renderField('Basis of Competition', 'marketIndustryAnalysis.competitiveLandscape.basisOfCompetition')}
</div>
</div>
{renderField('Barriers to Entry / Competitive Moat (Stated/Inferred)', 'marketIndustryAnalysis', 'textarea', 'Describe barriers to entry...', 4)}
{renderField('Barriers to Entry / Competitive Moat (Stated/Inferred)', 'marketIndustryAnalysis.barriersToEntry', 'textarea', 'Describe barriers to entry...', 4)}
</div>
);
@@ -467,12 +498,12 @@ const CIMReviewTemplate: React.FC<CIMReviewTemplateProps> = ({
<div className="space-y-4">
<h4 className="text-lg font-medium text-gray-900">Key Financial Notes & Observations</h4>
<div className="grid grid-cols-1 gap-6">
{renderField('Quality of Earnings/Adjustments (Initial Impression)', 'financialSummary', 'textarea', 'Assess quality of earnings...', 3)}
{renderField('Revenue Growth Drivers (Stated)', 'financialSummary', 'textarea', 'Identify revenue growth drivers...', 3)}
{renderField('Margin Stability/Trend Analysis', 'financialSummary', 'textarea', 'Analyze margin trends...', 3)}
{renderField('Capital Expenditures (Approx. LTM % of Revenue)', 'financialSummary')}
{renderField('Working Capital Intensity (Impression)', 'financialSummary', 'textarea', 'Assess working capital intensity...', 3)}
{renderField('Free Cash Flow (FCF) Proxy Quality (Impression)', 'financialSummary', 'textarea', 'Assess FCF quality...', 3)}
{renderField('Quality of Earnings/Adjustments (Initial Impression)', 'financialSummary.qualityOfEarnings', 'textarea', 'Assess quality of earnings...', 3)}
{renderField('Revenue Growth Drivers (Stated)', 'financialSummary.revenueGrowthDrivers', 'textarea', 'Identify revenue growth drivers...', 3)}
{renderField('Margin Stability/Trend Analysis', 'financialSummary.marginStabilityAnalysis', 'textarea', 'Analyze margin trends...', 3)}
{renderField('Capital Expenditures (Approx. LTM % of Revenue)', 'financialSummary.capitalExpenditures')}
{renderField('Working Capital Intensity (Impression)', 'financialSummary.workingCapitalIntensity', 'textarea', 'Assess working capital intensity...', 3)}
{renderField('Free Cash Flow (FCF) Proxy Quality (Impression)', 'financialSummary.freeCashFlowQuality', 'textarea', 'Assess FCF quality...', 3)}
</div>
</div>
</div>
@@ -481,31 +512,31 @@ const CIMReviewTemplate: React.FC<CIMReviewTemplateProps> = ({
case 'management-team':
return (
<div className="space-y-6">
{renderField('Key Leaders Identified (CEO, CFO, COO, Head of Sales, etc.)', 'managementTeamOverview', 'textarea', 'List key leaders...', 4)}
{renderField('Initial Assessment of Quality/Experience (Based on Bios)', 'managementTeamOverview', 'textarea', 'Assess management quality...', 4)}
{renderField('Management\'s Stated Post-Transaction Role/Intentions (if mentioned)', 'managementTeamOverview', 'textarea', 'Describe post-transaction intentions...', 4)}
{renderField('Organizational Structure Overview (Impression)', 'managementTeamOverview', 'textarea', 'Describe organizational structure...', 4)}
{renderField('Key Leaders Identified (CEO, CFO, COO, Head of Sales, etc.)', 'managementTeamOverview.keyLeaders', 'textarea', 'List key leaders...', 4)}
{renderField('Initial Assessment of Quality/Experience (Based on Bios)', 'managementTeamOverview.managementQualityAssessment', 'textarea', 'Assess management quality...', 4)}
{renderField('Management\'s Stated Post-Transaction Role/Intentions (if mentioned)', 'managementTeamOverview.postTransactionIntentions', 'textarea', 'Describe post-transaction intentions...', 4)}
{renderField('Organizational Structure Overview (Impression)', 'managementTeamOverview.organizationalStructure', 'textarea', 'Describe organizational structure...', 4)}
</div>
);
case 'investment-thesis':
return (
<div className="space-y-6">
{renderField('Key Attractions / Strengths (Why Invest?)', 'preliminaryInvestmentThesis', 'textarea', 'List key attractions...', 4)}
{renderField('Potential Risks / Concerns (Why Not Invest?)', 'preliminaryInvestmentThesis', 'textarea', 'List potential risks...', 4)}
{renderField('Initial Value Creation Levers (How PE Adds Value)', 'preliminaryInvestmentThesis', 'textarea', 'Identify value creation levers...', 4)}
{renderField('Alignment with Fund Strategy', 'preliminaryInvestmentThesis', 'textarea', 'Assess alignment with BPCP strategy...', 4)}
{renderField('Key Attractions / Strengths (Why Invest?)', 'preliminaryInvestmentThesis.keyAttractions', 'textarea', 'List key attractions...', 4)}
{renderField('Potential Risks / Concerns (Why Not Invest?)', 'preliminaryInvestmentThesis.potentialRisks', 'textarea', 'List potential risks...', 4)}
{renderField('Initial Value Creation Levers (How PE Adds Value)', 'preliminaryInvestmentThesis.valueCreationLevers', 'textarea', 'Identify value creation levers...', 4)}
{renderField('Alignment with Fund Strategy', 'preliminaryInvestmentThesis.alignmentWithFundStrategy', 'textarea', 'Assess alignment with BPCP strategy...', 4)}
</div>
);
case 'next-steps':
return (
<div className="space-y-6">
{renderField('Critical Questions Arising from CIM Review', 'keyQuestionsNextSteps', 'textarea', 'List critical questions...', 4)}
{renderField('Key Missing Information / Areas for Diligence Focus', 'keyQuestionsNextSteps', 'textarea', 'Identify missing information...', 4)}
{renderField('Preliminary Recommendation', 'keyQuestionsNextSteps')}
{renderField('Rationale for Recommendation (Brief)', 'keyQuestionsNextSteps', 'textarea', 'Provide rationale...', 4)}
{renderField('Proposed Next Steps', 'keyQuestionsNextSteps', 'textarea', 'Outline next steps...', 4)}
{renderField('Critical Questions Arising from CIM Review', 'keyQuestionsNextSteps.criticalQuestions', 'textarea', 'List critical questions...', 4)}
{renderField('Key Missing Information / Areas for Diligence Focus', 'keyQuestionsNextSteps.missingInformation', 'textarea', 'Identify missing information...', 4)}
{renderField('Preliminary Recommendation', 'keyQuestionsNextSteps.preliminaryRecommendation')}
{renderField('Rationale for Recommendation (Brief)', 'keyQuestionsNextSteps.rationaleForRecommendation', 'textarea', 'Provide rationale...', 4)}
{renderField('Proposed Next Steps', 'keyQuestionsNextSteps.proposedNextSteps', 'textarea', 'Outline next steps...', 4)}
</div>
);