Upload 10 files
Browse files- attached_assets/Pasted--Does-the-current-PD-model-align-with-the-latest-IFRS9-regulatory-requirements-Based-on-the-pr-1743447418785.txt +184 -0
- attached_assets/Pasted-Document-Preview-File-report-test-txt-Size-5-07-KB-File-Type-TXT-Preview-document-content--1745328656867.txt +88 -0
- attached_assets/Pasted-Document-Preview-File-report-test-txt-Size-5-07-KB-File-Type-TXT-Preview-document-content--1745328760272.txt +88 -0
- attached_assets/Pasted-import-os-import-uuid-import-logging-from-pathlib-import-Path-from-typing-import-List-Dict-from-pyd-1743447350035.txt +227 -0
- attached_assets/__init__.py +1 -0
- attached_assets/consolidated_analysis.py +205 -0
- attached_assets/file_handler.py +37 -0
- attached_assets/gap_analysis.py +77 -0
- attached_assets/ifrs9_analysis.py +146 -0
- attached_assets/pdf_generator.py +201 -0
attached_assets/Pasted--Does-the-current-PD-model-align-with-the-latest-IFRS9-regulatory-requirements-Based-on-the-pr-1743447418785.txt
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
🔹 Does the current PD model align with the latest IFRS9 regulatory requirements?
|
| 2 |
+
➡️ Based on the provided original document and best practice evidence, there is insufficient data to conclusively determine whether the current PD model aligns with the latest IFRS 9 regulatory requirements. The original document lacks specific details about the current PD model's structure, validation processes, and alignment with IFRS 9 standards.
|
| 3 |
+
|
| 4 |
+
However, the best practice evidence highlights several key areas that should be addressed for compliance, such as the need for robust validation frameworks, regular model validation, and the challenges posed by intense data requirements and ambiguous regulatory standards. It also emphasizes the importance of maintaining independence, completeness, adequacy, and soundness in validation processes.
|
| 5 |
+
|
| 6 |
+
The gap lies in the absence of detailed information in the original document regarding how the current PD model addresses these best practice elements and challenges. Without this information, it is not possible to assess whether the model follows best practices or identify specific areas of non-compliance. To bridge this gap, a comprehensive review of the current PD model's validation framework, data usage, and alignment with IFRS 9 requirements is necessary.
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
🔹 Are the data sources used for PD calculation comprehensive and up-to-date?
|
| 10 |
+
➡️ To perform a GAP analysis on the query regarding the comprehensiveness and currency of data sources used for Probability of Default (PD) calculation, we need to compare the original document against the best practice evidence provided.
|
| 11 |
+
|
| 12 |
+
**Best Practice Evidence Summary:**
|
| 13 |
+
1. Historical data for PD calculation should cover 5-10 years.
|
| 14 |
+
2. External data can be used if historical data is insufficient.
|
| 15 |
+
3. PDs should incorporate cyclical and non-cyclical, systematic, and obligor-specific information.
|
| 16 |
+
4. Industry-specific factors and macroeconomic indicators should be used to enhance predictive power.
|
| 17 |
+
5. Frequent re-rating of obligors is necessary to capture changes in PDs.
|
| 18 |
+
6. Data used in pooled models must meet specific data requirements or be adjusted accordingly.
|
| 19 |
+
|
| 20 |
+
**Original Document Analysis:**
|
| 21 |
+
- The original document must be assessed to determine if it includes:
|
| 22 |
+
- Historical data spanning 5-10 years or the use of external data if such historical data is unavailable.
|
| 23 |
+
- Consideration of cyclical and non-cyclical, systematic, and obligor-specific information.
|
| 24 |
+
- Utilization of industry-specific factors and macroeconomic indicators.
|
| 25 |
+
- Procedures for frequent re-rating of obligors.
|
| 26 |
+
- Verification that pooled data meets the necessary data requirements or is adjusted.
|
| 27 |
+
|
| 28 |
+
**GAP Analysis:**
|
| 29 |
+
- **Historical Data:** If the original document does not specify the use of 5-10 years of historical data or the use of external data when historical data is lacking, there is a gap.
|
| 30 |
+
- **Incorporation of Information:** If the document does not mention the inclusion of cyclical, non-cyclical, systematic, and obligor-specific information, there is a gap.
|
| 31 |
+
- **Industry and Macroeconomic Factors:** A gap exists if the document does not address the use of industry-specific factors and macroeconomic indicators.
|
| 32 |
+
- **Frequent Re-rating:** If the document lacks procedures for frequent re-rating of obligors, this is a gap.
|
| 33 |
+
- **Pooled Data Verification:** If the document does not ensure that pooled data meets data requirements or is adjusted, there is a gap.
|
| 34 |
+
|
| 35 |
+
**Conclusion:**
|
| 36 |
+
Without the specific content of the original document, it is not possible to definitively identify gaps. If the document does not address the points outlined in the best practice evidence, it does not follow best practices, and the gaps should be addressed accordingly. If the document does cover these aspects, it aligns with best practices. If the original document is not provided, there is insufficient data to perform a complete GAP analysis.
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
🔹 Is there a robust process in place for the regular back-testing of PD models?
|
| 40 |
+
➡️ Based on the original validation document and the best practice evidence provided, there appears to be a gap in the process for the regular back-testing of PD models. The best practice evidence emphasizes the importance of regular back-testing activities to assess the accuracy of the term structure of PD models. It suggests that validation should focus on minimizing predictive error using historical and economic forecast data, and should include measures of forecasting error and assessments of the discriminatory and predictive power of PDs. Additionally, it highlights the need for adequate governance, including policies and procedures, to ensure model accuracy and consistency.
|
| 41 |
+
|
| 42 |
+
The original document, however, does not provide sufficient detail on whether these best practices are being followed. There is no mention of regular back-testing activities, measures of forecasting error, or assessments of the discriminatory and predictive power of PDs. Furthermore, the document lacks information on the governance framework in place to ensure the accuracy and consistency of the models.
|
| 43 |
+
|
| 44 |
+
Therefore, the gap identified is the lack of evidence in the original document regarding the implementation of a robust process for regular back-testing of PD models, as well as the absence of a detailed governance framework to support this process. To align with best practices, the document should include specific details on the back-testing activities conducted, the measures used to assess model performance, and the governance policies and procedures established to ensure model accuracy and consistency.
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
🔹 How is the segmentation of portfolios handled in the PD model, and is it appropriate?
|
| 48 |
+
➡️ Based on the original document and best practice evidence provided, the GAP analysis for the segmentation of portfolios in the PD model is as follows:
|
| 49 |
+
|
| 50 |
+
1. **Segmentation Definition and Objectives**: The original document provides a clear definition of SME portfolio segmentation and outlines its objectives, which align with best practices. The focus on optimizing credit portfolio management and considering credit risk is appropriate.
|
| 51 |
+
|
| 52 |
+
2. **Segmentation Characteristics**: The document lists meaningful dimensions for segmentation, such as market segment, origination channel, geographic location, cohort, and underwriting criteria. These dimensions are consistent with best practices, as they allow for a comprehensive understanding of different portfolio segments.
|
| 53 |
+
|
| 54 |
+
3. **Issues and Challenges**: The document acknowledges challenges such as the lack of an intrinsic size metric for SMEs and the need for objective and reproducible segmentation criteria. It also emphasizes the importance of having a statistically meaningful number of segments, which aligns with best practices.
|
| 55 |
+
|
| 56 |
+
4. **Consideration of Behavioural Lives and Historical Information**: The best practice evidence suggests that segmentation should reflect different behavioural lives for different portfolio segments and consider whether historical behavioural information captures current conditions and forward-looking information. The original document does not explicitly address these aspects, indicating a gap in ensuring that segmentation reflects behavioural differences and incorporates relevant historical and forward-looking data.
|
| 57 |
+
|
| 58 |
+
**GAP**: The original document lacks explicit guidance on ensuring that segmentation reflects different behavioural lives of portfolio segments and the integration of historical and forward-looking information. This gap suggests that the segmentation approach may not fully capture the dynamic nature of credit risk and behavioural changes over time, which is crucial for accurate PD modeling.
|
| 59 |
+
|
| 60 |
+
**Recommendation**: To align with best practices, the document should include strategies for incorporating behavioural differences and adjusting historical data to reflect current and future conditions in the segmentation process. This would enhance the accuracy and relevance of the PD model.
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
🔹 Are the assumptions used in the PD model clearly documented and justified?
|
| 64 |
+
➡️ Based on the original validation document provided, it is unclear whether the assumptions used in the Probability of Default (PD) model are clearly documented and justified. The best practice evidence suggests that a well-documented model facilitates validation and periodic review. However, the original document does not provide sufficient information to determine if the assumptions are explicitly stated and supported with appropriate justification. This represents a gap in documentation, as best practices require clear articulation and rationale for all assumptions to ensure transparency and ease of validation. To address this gap, the original document should be revised to include a comprehensive section detailing each assumption, along with the reasoning and evidence supporting them.
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
🔹 What is the process for incorporating forward-looking information into the PD model?
|
| 68 |
+
➡️ To perform a GAP analysis on the process for incorporating forward-looking information into the Probability of Default (PD) model, we need to compare the original document's approach with the best practice evidence provided.
|
| 69 |
+
|
| 70 |
+
**Original Document:**
|
| 71 |
+
The original document mentions that the Group formulates a 'base case' view of future economic variables and a range of other possible forecast scenarios. These forecasts are then used to adjust estimates of PDs. However, the document does not provide detailed information on how forward-looking information is integrated into the PD model, nor does it specify the methodologies or considerations involved in this process.
|
| 72 |
+
|
| 73 |
+
**Best Practice Evidence:**
|
| 74 |
+
The best practice evidence outlines several key considerations for incorporating forward-looking information into Expected Credit Loss (ECL) models under IFRS 9:
|
| 75 |
+
1. Use of sound judgment and generally accepted methods for economic analysis and forecasting.
|
| 76 |
+
2. Demonstration of how relevant, reasonable, and supportable information is considered in the ECL assessment.
|
| 77 |
+
3. Application of experienced credit judgment in considering future scenarios and their potential impacts.
|
| 78 |
+
4. Inclusion of information even if the likelihood of an event is low or its impact is uncertain, unless it is not reasonable and supportable.
|
| 79 |
+
5. Documentation and justification for excluding information in exceptional circumstances.
|
| 80 |
+
6. Unbiased consideration of relevant factors affecting creditworthiness and cash shortfalls.
|
| 81 |
+
|
| 82 |
+
**GAP Analysis:**
|
| 83 |
+
The original document lacks specific details on several best practice elements:
|
| 84 |
+
- It does not describe the methodologies or frameworks used for economic analysis and forecasting.
|
| 85 |
+
- There is no mention of how the Group ensures the information is reasonable and supportable.
|
| 86 |
+
- The document does not discuss the application of credit judgment in scenario analysis or the consideration of low-likelihood events.
|
| 87 |
+
- There is no information on the documentation and justification process for excluding certain information.
|
| 88 |
+
|
| 89 |
+
**Conclusion:**
|
| 90 |
+
The original document does not fully align with best practices for incorporating forward-looking information into the PD model. It lacks detailed descriptions of the methodologies, judgment applications, and documentation processes that are considered best practice under IFRS 9. To close this gap, the document should include comprehensive details on these aspects to ensure a robust and transparent approach to integrating forward-looking information into the PD model.
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
🔹 Is there a mechanism for ongoing monitoring and validation of the PD model's performance?
|
| 94 |
+
➡️ Based on the original validation document provided, there is insufficient data to determine whether there is a mechanism for ongoing monitoring and validation of the PD model's performance. The document does not explicitly mention any established governance, policies, or procedures for ensuring the accuracy and consistency of the model, nor does it detail any periodic validation or monitoring processes.
|
| 95 |
+
|
| 96 |
+
Best practice evidence suggests that there should be a structured approach to model validation, including periodic validations and ongoing monitoring to assess the model's performance and identify any potential deterioration. This includes comparing the model's performance at development with its performance over subsequent periods and using monitoring as an early warning indicator.
|
| 97 |
+
|
| 98 |
+
The gap identified is the lack of explicit mention of these best practices in the original document. To align with best practices, the document should include detailed procedures for periodic validation and ongoing monitoring, as well as governance structures to ensure the model's continued accuracy and relevance.
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
🔹 How are overrides and expert judgments applied in the PD model, and are they documented?
|
| 102 |
+
➡️ To perform a GAP analysis on the application of overrides and expert judgments in the PD model, we need to compare the original validation document against the best practice evidence provided. However, the original document is not provided in your query, which makes it impossible to directly assess whether it follows best practices. Therefore, I will outline the best practices based on the evidence provided and identify potential gaps that could exist if these practices are not followed.
|
| 103 |
+
|
| 104 |
+
### Best Practices for Overrides and Expert Judgments:
|
| 105 |
+
|
| 106 |
+
1. **Framework and Guidelines**: There should be a clear framework with detailed guidelines and procedures for applying human judgment, including the use of pre-defined questionnaires.
|
| 107 |
+
|
| 108 |
+
2. **Documentation**: The use of human judgment must be documented to ensure that the rating assignment can be understood and replicated by a third party.
|
| 109 |
+
|
| 110 |
+
3. **Criteria for Qualitative Inputs**: Institutions should have clear criteria for using qualitative model inputs and ensure consistent application across personnel.
|
| 111 |
+
|
| 112 |
+
4. **Policies for Overrides**: There should be specific policies for using overrides in the rating assignment process, covering both inputs and outputs. These policies should be conservative, limiting potential decreases in estimates.
|
| 113 |
+
|
| 114 |
+
5. **Documentation of Overrides**: Each override should be documented with its scale and rationale, including a predefined list of justifications, the date, and the person responsible.
|
| 115 |
+
|
| 116 |
+
6. **Monitoring and Analysis**: Institutions should regularly monitor override levels and justifications, specifying maximum acceptable rates and taking measures if these are breached. They should also analyze the performance of exposures with overrides.
|
| 117 |
+
|
| 118 |
+
### Potential Gaps:
|
| 119 |
+
|
| 120 |
+
- **Lack of Framework**: If the original document does not establish a clear framework for applying human judgment, this would be a significant gap.
|
| 121 |
+
|
| 122 |
+
- **Inadequate Documentation**: Failure to document the use of human judgment or overrides comprehensively would not align with best practices.
|
| 123 |
+
|
| 124 |
+
- **Unclear Criteria**: If there are no clear criteria for qualitative inputs or inconsistent application, this would be a gap.
|
| 125 |
+
|
| 126 |
+
- **Absence of Override Policies**: Not having specific, conservative policies for overrides would be a deviation from best practices.
|
| 127 |
+
|
| 128 |
+
- **Insufficient Monitoring**: If the document does not outline regular monitoring and analysis of overrides, this would be a gap.
|
| 129 |
+
|
| 130 |
+
Without the original validation document, it is not possible to definitively identify specific gaps. If you can provide the original document or more details, a more precise GAP analysis can be conducted.
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
🔹 Are there clear governance and oversight structures for PD model development and validation?
|
| 134 |
+
➡️ Based on the original validation document and the best practice evidence provided, there appears to be a gap in the governance and oversight structures for PD model development and validation. The best practice evidence outlines several key components that should be present in an adequate governance framework, including the establishment of policies and procedures to ensure model accuracy, consistency, and risk component estimation. It also emphasizes the need for model validation at both the development stage and through periodic validation and monitoring thereafter. Additionally, the best practices highlight the importance of having a governance framework that includes transparency between accounting and risk interpretations, a forum for challenging and approving impairments, and a strong focus on data integrity and model validation.
|
| 135 |
+
|
| 136 |
+
The original document, however, does not provide sufficient information to confirm the presence of these elements. There is no mention of specific policies or procedures in place to ensure model accuracy and consistency, nor is there evidence of a structured process for periodic validation and monitoring. Furthermore, the document lacks details on the governance framework over model design, development, and maintenance, as well as the existence of forums for review and approval of impairments. The absence of these critical components suggests a gap between the current practices and the best practice standards. Therefore, it is recommended that the organization establish a comprehensive governance framework that includes clear policies, procedures, and oversight mechanisms to align with best practices.
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
🔹 What are the key risk factors considered in the PD model, and how are they quantified?
|
| 140 |
+
➡️ To perform a GAP analysis, we need to compare the original document's content regarding the key risk factors considered in the Probability of Default (PD) model and their quantification against the best practice evidence provided.
|
| 141 |
+
|
| 142 |
+
**Best Practice Evidence Summary:**
|
| 143 |
+
1. PD models should incorporate both quantitative and qualitative factors indicative of credit risk.
|
| 144 |
+
2. If qualitative factors cannot be included in the quantitative assessment, they should be considered separately.
|
| 145 |
+
3. PD models can be developed based on historical default data and should reflect current conditions (PIT PD) or average conditions over an economic cycle (TTC PD).
|
| 146 |
+
4. PD estimates should be based on statistical models using both internal data and, where available, market data.
|
| 147 |
+
|
| 148 |
+
**Original Document Analysis:**
|
| 149 |
+
The original document is not provided in the query, so we cannot directly assess its content. However, the best practice evidence outlines clear expectations for PD models, including the integration of qualitative factors, the use of historical data, and the application of statistical models.
|
| 150 |
+
|
| 151 |
+
**GAP Analysis:**
|
| 152 |
+
Without the original document, we cannot definitively determine if there is a gap. However, the analysis should focus on whether the original document:
|
| 153 |
+
- Includes both quantitative and qualitative factors in the PD model.
|
| 154 |
+
- Addresses the recalibration or adjustment of PDs when qualitative factors cannot be quantitatively assessed.
|
| 155 |
+
- Describes the use of historical default data and the types of PD models (PIT PD, TTC PD).
|
| 156 |
+
- Utilizes statistical models and considers market data for large corporate counterparties.
|
| 157 |
+
|
| 158 |
+
If the original document lacks any of these elements, it would not fully align with best practices, indicating a gap. If the document does not provide sufficient detail on these aspects, it would also suggest a gap in meeting best practice standards.
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
🔹 Is there a process for recalibrating the PD model in response to changing economic conditions?
|
| 162 |
+
➡️ The original document does not provide sufficient information to determine whether there is a process for recalibrating the PD model in response to changing economic conditions. The best practice evidence suggests that under a Point-in-Time (PIT) Probability of Default (PD) approach, PDs should be frequently updated to reflect changes in both cyclical and non-cyclical factors, including industry-specific and macroeconomic indicators. This requires a process for frequent re-rating of obligors to capture changes in their PDs.
|
| 163 |
+
|
| 164 |
+
The original document mentions the use of forecasts to adjust PD estimates based on advice from the Group Market Risk Committee and economic experts, but it does not explicitly describe a process for recalibrating the PD model in response to changing economic conditions. Specifically, it lacks details on how often recalibration occurs, what specific factors are considered, and how the recalibration process is implemented.
|
| 165 |
+
|
| 166 |
+
Therefore, the gap lies in the absence of a detailed and explicit process for recalibrating the PD model in response to changing economic conditions, as recommended by best practice evidence. To align with best practices, the document should include a clear description of the recalibration process, including the frequency of updates, the factors considered, and the methodology used to adjust PD estimates.
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
🔹 How is model risk managed and mitigated in the context of PD estimation under IFRS9?
|
| 170 |
+
➡️ Based on the original validation document provided, there are several gaps in the management and mitigation of model risk in the context of PD estimation under IFRS 9 when compared to best practice evidence.
|
| 171 |
+
|
| 172 |
+
1. **Lack of Comprehensive Model Validation Framework**: The document mentions the need for validation and monitoring tests to assess credit scoring models' quality but does not provide a detailed framework or methodology for how this validation should be conducted. Best practices would include a comprehensive validation framework that outlines specific tests, frequency of validation, and criteria for model performance assessment.
|
| 173 |
+
|
| 174 |
+
2. **Insufficient Detail on Model Risk Management**: While the document acknowledges increased model risk over long time horizons, it does not elaborate on specific strategies or controls to manage and mitigate this risk. Best practices would involve detailed risk management strategies, including stress testing, back-testing, and scenario analysis to ensure model robustness.
|
| 175 |
+
|
| 176 |
+
3. **Data Quality and Governance**: The document highlights the importance of underlying data quality but lacks a discussion on data governance practices. Best practices would include a robust data governance framework to ensure data accuracy, completeness, and timeliness, which are critical for reliable PD estimation.
|
| 177 |
+
|
| 178 |
+
4. **Ongoing Monitoring and Reporting**: There is no mention of ongoing monitoring and reporting mechanisms to track model performance over time. Best practices would include regular monitoring and reporting processes to identify model drift and recalibrate models as necessary.
|
| 179 |
+
|
| 180 |
+
5. **Documentation and Transparency**: The document does not provide sufficient detail on the documentation and transparency of the model development and validation process. Best practices would require thorough documentation of model assumptions, limitations, and validation results to ensure transparency and facilitate regulatory compliance.
|
| 181 |
+
|
| 182 |
+
6. **Integration with Risk Management Framework**: The document does not discuss how PD models are integrated into the broader risk management framework of the institution. Best practices would involve aligning PD models with the institution's overall risk appetite and risk management strategies.
|
| 183 |
+
|
| 184 |
+
In summary, the original document lacks detailed guidance on several key aspects of model risk management and mitigation, which are essential for aligning with best practices in the context of PD estimation under IFRS 9.
|
attached_assets/Pasted-Document-Preview-File-report-test-txt-Size-5-07-KB-File-Type-TXT-Preview-document-content--1745328656867.txt
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Document Preview
|
| 2 |
+
File: report_test.txt
|
| 3 |
+
|
| 4 |
+
Size: 5.07 KB
|
| 5 |
+
|
| 6 |
+
File Type: TXT
|
| 7 |
+
|
| 8 |
+
Preview document content
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
❌ Error during GAP analysis: Error code: 401 - {'error': {'message': 'Incorrect API key provided: sk-rU153***********************************************************************************A1cA. You can find your API key at https://platform.openai.com/account/api-keys.', 'type': 'invalid_request_error', 'param': None, 'code': 'invalid_api_key'}}
|
| 12 |
+
|
| 13 |
+
AuthenticationError: Error code: 401 - {'error': {'message': 'Incorrect API key provided: sk-rU153***********************************************************************************A1cA. You can find your API key at https://platform.openai.com/account/api-keys.', 'type': 'invalid_request_error', 'param': None, 'code': 'invalid_api_key'}}
|
| 14 |
+
Traceback:
|
| 15 |
+
File "/home/runner/workspace/app.py", line 162, in main
|
| 16 |
+
results = run_gap_analysis_pipeline(file_contents)
|
| 17 |
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 18 |
+
File "/home/runner/workspace/attached_assets/ifrs9_analysis.py", line 141, in run_gap_analysis_pipeline
|
| 19 |
+
final_state = graph.invoke(initial_state)
|
| 20 |
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 21 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langgraph/pregel/__init__.py", line 2683, in invoke
|
| 22 |
+
for chunk in self.stream(
|
| 23 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langgraph/pregel/__init__.py", line 2331, in stream
|
| 24 |
+
for _ in runner.tick(
|
| 25 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langgraph/pregel/runner.py", line 146, in tick
|
| 26 |
+
run_with_retry(
|
| 27 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langgraph/pregel/retry.py", line 40, in run_with_retry
|
| 28 |
+
return task.proc.invoke(task.input, config)
|
| 29 |
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 30 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langgraph/utils/runnable.py", line 606, in invoke
|
| 31 |
+
input = step.invoke(input, config, **kwargs)
|
| 32 |
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 33 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langgraph/utils/runnable.py", line 371, in invoke
|
| 34 |
+
ret = context.run(self.func, *args, **kwargs)
|
| 35 |
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 36 |
+
File "/home/runner/workspace/attached_assets/ifrs9_analysis.py", line 83, in generate_queries
|
| 37 |
+
response = query_chain.run(doc=state.original_doc, query_count=query_count)
|
| 38 |
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 39 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langchain_core/_api/deprecation.py", line 181, in warning_emitting_wrapper
|
| 40 |
+
return wrapped(*args, **kwargs)
|
| 41 |
+
^^^^^^^^^^^^^^^^^^^^^^^^
|
| 42 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langchain/chains/base.py", line 611, in run
|
| 43 |
+
return self(kwargs, callbacks=callbacks, tags=tags, metadata=metadata)[
|
| 44 |
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 45 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langchain_core/_api/deprecation.py", line 181, in warning_emitting_wrapper
|
| 46 |
+
return wrapped(*args, **kwargs)
|
| 47 |
+
^^^^^^^^^^^^^^^^^^^^^^^^
|
| 48 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langchain/chains/base.py", line 389, in __call__
|
| 49 |
+
return self.invoke(
|
| 50 |
+
^^^^^^^^^^^^
|
| 51 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langchain/chains/base.py", line 170, in invoke
|
| 52 |
+
raise e
|
| 53 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langchain/chains/base.py", line 160, in invoke
|
| 54 |
+
self._call(inputs, run_manager=run_manager)
|
| 55 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langchain/chains/llm.py", line 126, in _call
|
| 56 |
+
response = self.generate([inputs], run_manager=run_manager)
|
| 57 |
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 58 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langchain/chains/llm.py", line 138, in generate
|
| 59 |
+
return self.llm.generate_prompt(
|
| 60 |
+
^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 61 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langchain_core/language_models/chat_models.py", line 843, in generate_prompt
|
| 62 |
+
return self.generate(prompt_messages, stop=stop, callbacks=callbacks, **kwargs)
|
| 63 |
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 64 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langchain_core/language_models/chat_models.py", line 683, in generate
|
| 65 |
+
self._generate_with_cache(
|
| 66 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langchain_core/language_models/chat_models.py", line 908, in _generate_with_cache
|
| 67 |
+
result = self._generate(
|
| 68 |
+
^^^^^^^^^^^^^^^
|
| 69 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langchain_community/chat_models/openai.py", line 476, in _generate
|
| 70 |
+
response = self.completion_with_retry(
|
| 71 |
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 72 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langchain_community/chat_models/openai.py", line 387, in completion_with_retry
|
| 73 |
+
return self.client.create(**kwargs)
|
| 74 |
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 75 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/openai/_utils/_utils.py", line 279, in wrapper
|
| 76 |
+
return func(*args, **kwargs)
|
| 77 |
+
^^^^^^^^^^^^^^^^^^^^^
|
| 78 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/openai/resources/chat/completions/completions.py", line 914, in create
|
| 79 |
+
return self._post(
|
| 80 |
+
^^^^^^^^^^^
|
| 81 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/openai/_base_client.py", line 1242, in post
|
| 82 |
+
return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))
|
| 83 |
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 84 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/openai/_base_client.py", line 919, in request
|
| 85 |
+
return self._request(
|
| 86 |
+
^^^^^^^^^^^^^^
|
| 87 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/openai/_base_client.py", line 1023, in _request
|
| 88 |
+
raise self._make_status_error_from_response(err.response) from None
|
attached_assets/Pasted-Document-Preview-File-report-test-txt-Size-5-07-KB-File-Type-TXT-Preview-document-content--1745328760272.txt
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Document Preview
|
| 2 |
+
File: report_test.txt
|
| 3 |
+
|
| 4 |
+
Size: 5.07 KB
|
| 5 |
+
|
| 6 |
+
File Type: TXT
|
| 7 |
+
|
| 8 |
+
Preview document content
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
❌ Error during GAP analysis: Error code: 401 - {'error': {'message': 'Incorrect API key provided: sk-rU153***********************************************************************************A1cA. You can find your API key at https://platform.openai.com/account/api-keys.', 'type': 'invalid_request_error', 'param': None, 'code': 'invalid_api_key'}}
|
| 12 |
+
|
| 13 |
+
AuthenticationError: Error code: 401 - {'error': {'message': 'Incorrect API key provided: sk-rU153***********************************************************************************A1cA. You can find your API key at https://platform.openai.com/account/api-keys.', 'type': 'invalid_request_error', 'param': None, 'code': 'invalid_api_key'}}
|
| 14 |
+
Traceback:
|
| 15 |
+
File "/home/runner/workspace/app.py", line 162, in main
|
| 16 |
+
results = run_gap_analysis_pipeline(file_contents)
|
| 17 |
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 18 |
+
File "/home/runner/workspace/attached_assets/ifrs9_analysis.py", line 141, in run_gap_analysis_pipeline
|
| 19 |
+
final_state = graph.invoke(initial_state)
|
| 20 |
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 21 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langgraph/pregel/__init__.py", line 2683, in invoke
|
| 22 |
+
for chunk in self.stream(
|
| 23 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langgraph/pregel/__init__.py", line 2331, in stream
|
| 24 |
+
for _ in runner.tick(
|
| 25 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langgraph/pregel/runner.py", line 146, in tick
|
| 26 |
+
run_with_retry(
|
| 27 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langgraph/pregel/retry.py", line 40, in run_with_retry
|
| 28 |
+
return task.proc.invoke(task.input, config)
|
| 29 |
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 30 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langgraph/utils/runnable.py", line 606, in invoke
|
| 31 |
+
input = step.invoke(input, config, **kwargs)
|
| 32 |
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 33 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langgraph/utils/runnable.py", line 371, in invoke
|
| 34 |
+
ret = context.run(self.func, *args, **kwargs)
|
| 35 |
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 36 |
+
File "/home/runner/workspace/attached_assets/ifrs9_analysis.py", line 83, in generate_queries
|
| 37 |
+
response = query_chain.run(doc=state.original_doc, query_count=query_count)
|
| 38 |
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 39 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langchain_core/_api/deprecation.py", line 181, in warning_emitting_wrapper
|
| 40 |
+
return wrapped(*args, **kwargs)
|
| 41 |
+
^^^^^^^^^^^^^^^^^^^^^^^^
|
| 42 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langchain/chains/base.py", line 611, in run
|
| 43 |
+
return self(kwargs, callbacks=callbacks, tags=tags, metadata=metadata)[
|
| 44 |
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 45 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langchain_core/_api/deprecation.py", line 181, in warning_emitting_wrapper
|
| 46 |
+
return wrapped(*args, **kwargs)
|
| 47 |
+
^^^^^^^^^^^^^^^^^^^^^^^^
|
| 48 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langchain/chains/base.py", line 389, in __call__
|
| 49 |
+
return self.invoke(
|
| 50 |
+
^^^^^^^^^^^^
|
| 51 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langchain/chains/base.py", line 170, in invoke
|
| 52 |
+
raise e
|
| 53 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langchain/chains/base.py", line 160, in invoke
|
| 54 |
+
self._call(inputs, run_manager=run_manager)
|
| 55 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langchain/chains/llm.py", line 126, in _call
|
| 56 |
+
response = self.generate([inputs], run_manager=run_manager)
|
| 57 |
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 58 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langchain/chains/llm.py", line 138, in generate
|
| 59 |
+
return self.llm.generate_prompt(
|
| 60 |
+
^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 61 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langchain_core/language_models/chat_models.py", line 843, in generate_prompt
|
| 62 |
+
return self.generate(prompt_messages, stop=stop, callbacks=callbacks, **kwargs)
|
| 63 |
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 64 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langchain_core/language_models/chat_models.py", line 683, in generate
|
| 65 |
+
self._generate_with_cache(
|
| 66 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langchain_core/language_models/chat_models.py", line 908, in _generate_with_cache
|
| 67 |
+
result = self._generate(
|
| 68 |
+
^^^^^^^^^^^^^^^
|
| 69 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langchain_community/chat_models/openai.py", line 476, in _generate
|
| 70 |
+
response = self.completion_with_retry(
|
| 71 |
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 72 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/langchain_community/chat_models/openai.py", line 387, in completion_with_retry
|
| 73 |
+
return self.client.create(**kwargs)
|
| 74 |
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 75 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/openai/_utils/_utils.py", line 279, in wrapper
|
| 76 |
+
return func(*args, **kwargs)
|
| 77 |
+
^^^^^^^^^^^^^^^^^^^^^
|
| 78 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/openai/resources/chat/completions/completions.py", line 914, in create
|
| 79 |
+
return self._post(
|
| 80 |
+
^^^^^^^^^^^
|
| 81 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/openai/_base_client.py", line 1242, in post
|
| 82 |
+
return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))
|
| 83 |
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 84 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/openai/_base_client.py", line 919, in request
|
| 85 |
+
return self._request(
|
| 86 |
+
^^^^^^^^^^^^^^
|
| 87 |
+
File "/home/runner/workspace/.pythonlibs/lib/python3.11/site-packages/openai/_base_client.py", line 1023, in _request
|
| 88 |
+
raise self._make_status_error_from_response(err.response) from None
|
attached_assets/Pasted-import-os-import-uuid-import-logging-from-pathlib-import-Path-from-typing-import-List-Dict-from-pyd-1743447350035.txt
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import uuid
|
| 3 |
+
import logging
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import List, Dict
|
| 6 |
+
from pydantic import BaseModel, Field
|
| 7 |
+
|
| 8 |
+
from langchain.chat_models import ChatOpenAI
|
| 9 |
+
from langchain.prompts import PromptTemplate
|
| 10 |
+
from langchain.chains import LLMChain
|
| 11 |
+
from langchain.output_parsers import PydanticOutputParser
|
| 12 |
+
from langchain.vectorstores import FAISS
|
| 13 |
+
from langchain_community.embeddings import HuggingFaceEmbeddings
|
| 14 |
+
from langchain_core.documents import Document
|
| 15 |
+
|
| 16 |
+
from langgraph.graph import StateGraph, END
|
| 17 |
+
|
| 18 |
+
# ========================
|
| 19 |
+
# Logging Setup
|
| 20 |
+
# ========================
|
| 21 |
+
logging.basicConfig(
|
| 22 |
+
level=logging.INFO,
|
| 23 |
+
format="%(asctime)s - %(levelname)s - %(message)s"
|
| 24 |
+
)
|
| 25 |
+
logger = logging.getLogger(__name__)
|
| 26 |
+
|
| 27 |
+
LLM_LOG_DIR = Path("logs/llm_logs")
|
| 28 |
+
LLM_LOG_DIR.mkdir(parents=True, exist_ok=True)
|
| 29 |
+
|
| 30 |
+
def log_llm_io(step: str, input_text: str, output_text: str):
|
| 31 |
+
filename = LLM_LOG_DIR / f"{step}_{uuid.uuid4().hex[:8]}.txt"
|
| 32 |
+
with open(filename, "w", encoding="utf-8") as f:
|
| 33 |
+
f.write("=== INPUT ===\n")
|
| 34 |
+
f.write(input_text.strip() + "\n\n")
|
| 35 |
+
f.write("=== OUTPUT ===\n")
|
| 36 |
+
f.write(output_text.strip())
|
| 37 |
+
logger.debug(f"📝 Logged LLM I/O to: {filename}")
|
| 38 |
+
|
| 39 |
+
# ========================
|
| 40 |
+
# LLM Setup (GPT-4o)
|
| 41 |
+
# ========================
|
| 42 |
+
llm = ChatOpenAI(
|
| 43 |
+
model="gpt-4o",
|
| 44 |
+
temperature=0,
|
| 45 |
+
api_key=os.environ["OPENAI_API_KEY"]
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
# ========================
|
| 49 |
+
# Embeddings & Vector Store
|
| 50 |
+
# ========================
|
| 51 |
+
embedding_model = HuggingFaceEmbeddings(
|
| 52 |
+
model_name="intfloat/e5-large-v2",
|
| 53 |
+
model_kwargs={"device": "cpu"}
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
vector_store = FAISS.load_local(
|
| 57 |
+
"gpu_vectorstore",
|
| 58 |
+
embeddings=embedding_model,
|
| 59 |
+
allow_dangerous_deserialization=True
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
retriever_catA = vector_store.as_retriever(
|
| 63 |
+
search_type="similarity",
|
| 64 |
+
search_kwargs={"k": 10, "filter": {"category": "catA"}}
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
retriever_catB = vector_store.as_retriever(
|
| 69 |
+
search_type="similarity",
|
| 70 |
+
search_kwargs={"k": 10, "filter": {"category": "catB"}}
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
# ========================
|
| 74 |
+
# State Definition
|
| 75 |
+
# ========================
|
| 76 |
+
class GapAnalysisState(BaseModel):
|
| 77 |
+
original_doc: str
|
| 78 |
+
queries: List[str] = []
|
| 79 |
+
raw_results: Dict[str, Dict[str, List[Document]]] = {}
|
| 80 |
+
filtered_results: Dict[str, List[Document]] = {}
|
| 81 |
+
gap_analyses: Dict[str, str] = {}
|
| 82 |
+
|
| 83 |
+
# ========================
|
| 84 |
+
# Step 1: Query Generation
|
| 85 |
+
# ========================
|
| 86 |
+
class QueryListOutput(BaseModel):
|
| 87 |
+
queries: List[str] = Field(..., description="List of GAP analysis questions")
|
| 88 |
+
|
| 89 |
+
query_parser = PydanticOutputParser(pydantic_object=QueryListOutput)
|
| 90 |
+
|
| 91 |
+
query_prompt = PromptTemplate.from_template(
|
| 92 |
+
"You are an expert in IFRS9 PD validation. Based on the following document, extract 12 critical questions to perform a GAP analysis.\n\n{doc}\n\n{format_instructions}"
|
| 93 |
+
).partial(format_instructions=query_parser.get_format_instructions())
|
| 94 |
+
|
| 95 |
+
query_chain = LLMChain(llm=llm, prompt=query_prompt)
|
| 96 |
+
|
| 97 |
+
def generate_queries(state: GapAnalysisState) -> GapAnalysisState:
|
| 98 |
+
logger.info("🟦 Generating queries from original document...")
|
| 99 |
+
llm_input = query_prompt.format(doc=state.original_doc)
|
| 100 |
+
response = query_chain.run(doc=state.original_doc)
|
| 101 |
+
log_llm_io("01_generate_queries", llm_input, response)
|
| 102 |
+
state.queries = query_parser.parse(response).queries
|
| 103 |
+
logger.info(f"✅ Generated {len(state.queries)} queries.")
|
| 104 |
+
return state
|
| 105 |
+
|
| 106 |
+
# ========================
|
| 107 |
+
# Step 2: Vector Retrieval
|
| 108 |
+
# ========================
|
| 109 |
+
def run_retrieval(state: GapAnalysisState) -> GapAnalysisState:
|
| 110 |
+
logger.info("🟦 Running vector store retrieval for all queries...")
|
| 111 |
+
state.raw_results = {}
|
| 112 |
+
for query in state.queries:
|
| 113 |
+
docsA = retriever_catA.get_relevant_documents(query)[:10]
|
| 114 |
+
docsB = retriever_catB.get_relevant_documents(query)[:10]
|
| 115 |
+
state.raw_results[query] = {
|
| 116 |
+
"catA": docsA,
|
| 117 |
+
"catB": docsB
|
| 118 |
+
}
|
| 119 |
+
logger.debug(f"🔍 Retrieved {len(docsA)} catA and {len(docsB)} catB docs for query: '{query}'")
|
| 120 |
+
logger.info("✅ Retrieval complete.")
|
| 121 |
+
return state
|
| 122 |
+
|
| 123 |
+
# ========================
|
| 124 |
+
# Step 3: Relevance Filtering
|
| 125 |
+
# ========================
|
| 126 |
+
class RelevantDocSelector(BaseModel):
|
| 127 |
+
relevant_indices: List[int] = Field(..., description="Indices of relevant excerpts")
|
| 128 |
+
|
| 129 |
+
filter_parser = PydanticOutputParser(pydantic_object=RelevantDocSelector)
|
| 130 |
+
|
| 131 |
+
filter_prompt = PromptTemplate.from_template(
|
| 132 |
+
"You are reviewing documents for this GAP query:\n\nQuery: {query}\n\n"
|
| 133 |
+
"Original Validation Document (truncated):\n{original_doc}\n\n"
|
| 134 |
+
"Candidate Excerpts:\n{excerpts}\n\n"
|
| 135 |
+
"{format_instructions}"
|
| 136 |
+
).partial(format_instructions=filter_parser.get_format_instructions())
|
| 137 |
+
|
| 138 |
+
filter_chain = LLMChain(llm=llm, prompt=filter_prompt)
|
| 139 |
+
|
| 140 |
+
def filter_docs(state: GapAnalysisState) -> GapAnalysisState:
|
| 141 |
+
logger.info("🟦 Filtering relevant documents for each query...")
|
| 142 |
+
for query in state.queries:
|
| 143 |
+
docs = state.raw_results[query]["catA"] + state.raw_results[query]["catB"]
|
| 144 |
+
excerpt_texts = "\n".join([f"[{i}] {doc.page_content}" for i, doc in enumerate(docs)])
|
| 145 |
+
|
| 146 |
+
llm_input = filter_prompt.format(
|
| 147 |
+
query=query,
|
| 148 |
+
original_doc=state.original_doc[:3000],
|
| 149 |
+
excerpts=excerpt_texts
|
| 150 |
+
)
|
| 151 |
+
response = filter_chain.run(
|
| 152 |
+
query=query,
|
| 153 |
+
original_doc=state.original_doc[:3000],
|
| 154 |
+
excerpts=excerpt_texts
|
| 155 |
+
)
|
| 156 |
+
log_llm_io(f"02_filter_docs_{state.queries.index(query)+1}", llm_input, response)
|
| 157 |
+
|
| 158 |
+
try:
|
| 159 |
+
relevant_indices = filter_parser.parse(response).relevant_indices
|
| 160 |
+
except Exception as e:
|
| 161 |
+
logger.warning(f"⚠️ Failed to parse relevant indices for query '{query}': {e}")
|
| 162 |
+
relevant_indices = []
|
| 163 |
+
|
| 164 |
+
state.filtered_results[query] = [docs[i] for i in relevant_indices if i < len(docs)]
|
| 165 |
+
logger.info("✅ Document filtering complete.")
|
| 166 |
+
return state
|
| 167 |
+
|
| 168 |
+
# ========================
|
| 169 |
+
# Step 4: GAP Analysis
|
| 170 |
+
# ========================
|
| 171 |
+
gap_prompt = PromptTemplate.from_template(
|
| 172 |
+
"You are performing a GAP analysis for this query:\n\n{query}\n\n"
|
| 173 |
+
"Based on the original validation document and the best practice evidence, determine whether the document follows best practices. "
|
| 174 |
+
"If not, explain the gap. If insufficient data, say so.\n\n"
|
| 175 |
+
"Original Document:\n{original_doc}\n\n"
|
| 176 |
+
"Best Practice Evidence:\n{best_practices}\n\n"
|
| 177 |
+
"GAP Analysis Paragraph:"
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
gap_chain = LLMChain(llm=llm, prompt=gap_prompt)
|
| 181 |
+
|
| 182 |
+
def generate_gap_analysis(state: GapAnalysisState) -> GapAnalysisState:
|
| 183 |
+
logger.info("🟦 Generating GAP analysis for each query...")
|
| 184 |
+
for query in state.queries:
|
| 185 |
+
relevant_texts = "\n\n".join([doc.page_content for doc in state.filtered_results.get(query, [])])
|
| 186 |
+
llm_input = gap_prompt.format(
|
| 187 |
+
query=query,
|
| 188 |
+
original_doc=state.original_doc[:3000],
|
| 189 |
+
best_practices=relevant_texts[:3000]
|
| 190 |
+
)
|
| 191 |
+
response = gap_chain.run(
|
| 192 |
+
query=query,
|
| 193 |
+
original_doc=state.original_doc[:3000],
|
| 194 |
+
best_practices=relevant_texts[:3000]
|
| 195 |
+
)
|
| 196 |
+
log_llm_io(f"03_gap_analysis_{state.queries.index(query)+1}", llm_input, response)
|
| 197 |
+
state.gap_analyses[query] = response.strip()
|
| 198 |
+
logger.info("✅ GAP analysis generation complete.")
|
| 199 |
+
return state
|
| 200 |
+
|
| 201 |
+
# ========================
|
| 202 |
+
# LangGraph Pipeline
|
| 203 |
+
# ========================
|
| 204 |
+
builder = StateGraph(GapAnalysisState)
|
| 205 |
+
|
| 206 |
+
builder.add_node("generate_queries", generate_queries)
|
| 207 |
+
builder.add_node("retrieve_docs", run_retrieval)
|
| 208 |
+
builder.add_node("filter_docs", filter_docs)
|
| 209 |
+
builder.add_node("generate_gap", generate_gap_analysis)
|
| 210 |
+
|
| 211 |
+
builder.set_entry_point("generate_queries")
|
| 212 |
+
builder.add_edge("generate_queries", "retrieve_docs")
|
| 213 |
+
builder.add_edge("retrieve_docs", "filter_docs")
|
| 214 |
+
builder.add_edge("filter_docs", "generate_gap")
|
| 215 |
+
builder.add_edge("generate_gap", END)
|
| 216 |
+
|
| 217 |
+
graph = builder.compile()
|
| 218 |
+
|
| 219 |
+
# ========================
|
| 220 |
+
# Entrypoint
|
| 221 |
+
# ========================
|
| 222 |
+
def run_gap_analysis_pipeline(original_doc: str):
|
| 223 |
+
logger.info("🚀 Starting GAP analysis pipeline...")
|
| 224 |
+
initial_state = GapAnalysisState(original_doc=original_doc)
|
| 225 |
+
final_state = graph.invoke(initial_state)
|
| 226 |
+
logger.info("🎉 GAP analysis pipeline completed successfully.")
|
| 227 |
+
return final_state
|
attached_assets/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# This file makes the attached_assets directory a Python package
|
attached_assets/consolidated_analysis.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import numpy as np
|
| 3 |
+
import plotly.express as px
|
| 4 |
+
import plotly.graph_objects as go
|
| 5 |
+
import pandas as pd
|
| 6 |
+
import streamlit as st
|
| 7 |
+
from typing import Dict, List, Tuple
|
| 8 |
+
from langchain_community.chat_models import ChatOpenAI
|
| 9 |
+
from langchain.prompts import PromptTemplate
|
| 10 |
+
from langchain.chains import LLMChain
|
| 11 |
+
from pydantic import BaseModel, Field
|
| 12 |
+
import logging
|
| 13 |
+
|
| 14 |
+
# Set up logging
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
class ConsolidatedAnalysisOutput(BaseModel):
|
| 18 |
+
"""Output schema for the consolidated analysis."""
|
| 19 |
+
compliance_percentage: float = Field(..., description="Overall compliance percentage with IFRS9 standards (0-100)")
|
| 20 |
+
executive_summary: str = Field(..., description="Executive summary of the key findings and main areas for improvement")
|
| 21 |
+
key_areas: List[Dict] = Field(..., description="List of key areas with their compliance scores and descriptions")
|
| 22 |
+
detailed_analysis: str = Field(..., description="Detailed consolidated gap analysis")
|
| 23 |
+
|
| 24 |
+
def generate_consolidated_analysis(queries: List[str], gap_analyses: Dict[str, str]) -> ConsolidatedAnalysisOutput:
|
| 25 |
+
"""Generate a consolidated analysis from individual gap analyses."""
|
| 26 |
+
logger.info("Generating consolidated analysis...")
|
| 27 |
+
|
| 28 |
+
# Initialize OpenAI model
|
| 29 |
+
llm = ChatOpenAI(
|
| 30 |
+
temperature=0,
|
| 31 |
+
model="gpt-4o-mini",
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
# Create a combined input from all queries and analyses
|
| 35 |
+
combined_input = "\n\n".join([f"Question: {q}\nAnalysis: {gap_analyses.get(q, '')}" for q in queries])
|
| 36 |
+
|
| 37 |
+
# Create prompt for consolidated analysis
|
| 38 |
+
consolidation_prompt = PromptTemplate.from_template(
|
| 39 |
+
"You are an expert IFRS9 consultant tasked with creating a consolidated gap analysis report.\n\n"
|
| 40 |
+
"Below are individual gap analyses for different aspects of an IFRS9 PD model implementation:\n\n"
|
| 41 |
+
"{combined_analyses}\n\n"
|
| 42 |
+
"Based on these individual analyses, create a consolidated report with the following components:\n"
|
| 43 |
+
"1. An overall compliance percentage (0-100%) that represents how well the current implementation adheres to IFRS9 standards\n"
|
| 44 |
+
"2. An executive summary highlighting the 3-5 most critical findings and areas for improvement\n"
|
| 45 |
+
"3. A list of key areas with individual compliance scores and brief descriptions\n"
|
| 46 |
+
"4. A detailed consolidated analysis that brings together all findings into a cohesive narrative\n\n"
|
| 47 |
+
"Format your response exactly as follows:\n"
|
| 48 |
+
"COMPLIANCE_PERCENTAGE: [overall percentage as a number between 0-100]\n\n"
|
| 49 |
+
"EXECUTIVE_SUMMARY:\n[Your executive summary text here]\n\n"
|
| 50 |
+
"KEY_AREAS:\n[area1_name]|[score1]|[area1_description]\n[area2_name]|[score2]|[area2_description]\n[etc.]\n\n"
|
| 51 |
+
"DETAILED_ANALYSIS:\n[Your detailed consolidated analysis here]"
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
# Create chain for consolidated analysis
|
| 55 |
+
consolidation_chain = LLMChain(llm=llm, prompt=consolidation_prompt)
|
| 56 |
+
|
| 57 |
+
# Run the chain
|
| 58 |
+
response = consolidation_chain.run(combined_analyses=combined_input)
|
| 59 |
+
|
| 60 |
+
# Parse the response
|
| 61 |
+
try:
|
| 62 |
+
sections = {}
|
| 63 |
+
current_section = None
|
| 64 |
+
section_content = []
|
| 65 |
+
|
| 66 |
+
for line in response.split("\n"):
|
| 67 |
+
if line.startswith("COMPLIANCE_PERCENTAGE:"):
|
| 68 |
+
sections["compliance_percentage"] = float(line.split(":", 1)[1].strip())
|
| 69 |
+
elif line.startswith("EXECUTIVE_SUMMARY:"):
|
| 70 |
+
current_section = "executive_summary"
|
| 71 |
+
section_content = []
|
| 72 |
+
elif line.startswith("KEY_AREAS:"):
|
| 73 |
+
if current_section:
|
| 74 |
+
sections[current_section] = "\n".join(section_content).strip()
|
| 75 |
+
current_section = "key_areas"
|
| 76 |
+
section_content = []
|
| 77 |
+
elif line.startswith("DETAILED_ANALYSIS:"):
|
| 78 |
+
if current_section:
|
| 79 |
+
sections[current_section] = "\n".join(section_content).strip()
|
| 80 |
+
current_section = "detailed_analysis"
|
| 81 |
+
section_content = []
|
| 82 |
+
else:
|
| 83 |
+
section_content.append(line)
|
| 84 |
+
|
| 85 |
+
if current_section:
|
| 86 |
+
sections[current_section] = "\n".join(section_content).strip()
|
| 87 |
+
|
| 88 |
+
# Process key areas
|
| 89 |
+
key_areas = []
|
| 90 |
+
for line in sections.get("key_areas", "").split("\n"):
|
| 91 |
+
if "|" in line:
|
| 92 |
+
parts = line.split("|")
|
| 93 |
+
if len(parts) >= 3:
|
| 94 |
+
key_areas.append({
|
| 95 |
+
"name": parts[0].strip(),
|
| 96 |
+
"score": float(parts[1].strip()),
|
| 97 |
+
"description": parts[2].strip()
|
| 98 |
+
})
|
| 99 |
+
|
| 100 |
+
return ConsolidatedAnalysisOutput(
|
| 101 |
+
compliance_percentage=sections.get("compliance_percentage", 0.0),
|
| 102 |
+
executive_summary=sections.get("executive_summary", ""),
|
| 103 |
+
key_areas=key_areas,
|
| 104 |
+
detailed_analysis=sections.get("detailed_analysis", "")
|
| 105 |
+
)
|
| 106 |
+
except Exception as e:
|
| 107 |
+
logger.error(f"Error parsing consolidated analysis: {str(e)}")
|
| 108 |
+
raise
|
| 109 |
+
|
| 110 |
+
def create_compliance_gauge(compliance_percentage: float):
|
| 111 |
+
"""Create a gauge chart for overall compliance percentage."""
|
| 112 |
+
fig = go.Figure(go.Indicator(
|
| 113 |
+
mode="gauge+number",
|
| 114 |
+
value=compliance_percentage,
|
| 115 |
+
domain={'x': [0, 1], 'y': [0, 1]},
|
| 116 |
+
title={'text': "IFRS9 Compliance Score", 'font': {'size': 24}},
|
| 117 |
+
gauge={
|
| 118 |
+
'axis': {'range': [0, 100], 'tickwidth': 1, 'tickcolor': "darkblue"},
|
| 119 |
+
'bar': {'color': "darkblue"},
|
| 120 |
+
'bgcolor': "white",
|
| 121 |
+
'borderwidth': 2,
|
| 122 |
+
'bordercolor': "gray",
|
| 123 |
+
'steps': [
|
| 124 |
+
{'range': [0, 50], 'color': 'red'},
|
| 125 |
+
{'range': [50, 75], 'color': 'orange'},
|
| 126 |
+
{'range': [75, 90], 'color': 'yellow'},
|
| 127 |
+
{'range': [90, 100], 'color': 'green'}
|
| 128 |
+
],
|
| 129 |
+
}
|
| 130 |
+
))
|
| 131 |
+
|
| 132 |
+
fig.update_layout(
|
| 133 |
+
height=300,
|
| 134 |
+
margin=dict(l=20, r=20, t=50, b=20),
|
| 135 |
+
paper_bgcolor="white",
|
| 136 |
+
font={'color': "darkblue", 'family': "Arial"}
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
return fig
|
| 140 |
+
|
| 141 |
+
def create_compliance_areas_chart(key_areas):
|
| 142 |
+
"""Create a horizontal bar chart for key area compliance scores."""
|
| 143 |
+
# Extract data from key areas
|
| 144 |
+
area_names = [area["name"] for area in key_areas]
|
| 145 |
+
scores = [area["score"] for area in key_areas]
|
| 146 |
+
|
| 147 |
+
# Generate color scale based on scores
|
| 148 |
+
colors = ['red' if score < 50 else 'orange' if score < 75 else 'yellow' if score < 90 else 'green'
|
| 149 |
+
for score in scores]
|
| 150 |
+
|
| 151 |
+
fig = go.Figure(go.Bar(
|
| 152 |
+
x=scores,
|
| 153 |
+
y=area_names,
|
| 154 |
+
orientation='h',
|
| 155 |
+
marker_color=colors,
|
| 156 |
+
text=scores,
|
| 157 |
+
textposition='auto',
|
| 158 |
+
))
|
| 159 |
+
|
| 160 |
+
fig.update_layout(
|
| 161 |
+
title="Compliance by Key Areas",
|
| 162 |
+
xaxis_title="Compliance Score (%)",
|
| 163 |
+
yaxis_title="Key Areas",
|
| 164 |
+
height=400,
|
| 165 |
+
margin=dict(l=20, r=20, t=50, b=20),
|
| 166 |
+
paper_bgcolor="white",
|
| 167 |
+
plot_bgcolor="white",
|
| 168 |
+
font={'color': "darkblue", 'family': "Arial"},
|
| 169 |
+
xaxis=dict(range=[0, 100])
|
| 170 |
+
)
|
| 171 |
+
|
| 172 |
+
return fig
|
| 173 |
+
|
| 174 |
+
def display_consolidated_analysis(consolidated_result):
|
| 175 |
+
"""Display the consolidated analysis in streamlit."""
|
| 176 |
+
# Overall compliance score gauge
|
| 177 |
+
st.header("📊 IFRS9 Compliance Analysis")
|
| 178 |
+
|
| 179 |
+
col1, col2 = st.columns([1, 2])
|
| 180 |
+
|
| 181 |
+
with col1:
|
| 182 |
+
gauge_chart = create_compliance_gauge(consolidated_result.compliance_percentage)
|
| 183 |
+
st.plotly_chart(gauge_chart, use_container_width=True)
|
| 184 |
+
|
| 185 |
+
with col2:
|
| 186 |
+
st.subheader("📝 Executive Summary")
|
| 187 |
+
st.write(consolidated_result.executive_summary)
|
| 188 |
+
|
| 189 |
+
# Key areas visualization
|
| 190 |
+
st.subheader("🔑 Key Areas for Improvement")
|
| 191 |
+
areas_chart = create_compliance_areas_chart(consolidated_result.key_areas)
|
| 192 |
+
st.plotly_chart(areas_chart, use_container_width=True)
|
| 193 |
+
|
| 194 |
+
# Display key areas details
|
| 195 |
+
cols = st.columns(len(consolidated_result.key_areas) if len(consolidated_result.key_areas) <= 3 else 3)
|
| 196 |
+
|
| 197 |
+
for i, area in enumerate(consolidated_result.key_areas):
|
| 198 |
+
with cols[i % 3]:
|
| 199 |
+
color = "red" if area["score"] < 50 else "orange" if area["score"] < 75 else "yellow" if area["score"] < 90 else "green"
|
| 200 |
+
st.markdown(f"<h4 style='color:{color};'>{area['name']} ({area['score']}%)</h4>", unsafe_allow_html=True)
|
| 201 |
+
st.write(area["description"])
|
| 202 |
+
|
| 203 |
+
# Detailed analysis
|
| 204 |
+
with st.expander("📋 Detailed Analysis", expanded=False):
|
| 205 |
+
st.write(consolidated_result.detailed_analysis)
|
attached_assets/file_handler.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import io
|
| 2 |
+
import docx
|
| 3 |
+
from PyPDF2 import PdfReader
|
| 4 |
+
|
| 5 |
+
def read_text_file(file_content):
|
| 6 |
+
"""Read content from a text file."""
|
| 7 |
+
return file_content.decode("utf-8")
|
| 8 |
+
|
| 9 |
+
def read_pdf_file(file_content):
|
| 10 |
+
"""Extract text from a PDF file."""
|
| 11 |
+
pdf_reader = PdfReader(io.BytesIO(file_content))
|
| 12 |
+
text = ""
|
| 13 |
+
for page in pdf_reader.pages:
|
| 14 |
+
text += page.extract_text() + "\n"
|
| 15 |
+
return text
|
| 16 |
+
|
| 17 |
+
def read_docx_file(file_content):
|
| 18 |
+
"""Extract text from a Word document."""
|
| 19 |
+
doc = docx.Document(io.BytesIO(file_content))
|
| 20 |
+
full_text = []
|
| 21 |
+
for para in doc.paragraphs:
|
| 22 |
+
full_text.append(para.text)
|
| 23 |
+
return '\n'.join(full_text)
|
| 24 |
+
|
| 25 |
+
def extract_text_from_file(uploaded_file):
|
| 26 |
+
"""Extract text from the uploaded file based on its type."""
|
| 27 |
+
file_extension = uploaded_file.name.split('.')[-1].lower()
|
| 28 |
+
file_content = uploaded_file.getvalue()
|
| 29 |
+
|
| 30 |
+
if file_extension == 'txt':
|
| 31 |
+
return read_text_file(file_content)
|
| 32 |
+
elif file_extension == 'pdf':
|
| 33 |
+
return read_pdf_file(file_content)
|
| 34 |
+
elif file_extension in ['docx', 'doc']:
|
| 35 |
+
return read_docx_file(file_content)
|
| 36 |
+
else:
|
| 37 |
+
raise ValueError(f"Unsupported file type: {file_extension}")
|
attached_assets/gap_analysis.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import logging
|
| 3 |
+
import re
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import List, Dict, Optional
|
| 6 |
+
|
| 7 |
+
# Simple data class for the state
|
| 8 |
+
class GapAnalysisState:
|
| 9 |
+
def __init__(self, original_doc: str):
|
| 10 |
+
self.original_doc = original_doc
|
| 11 |
+
self.queries: List[str] = []
|
| 12 |
+
self.gap_analyses: Dict[str, str] = {}
|
| 13 |
+
|
| 14 |
+
# Configure logging
|
| 15 |
+
logging.basicConfig(
|
| 16 |
+
level=logging.INFO,
|
| 17 |
+
format="%(asctime)s - %(levelname)s - %(message)s"
|
| 18 |
+
)
|
| 19 |
+
logger = logging.getLogger(__name__)
|
| 20 |
+
|
| 21 |
+
def run_gap_analysis_pipeline(original_doc: str) -> GapAnalysisState:
|
| 22 |
+
"""Simulated gap analysis pipeline that returns example results from the provided file."""
|
| 23 |
+
logger.info("🚀 Starting GAP analysis pipeline...")
|
| 24 |
+
|
| 25 |
+
# Initialize state
|
| 26 |
+
state = GapAnalysisState(original_doc=original_doc)
|
| 27 |
+
|
| 28 |
+
# Define the standard queries used in the GAP analysis
|
| 29 |
+
state.queries = [
|
| 30 |
+
"Does the current PD model align with the latest IFRS9 regulatory requirements?",
|
| 31 |
+
"Are the data sources used for PD calculation comprehensive and up-to-date?",
|
| 32 |
+
"Is there a robust process in place for the regular back-testing of PD models?",
|
| 33 |
+
"How is the segmentation of portfolios handled in the PD model, and is it appropriate?",
|
| 34 |
+
"Are the assumptions used in the PD model clearly documented and justified?",
|
| 35 |
+
"What is the process for incorporating forward-looking information into the PD model?",
|
| 36 |
+
"Is there a mechanism for ongoing monitoring and validation of the PD model's performance?",
|
| 37 |
+
"How are overrides and expert judgments applied in the PD model, and are they documented?",
|
| 38 |
+
"What methods are used to ensure appropriate granularity in the PD model?",
|
| 39 |
+
"How are model uncertainties and limitations addressed in the PD estimation?",
|
| 40 |
+
"Is the PD model integration with other IFRS9 components (LGD, EAD) well-designed?",
|
| 41 |
+
"How is the PD model governance structured, and does it ensure proper oversight?"
|
| 42 |
+
]
|
| 43 |
+
|
| 44 |
+
# Load example gap analysis results from the provided file
|
| 45 |
+
try:
|
| 46 |
+
logger.info("📄 Loading example GAP analysis results...")
|
| 47 |
+
example_file = Path('attached_assets/Pasted--Does-the-current-PD-model-align-with-the-latest-IFRS9-regulatory-requirements-Based-on-the-pr-1743447418785.txt')
|
| 48 |
+
|
| 49 |
+
if example_file.exists():
|
| 50 |
+
with open(example_file, 'r') as f:
|
| 51 |
+
example_text = f.read()
|
| 52 |
+
|
| 53 |
+
# Parse the example text into our structure
|
| 54 |
+
# Pattern matches each query-response pair in the example file
|
| 55 |
+
pattern = r"🔹 (.+?)\n➡️ (.+?)(?=\n\n🔹|\Z)"
|
| 56 |
+
matches = re.findall(pattern, example_text, re.DOTALL)
|
| 57 |
+
|
| 58 |
+
for query, analysis in matches:
|
| 59 |
+
query = query.strip()
|
| 60 |
+
analysis = analysis.strip()
|
| 61 |
+
state.gap_analyses[query] = analysis
|
| 62 |
+
|
| 63 |
+
logger.info(f"✅ Successfully parsed {len(state.gap_analyses)} GAP analysis results")
|
| 64 |
+
else:
|
| 65 |
+
logger.warning("⚠️ Example file not found, using placeholder data")
|
| 66 |
+
# If the file is not found, provide a placeholder message
|
| 67 |
+
for query in state.queries:
|
| 68 |
+
state.gap_analyses[query] = "Analysis would be performed on the uploaded document in a real implementation."
|
| 69 |
+
|
| 70 |
+
except Exception as e:
|
| 71 |
+
logger.error(f"❌ Error parsing example GAP analysis: {str(e)}")
|
| 72 |
+
# In case of any error, provide a generic message
|
| 73 |
+
for query in state.queries:
|
| 74 |
+
state.gap_analyses[query] = f"Error processing analysis for this query: {str(e)}"
|
| 75 |
+
|
| 76 |
+
logger.info("🎉 GAP analysis pipeline completed")
|
| 77 |
+
return state
|
attached_assets/ifrs9_analysis.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from uuid6 import uuid7
|
| 3 |
+
import logging
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import List, Dict
|
| 6 |
+
from pydantic import BaseModel, Field
|
| 7 |
+
|
| 8 |
+
from langchain.chat_models import ChatOpenAI
|
| 9 |
+
from langchain.prompts import PromptTemplate
|
| 10 |
+
from langchain.chains import LLMChain
|
| 11 |
+
from langchain.output_parsers import PydanticOutputParser
|
| 12 |
+
|
| 13 |
+
from langgraph.graph import StateGraph, END
|
| 14 |
+
|
| 15 |
+
# ========================
|
| 16 |
+
# Logging Setup
|
| 17 |
+
# ========================
|
| 18 |
+
logging.basicConfig(
|
| 19 |
+
level=logging.INFO,
|
| 20 |
+
format="%(asctime)s - %(levelname)s - %(message)s"
|
| 21 |
+
)
|
| 22 |
+
logger = logging.getLogger(__name__)
|
| 23 |
+
|
| 24 |
+
LLM_LOG_DIR = Path("logs/llm_logs")
|
| 25 |
+
LLM_LOG_DIR.mkdir(parents=True, exist_ok=True)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
#We use uuid7 to generate a unique identifier for each log file.
|
| 29 |
+
#They can be sorted by time and are "effectively" (not literally).
|
| 30 |
+
def log_llm_io(step: str, input_text: str, output_text: str):
|
| 31 |
+
filename = LLM_LOG_DIR / f"{step}_{uuid7().hex}.txt"
|
| 32 |
+
with open(filename, "w", encoding="utf-8") as f:
|
| 33 |
+
f.write("=== INPUT ===\n")
|
| 34 |
+
f.write(input_text.strip() + "\n\n")
|
| 35 |
+
f.write("=== OUTPUT ===\n")
|
| 36 |
+
f.write(output_text.strip())
|
| 37 |
+
logger.debug(f"📝 Logged LLM I/O to: {filename}")
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
# ========================
|
| 41 |
+
# State Definition
|
| 42 |
+
# ========================
|
| 43 |
+
class GapAnalysisState(BaseModel):
|
| 44 |
+
original_doc: str
|
| 45 |
+
queries: List[str] = []
|
| 46 |
+
gap_analyses: Dict[str, str] = {}
|
| 47 |
+
|
| 48 |
+
# ========================
|
| 49 |
+
# LLM Setup (GPT-4o)
|
| 50 |
+
# ========================
|
| 51 |
+
llm = ChatOpenAI(
|
| 52 |
+
model="gpt-4o",
|
| 53 |
+
temperature=0,
|
| 54 |
+
api_key=os.environ["OPENAI_API_KEY"]
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
# ========================
|
| 58 |
+
# Step 1: Query Generation
|
| 59 |
+
# ========================
|
| 60 |
+
class QueryListOutput(BaseModel):
|
| 61 |
+
queries: List[str] = Field(..., description="List of GAP analysis questions")
|
| 62 |
+
|
| 63 |
+
query_parser = PydanticOutputParser(pydantic_object=QueryListOutput)
|
| 64 |
+
|
| 65 |
+
def calculate_query_count(doc_length: int) -> int:
|
| 66 |
+
"""Calculate number of queries based on document length."""
|
| 67 |
+
token_estimate = doc_length / 4 # Rough estimate of tokens (4 chars per token)
|
| 68 |
+
if token_estimate <= 4000:
|
| 69 |
+
return max(2, int(token_estimate / 400) * 2) # 2 queries per 400 tokens
|
| 70 |
+
else:
|
| 71 |
+
base_queries = 20 # 2 queries/400 tokens for first 4000 tokens
|
| 72 |
+
additional_queries = int((token_estimate - 4000) / 400) # 1 query/400 tokens after
|
| 73 |
+
return min(30, base_queries + additional_queries) # Cap at 30 queries
|
| 74 |
+
|
| 75 |
+
query_prompt = PromptTemplate.from_template(
|
| 76 |
+
"You are an expert in IFRS9 PD validation. Based on the following document, extract {query_count} critical questions to perform a GAP analysis.\n\n{doc}\n\n{format_instructions}"
|
| 77 |
+
).partial(format_instructions=query_parser.get_format_instructions())
|
| 78 |
+
|
| 79 |
+
query_chain = LLMChain(llm=llm, prompt=query_prompt)
|
| 80 |
+
|
| 81 |
+
def generate_queries(state: GapAnalysisState) -> GapAnalysisState:
|
| 82 |
+
logger.info("🟦 Generating queries from original document...")
|
| 83 |
+
query_count = calculate_query_count(len(state.original_doc))
|
| 84 |
+
logger.info(f"📊 Generating {query_count} queries based on document length...")
|
| 85 |
+
llm_input = query_prompt.format(doc=state.original_doc, query_count=query_count)
|
| 86 |
+
response = query_chain.run(doc=state.original_doc, query_count=query_count)
|
| 87 |
+
log_llm_io("01_generate_queries", llm_input, response)
|
| 88 |
+
state.queries = query_parser.parse(response).queries
|
| 89 |
+
logger.info(f"✅ Generated {len(state.queries)} queries.")
|
| 90 |
+
return state
|
| 91 |
+
|
| 92 |
+
# ========================
|
| 93 |
+
# Step 2: GAP Analysis
|
| 94 |
+
# ========================
|
| 95 |
+
gap_prompt = PromptTemplate.from_template(
|
| 96 |
+
"You are performing a GAP analysis for this query:\n\n{query}\n\n"
|
| 97 |
+
"Based on the original validation document, determine whether the IFRS9 PD model follows best practices."
|
| 98 |
+
"If not, explain the gap. If insufficient data, say so.\n\n"
|
| 99 |
+
"Original Document:\n{original_doc}\n\n"
|
| 100 |
+
"GAP Analysis Paragraph:"
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
gap_chain = LLMChain(llm=llm, prompt=gap_prompt)
|
| 104 |
+
|
| 105 |
+
def generate_gap_analysis(state: GapAnalysisState) -> GapAnalysisState:
|
| 106 |
+
logger.info("🟦 Generating GAP analysis for each query...")
|
| 107 |
+
for query in state.queries:
|
| 108 |
+
llm_input = gap_prompt.format(
|
| 109 |
+
query=query,
|
| 110 |
+
original_doc=state.original_doc[:3000],
|
| 111 |
+
)
|
| 112 |
+
response = gap_chain.run(
|
| 113 |
+
query=query,
|
| 114 |
+
original_doc=state.original_doc[:3000],
|
| 115 |
+
)
|
| 116 |
+
log_llm_io(f"02_gap_analysis_{state.queries.index(query)+1}", llm_input, response)
|
| 117 |
+
state.gap_analyses[query] = response.strip()
|
| 118 |
+
logger.info("✅ GAP analysis generation complete.")
|
| 119 |
+
return state
|
| 120 |
+
|
| 121 |
+
# ========================
|
| 122 |
+
# LangGraph Pipeline
|
| 123 |
+
# ========================
|
| 124 |
+
def create_graph():
|
| 125 |
+
builder = StateGraph(GapAnalysisState)
|
| 126 |
+
|
| 127 |
+
builder.add_node("generate_queries", generate_queries)
|
| 128 |
+
builder.add_node("generate_gap", generate_gap_analysis)
|
| 129 |
+
|
| 130 |
+
builder.set_entry_point("generate_queries")
|
| 131 |
+
builder.add_edge("generate_queries", "generate_gap")
|
| 132 |
+
builder.add_edge("generate_gap", END)
|
| 133 |
+
|
| 134 |
+
return builder.compile()
|
| 135 |
+
|
| 136 |
+
graph = create_graph()
|
| 137 |
+
|
| 138 |
+
# ========================
|
| 139 |
+
# Entrypoint
|
| 140 |
+
# ========================
|
| 141 |
+
def run_gap_analysis_pipeline(original_doc: str):
|
| 142 |
+
logger.info("🚀 Starting GAP analysis pipeline...")
|
| 143 |
+
initial_state = GapAnalysisState(original_doc=original_doc)
|
| 144 |
+
final_state = graph.invoke(initial_state)
|
| 145 |
+
logger.info("🎉 GAP analysis pipeline completed successfully.")
|
| 146 |
+
return final_state
|
attached_assets/pdf_generator.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from fpdf import FPDF
|
| 3 |
+
import textwrap
|
| 4 |
+
import tempfile
|
| 5 |
+
import matplotlib.pyplot as plt
|
| 6 |
+
import numpy as np
|
| 7 |
+
import io
|
| 8 |
+
from typing import Dict, List, Optional
|
| 9 |
+
|
| 10 |
+
class GAPReportPDF(FPDF):
|
| 11 |
+
def __init__(self, title="IFRS9 GAP Analysis Report"):
|
| 12 |
+
super().__init__()
|
| 13 |
+
self.set_auto_page_break(auto=True, margin=15)
|
| 14 |
+
self.add_page()
|
| 15 |
+
self.set_font("helvetica", "B", 16)
|
| 16 |
+
self.cell(0, 10, title, ln=True, align="C")
|
| 17 |
+
self.ln(10)
|
| 18 |
+
|
| 19 |
+
def add_section_header(self, title, size=14):
|
| 20 |
+
"""Add a section header to the PDF."""
|
| 21 |
+
self.set_font("helvetica", "B", size)
|
| 22 |
+
self.cell(0, 10, title, ln=True)
|
| 23 |
+
self.ln(2)
|
| 24 |
+
|
| 25 |
+
def add_paragraph(self, text, size=11):
|
| 26 |
+
"""Add a paragraph to the PDF."""
|
| 27 |
+
self.set_font("helvetica", "", size)
|
| 28 |
+
wrapped_text = textwrap.fill(text, width=90)
|
| 29 |
+
self.multi_cell(0, 8, wrapped_text)
|
| 30 |
+
self.ln(5)
|
| 31 |
+
|
| 32 |
+
def add_compliance_gauge(self, compliance_percentage):
|
| 33 |
+
"""Add compliance gauge chart to the PDF."""
|
| 34 |
+
# Create matplotlib figure for gauge
|
| 35 |
+
fig, ax = plt.subplots(figsize=(6, 3), subplot_kw=dict(polar=True))
|
| 36 |
+
|
| 37 |
+
# Set the limits
|
| 38 |
+
ax.set_theta_offset(np.pi / 2)
|
| 39 |
+
ax.set_theta_direction(-1)
|
| 40 |
+
|
| 41 |
+
# Set the limit for the gauge
|
| 42 |
+
ax.set_thetamin(0)
|
| 43 |
+
ax.set_thetamax(180)
|
| 44 |
+
|
| 45 |
+
# Draw gauge background
|
| 46 |
+
ax.set_ylim(0, 10)
|
| 47 |
+
ax.set_yticks([])
|
| 48 |
+
ax.set_xticks(np.linspace(0, np.pi, 5))
|
| 49 |
+
ax.set_xticklabels(['0%', '25%', '50%', '75%', '100%'])
|
| 50 |
+
|
| 51 |
+
# Add colored ranges
|
| 52 |
+
theta = np.linspace(0, np.pi, 100)
|
| 53 |
+
ax.fill_between(theta, 5, 9, color='red', alpha=0.3, where=(theta <= np.pi * 0.5))
|
| 54 |
+
ax.fill_between(theta, 5, 9, color='orange', alpha=0.3, where=((theta > np.pi * 0.5) & (theta <= np.pi * 0.75)))
|
| 55 |
+
ax.fill_between(theta, 5, 9, color='yellow', alpha=0.3, where=((theta > np.pi * 0.75) & (theta <= np.pi * 0.9)))
|
| 56 |
+
ax.fill_between(theta, 5, 9, color='green', alpha=0.3, where=(theta > np.pi * 0.9))
|
| 57 |
+
|
| 58 |
+
# Add needle
|
| 59 |
+
needle_angle = np.pi * compliance_percentage / 100
|
| 60 |
+
ax.plot([0, needle_angle], [0, 7], color='black', linewidth=2)
|
| 61 |
+
ax.plot([0], [0], marker='o', markersize=10, color='black')
|
| 62 |
+
|
| 63 |
+
# Add compliance percentage text
|
| 64 |
+
ax.text(np.pi/2, 3, f"{compliance_percentage:.1f}%", ha='center', va='center', fontsize=16, fontweight='bold')
|
| 65 |
+
|
| 66 |
+
# Add title
|
| 67 |
+
plt.title("IFRS9 Compliance Score", pad=20)
|
| 68 |
+
|
| 69 |
+
# Save figure to buffer
|
| 70 |
+
buf = io.BytesIO()
|
| 71 |
+
plt.savefig(buf, format='png', dpi=100, bbox_inches='tight')
|
| 72 |
+
plt.close(fig)
|
| 73 |
+
|
| 74 |
+
# Add image to PDF
|
| 75 |
+
buf.seek(0)
|
| 76 |
+
self.image(buf, x=50, y=None, w=100)
|
| 77 |
+
self.ln(5)
|
| 78 |
+
|
| 79 |
+
def add_key_areas_chart(self, key_areas):
|
| 80 |
+
"""Add key areas chart to the PDF."""
|
| 81 |
+
# Sort areas by score
|
| 82 |
+
sorted_areas = sorted(key_areas, key=lambda x: x['score'])
|
| 83 |
+
|
| 84 |
+
# Create horizontal bar chart
|
| 85 |
+
fig, ax = plt.subplots(figsize=(7, 3 + 0.5 * len(sorted_areas)))
|
| 86 |
+
|
| 87 |
+
area_names = [area['name'] for area in sorted_areas]
|
| 88 |
+
scores = [area['score'] for area in sorted_areas]
|
| 89 |
+
|
| 90 |
+
# Generate colors based on scores
|
| 91 |
+
colors = ['red' if score < 50 else 'orange' if score < 75 else 'yellow' if score < 90 else 'green'
|
| 92 |
+
for score in scores]
|
| 93 |
+
|
| 94 |
+
y_pos = np.arange(len(area_names))
|
| 95 |
+
ax.barh(y_pos, scores, color=colors)
|
| 96 |
+
ax.set_yticks(y_pos)
|
| 97 |
+
ax.set_yticklabels(area_names)
|
| 98 |
+
ax.set_xlabel('Compliance Score (%)')
|
| 99 |
+
ax.set_xlim(0, 100)
|
| 100 |
+
ax.set_title('Compliance by Key Areas')
|
| 101 |
+
|
| 102 |
+
# Add score labels
|
| 103 |
+
for i, score in enumerate(scores):
|
| 104 |
+
ax.text(score + 1, i, f"{score:.1f}%", va='center')
|
| 105 |
+
|
| 106 |
+
plt.tight_layout()
|
| 107 |
+
|
| 108 |
+
# Save figure to buffer
|
| 109 |
+
buf = io.BytesIO()
|
| 110 |
+
plt.savefig(buf, format='png', dpi=100, bbox_inches='tight')
|
| 111 |
+
plt.close(fig)
|
| 112 |
+
|
| 113 |
+
# Add image to PDF
|
| 114 |
+
buf.seek(0)
|
| 115 |
+
self.image(buf, x=15, y=None, w=180)
|
| 116 |
+
self.ln(5)
|
| 117 |
+
|
| 118 |
+
def add_key_areas_details(self, key_areas):
|
| 119 |
+
"""Add key areas details to the PDF."""
|
| 120 |
+
for area in key_areas:
|
| 121 |
+
self.set_font("helvetica", "B", 12)
|
| 122 |
+
self.cell(0, 10, f"{area['name']} ({area['score']:.1f}%)", ln=True)
|
| 123 |
+
self.set_font("helvetica", "", 11)
|
| 124 |
+
wrapped_desc = textwrap.fill(area['description'], width=90)
|
| 125 |
+
self.multi_cell(0, 8, wrapped_desc)
|
| 126 |
+
self.ln(5)
|
| 127 |
+
|
| 128 |
+
def add_query_section(self, query: str, gap_analysis: str):
|
| 129 |
+
"""Add a query and its analysis to the PDF."""
|
| 130 |
+
# Query header
|
| 131 |
+
self.set_font("helvetica", "B", 12)
|
| 132 |
+
wrapped_query = textwrap.fill(query, width=90)
|
| 133 |
+
self.multi_cell(0, 10, wrapped_query)
|
| 134 |
+
self.ln(5)
|
| 135 |
+
|
| 136 |
+
# GAP Analysis content
|
| 137 |
+
self.set_font("helvetica", "", 11)
|
| 138 |
+
wrapped_analysis = textwrap.fill(gap_analysis, width=90)
|
| 139 |
+
self.multi_cell(0, 8, wrapped_analysis)
|
| 140 |
+
self.ln(10)
|
| 141 |
+
|
| 142 |
+
def generate_consolidated_pdf(consolidated_result, queries: list = None, gap_analyses: dict = None, output_path: str = "gap_analysis_report.pdf"):
|
| 143 |
+
"""Generate a consolidated PDF report with visualizations."""
|
| 144 |
+
try:
|
| 145 |
+
pdf = GAPReportPDF("IFRS9 Consolidated GAP Analysis Report")
|
| 146 |
+
|
| 147 |
+
# Add compliance gauge
|
| 148 |
+
pdf.add_compliance_gauge(consolidated_result.compliance_percentage)
|
| 149 |
+
|
| 150 |
+
# Add executive summary
|
| 151 |
+
pdf.add_section_header("Executive Summary")
|
| 152 |
+
pdf.add_paragraph(consolidated_result.executive_summary)
|
| 153 |
+
|
| 154 |
+
# Add key areas visualization
|
| 155 |
+
pdf.add_section_header("Key Areas for Improvement")
|
| 156 |
+
pdf.add_key_areas_chart(consolidated_result.key_areas)
|
| 157 |
+
|
| 158 |
+
# Add key areas details
|
| 159 |
+
pdf.add_section_header("Key Areas Details")
|
| 160 |
+
pdf.add_key_areas_details(consolidated_result.key_areas)
|
| 161 |
+
|
| 162 |
+
# Add detailed analysis
|
| 163 |
+
pdf.add_section_header("Detailed Analysis")
|
| 164 |
+
pdf.add_paragraph(consolidated_result.detailed_analysis)
|
| 165 |
+
|
| 166 |
+
# Add individual query analyses if provided
|
| 167 |
+
if queries and gap_analyses:
|
| 168 |
+
pdf.add_page()
|
| 169 |
+
pdf.add_section_header("Individual Query Analyses")
|
| 170 |
+
for query in queries:
|
| 171 |
+
if query in gap_analyses:
|
| 172 |
+
pdf.add_query_section(query, gap_analyses[query])
|
| 173 |
+
|
| 174 |
+
# Use a temporary file path
|
| 175 |
+
temp_dir = tempfile.gettempdir()
|
| 176 |
+
temp_path = os.path.join(temp_dir, output_path)
|
| 177 |
+
|
| 178 |
+
pdf.output(temp_path)
|
| 179 |
+
return temp_path
|
| 180 |
+
except Exception as e:
|
| 181 |
+
print(f"Error generating consolidated PDF: {str(e)}")
|
| 182 |
+
raise
|
| 183 |
+
|
| 184 |
+
def generate_gap_analysis_pdf(queries: list, gap_analyses: dict, output_path: str = "gap_analysis_report.pdf"):
|
| 185 |
+
"""Generate a PDF with individual query analyses."""
|
| 186 |
+
try:
|
| 187 |
+
pdf = GAPReportPDF()
|
| 188 |
+
|
| 189 |
+
for query in queries:
|
| 190 |
+
if query in gap_analyses:
|
| 191 |
+
pdf.add_query_section(query, gap_analyses[query])
|
| 192 |
+
|
| 193 |
+
# Use a temporary file path
|
| 194 |
+
temp_dir = tempfile.gettempdir()
|
| 195 |
+
temp_path = os.path.join(temp_dir, output_path)
|
| 196 |
+
|
| 197 |
+
pdf.output(temp_path)
|
| 198 |
+
return temp_path
|
| 199 |
+
except Exception as e:
|
| 200 |
+
print(f"Error generating PDF: {str(e)}")
|
| 201 |
+
raise
|