validmind 2.5.8__py3-none-any.whl → 2.5.18__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. validmind/__version__.py +1 -1
  2. validmind/ai/test_descriptions.py +80 -119
  3. validmind/ai/test_result_description/config.yaml +29 -0
  4. validmind/ai/test_result_description/context.py +73 -0
  5. validmind/ai/test_result_description/image_processing.py +124 -0
  6. validmind/ai/test_result_description/system.jinja +39 -0
  7. validmind/ai/test_result_description/user.jinja +25 -0
  8. validmind/api_client.py +89 -43
  9. validmind/client.py +2 -2
  10. validmind/client_config.py +11 -14
  11. validmind/datasets/credit_risk/__init__.py +1 -0
  12. validmind/datasets/credit_risk/datasets/lending_club_biased.csv.gz +0 -0
  13. validmind/datasets/credit_risk/lending_club_bias.py +142 -0
  14. validmind/datasets/regression/fred_timeseries.py +67 -138
  15. validmind/template.py +1 -0
  16. validmind/test_suites/__init__.py +0 -2
  17. validmind/test_suites/statsmodels_timeseries.py +1 -1
  18. validmind/test_suites/summarization.py +0 -1
  19. validmind/test_suites/time_series.py +0 -43
  20. validmind/tests/__types__.py +14 -15
  21. validmind/tests/data_validation/ACFandPACFPlot.py +15 -13
  22. validmind/tests/data_validation/ADF.py +31 -24
  23. validmind/tests/data_validation/AutoAR.py +9 -9
  24. validmind/tests/data_validation/AutoMA.py +23 -16
  25. validmind/tests/data_validation/AutoSeasonality.py +18 -16
  26. validmind/tests/data_validation/AutoStationarity.py +21 -16
  27. validmind/tests/data_validation/BivariateScatterPlots.py +67 -96
  28. validmind/tests/{model_validation/statsmodels → data_validation}/BoxPierce.py +34 -34
  29. validmind/tests/data_validation/ChiSquaredFeaturesTable.py +85 -124
  30. validmind/tests/data_validation/ClassImbalance.py +15 -12
  31. validmind/tests/data_validation/DFGLSArch.py +19 -13
  32. validmind/tests/data_validation/DatasetDescription.py +17 -11
  33. validmind/tests/data_validation/DatasetSplit.py +7 -5
  34. validmind/tests/data_validation/DescriptiveStatistics.py +28 -21
  35. validmind/tests/data_validation/Duplicates.py +33 -25
  36. validmind/tests/data_validation/EngleGrangerCoint.py +35 -33
  37. validmind/tests/data_validation/FeatureTargetCorrelationPlot.py +59 -71
  38. validmind/tests/data_validation/HighCardinality.py +19 -12
  39. validmind/tests/data_validation/HighPearsonCorrelation.py +27 -22
  40. validmind/tests/data_validation/IQROutliersBarPlot.py +13 -10
  41. validmind/tests/data_validation/IQROutliersTable.py +40 -36
  42. validmind/tests/data_validation/IsolationForestOutliers.py +21 -14
  43. validmind/tests/data_validation/JarqueBera.py +70 -0
  44. validmind/tests/data_validation/KPSS.py +34 -29
  45. validmind/tests/data_validation/LJungBox.py +66 -0
  46. validmind/tests/data_validation/LaggedCorrelationHeatmap.py +22 -15
  47. validmind/tests/data_validation/MissingValues.py +32 -27
  48. validmind/tests/data_validation/MissingValuesBarPlot.py +25 -21
  49. validmind/tests/data_validation/PearsonCorrelationMatrix.py +71 -84
  50. validmind/tests/data_validation/PhillipsPerronArch.py +37 -30
  51. validmind/tests/data_validation/ProtectedClassesCombination.py +197 -0
  52. validmind/tests/data_validation/ProtectedClassesDescription.py +130 -0
  53. validmind/tests/data_validation/ProtectedClassesDisparity.py +133 -0
  54. validmind/tests/data_validation/ProtectedClassesThresholdOptimizer.py +172 -0
  55. validmind/tests/data_validation/RollingStatsPlot.py +31 -23
  56. validmind/tests/data_validation/RunsTest.py +72 -0
  57. validmind/tests/data_validation/ScatterPlot.py +63 -78
  58. validmind/tests/data_validation/SeasonalDecompose.py +38 -34
  59. validmind/tests/{model_validation/statsmodels → data_validation}/ShapiroWilk.py +35 -30
  60. validmind/tests/data_validation/Skewness.py +35 -37
  61. validmind/tests/data_validation/SpreadPlot.py +35 -35
  62. validmind/tests/data_validation/TabularCategoricalBarPlots.py +23 -17
  63. validmind/tests/data_validation/TabularDateTimeHistograms.py +21 -13
  64. validmind/tests/data_validation/TabularDescriptionTables.py +51 -16
  65. validmind/tests/data_validation/TabularNumericalHistograms.py +25 -22
  66. validmind/tests/data_validation/TargetRateBarPlots.py +21 -14
  67. validmind/tests/data_validation/TimeSeriesDescription.py +25 -18
  68. validmind/tests/data_validation/TimeSeriesDescriptiveStatistics.py +23 -17
  69. validmind/tests/data_validation/TimeSeriesFrequency.py +24 -17
  70. validmind/tests/data_validation/TimeSeriesHistogram.py +33 -32
  71. validmind/tests/data_validation/TimeSeriesLinePlot.py +17 -10
  72. validmind/tests/data_validation/TimeSeriesMissingValues.py +15 -10
  73. validmind/tests/data_validation/TimeSeriesOutliers.py +37 -33
  74. validmind/tests/data_validation/TooManyZeroValues.py +16 -11
  75. validmind/tests/data_validation/UniqueRows.py +11 -6
  76. validmind/tests/data_validation/WOEBinPlots.py +23 -16
  77. validmind/tests/data_validation/WOEBinTable.py +35 -30
  78. validmind/tests/data_validation/ZivotAndrewsArch.py +34 -28
  79. validmind/tests/data_validation/nlp/CommonWords.py +21 -14
  80. validmind/tests/data_validation/nlp/Hashtags.py +42 -40
  81. validmind/tests/data_validation/nlp/LanguageDetection.py +33 -14
  82. validmind/tests/data_validation/nlp/Mentions.py +21 -15
  83. validmind/tests/data_validation/nlp/PolarityAndSubjectivity.py +32 -9
  84. validmind/tests/data_validation/nlp/Punctuations.py +24 -20
  85. validmind/tests/data_validation/nlp/Sentiment.py +27 -8
  86. validmind/tests/data_validation/nlp/StopWords.py +26 -19
  87. validmind/tests/data_validation/nlp/TextDescription.py +39 -36
  88. validmind/tests/data_validation/nlp/Toxicity.py +32 -9
  89. validmind/tests/decorator.py +81 -42
  90. validmind/tests/model_validation/BertScore.py +36 -27
  91. validmind/tests/model_validation/BleuScore.py +25 -19
  92. validmind/tests/model_validation/ClusterSizeDistribution.py +38 -34
  93. validmind/tests/model_validation/ContextualRecall.py +38 -13
  94. validmind/tests/model_validation/FeaturesAUC.py +32 -13
  95. validmind/tests/model_validation/MeteorScore.py +46 -33
  96. validmind/tests/model_validation/ModelMetadata.py +32 -64
  97. validmind/tests/model_validation/ModelPredictionResiduals.py +75 -73
  98. validmind/tests/model_validation/RegardScore.py +30 -14
  99. validmind/tests/model_validation/RegressionResidualsPlot.py +10 -5
  100. validmind/tests/model_validation/RougeScore.py +36 -30
  101. validmind/tests/model_validation/TimeSeriesPredictionWithCI.py +30 -14
  102. validmind/tests/model_validation/TimeSeriesPredictionsPlot.py +27 -30
  103. validmind/tests/model_validation/TimeSeriesR2SquareBySegments.py +68 -63
  104. validmind/tests/model_validation/TokenDisparity.py +31 -23
  105. validmind/tests/model_validation/ToxicityScore.py +26 -17
  106. validmind/tests/model_validation/embeddings/ClusterDistribution.py +24 -20
  107. validmind/tests/model_validation/embeddings/CosineSimilarityComparison.py +30 -27
  108. validmind/tests/model_validation/embeddings/CosineSimilarityDistribution.py +7 -5
  109. validmind/tests/model_validation/embeddings/CosineSimilarityHeatmap.py +32 -23
  110. validmind/tests/model_validation/embeddings/DescriptiveAnalytics.py +7 -5
  111. validmind/tests/model_validation/embeddings/EmbeddingsVisualization2D.py +15 -11
  112. validmind/tests/model_validation/embeddings/EuclideanDistanceComparison.py +29 -29
  113. validmind/tests/model_validation/embeddings/EuclideanDistanceHeatmap.py +34 -25
  114. validmind/tests/model_validation/embeddings/PCAComponentsPairwisePlots.py +38 -26
  115. validmind/tests/model_validation/embeddings/StabilityAnalysis.py +40 -1
  116. validmind/tests/model_validation/embeddings/StabilityAnalysisKeyword.py +18 -17
  117. validmind/tests/model_validation/embeddings/StabilityAnalysisRandomNoise.py +40 -45
  118. validmind/tests/model_validation/embeddings/StabilityAnalysisSynonyms.py +17 -19
  119. validmind/tests/model_validation/embeddings/StabilityAnalysisTranslation.py +29 -25
  120. validmind/tests/model_validation/embeddings/TSNEComponentsPairwisePlots.py +38 -28
  121. validmind/tests/model_validation/ragas/AnswerCorrectness.py +5 -4
  122. validmind/tests/model_validation/ragas/AnswerRelevance.py +5 -4
  123. validmind/tests/model_validation/ragas/AnswerSimilarity.py +5 -4
  124. validmind/tests/model_validation/ragas/AspectCritique.py +12 -6
  125. validmind/tests/model_validation/ragas/ContextEntityRecall.py +9 -8
  126. validmind/tests/model_validation/ragas/ContextPrecision.py +5 -4
  127. validmind/tests/model_validation/ragas/ContextRecall.py +5 -4
  128. validmind/tests/model_validation/ragas/ContextUtilization.py +155 -0
  129. validmind/tests/model_validation/ragas/Faithfulness.py +5 -4
  130. validmind/tests/model_validation/ragas/NoiseSensitivity.py +152 -0
  131. validmind/tests/model_validation/ragas/utils.py +6 -0
  132. validmind/tests/model_validation/sklearn/AdjustedMutualInformation.py +19 -12
  133. validmind/tests/model_validation/sklearn/AdjustedRandIndex.py +22 -17
  134. validmind/tests/model_validation/sklearn/ClassifierPerformance.py +27 -25
  135. validmind/tests/model_validation/sklearn/ClusterCosineSimilarity.py +7 -5
  136. validmind/tests/model_validation/sklearn/ClusterPerformance.py +40 -78
  137. validmind/tests/model_validation/sklearn/ClusterPerformanceMetrics.py +15 -17
  138. validmind/tests/model_validation/sklearn/CompletenessScore.py +17 -11
  139. validmind/tests/model_validation/sklearn/ConfusionMatrix.py +22 -15
  140. validmind/tests/model_validation/sklearn/FeatureImportance.py +95 -0
  141. validmind/tests/model_validation/sklearn/FowlkesMallowsScore.py +7 -7
  142. validmind/tests/model_validation/sklearn/HomogeneityScore.py +19 -12
  143. validmind/tests/model_validation/sklearn/HyperParametersTuning.py +35 -30
  144. validmind/tests/model_validation/sklearn/KMeansClustersOptimization.py +10 -5
  145. validmind/tests/model_validation/sklearn/MinimumAccuracy.py +32 -32
  146. validmind/tests/model_validation/sklearn/MinimumF1Score.py +23 -23
  147. validmind/tests/model_validation/sklearn/MinimumROCAUCScore.py +15 -10
  148. validmind/tests/model_validation/sklearn/ModelsPerformanceComparison.py +26 -19
  149. validmind/tests/model_validation/sklearn/OverfitDiagnosis.py +38 -18
  150. validmind/tests/model_validation/sklearn/PermutationFeatureImportance.py +32 -26
  151. validmind/tests/model_validation/sklearn/PopulationStabilityIndex.py +8 -6
  152. validmind/tests/model_validation/sklearn/PrecisionRecallCurve.py +24 -17
  153. validmind/tests/model_validation/sklearn/ROCCurve.py +12 -7
  154. validmind/tests/model_validation/sklearn/RegressionErrors.py +74 -130
  155. validmind/tests/model_validation/sklearn/RegressionErrorsComparison.py +27 -12
  156. validmind/tests/model_validation/sklearn/{RegressionModelsPerformanceComparison.py → RegressionPerformance.py} +18 -20
  157. validmind/tests/model_validation/sklearn/RegressionR2Square.py +55 -94
  158. validmind/tests/model_validation/sklearn/RegressionR2SquareComparison.py +32 -13
  159. validmind/tests/model_validation/sklearn/RobustnessDiagnosis.py +36 -32
  160. validmind/tests/model_validation/sklearn/SHAPGlobalImportance.py +66 -5
  161. validmind/tests/model_validation/sklearn/SilhouettePlot.py +27 -19
  162. validmind/tests/model_validation/sklearn/TrainingTestDegradation.py +25 -18
  163. validmind/tests/model_validation/sklearn/VMeasure.py +14 -13
  164. validmind/tests/model_validation/sklearn/WeakspotsDiagnosis.py +7 -5
  165. validmind/tests/model_validation/statsmodels/AutoARIMA.py +24 -18
  166. validmind/tests/model_validation/statsmodels/CumulativePredictionProbabilities.py +73 -104
  167. validmind/tests/model_validation/statsmodels/DurbinWatsonTest.py +59 -32
  168. validmind/tests/model_validation/statsmodels/GINITable.py +44 -77
  169. validmind/tests/model_validation/statsmodels/KolmogorovSmirnov.py +33 -34
  170. validmind/tests/model_validation/statsmodels/Lilliefors.py +27 -24
  171. validmind/tests/model_validation/statsmodels/PredictionProbabilitiesHistogram.py +86 -119
  172. validmind/tests/model_validation/statsmodels/RegressionCoeffs.py +100 -0
  173. validmind/tests/model_validation/statsmodels/RegressionFeatureSignificance.py +14 -9
  174. validmind/tests/model_validation/statsmodels/RegressionModelForecastPlot.py +17 -13
  175. validmind/tests/model_validation/statsmodels/RegressionModelForecastPlotLevels.py +46 -43
  176. validmind/tests/model_validation/statsmodels/RegressionModelSensitivityPlot.py +38 -36
  177. validmind/tests/model_validation/statsmodels/RegressionModelSummary.py +30 -28
  178. validmind/tests/model_validation/statsmodels/RegressionPermutationFeatureImportance.py +18 -11
  179. validmind/tests/model_validation/statsmodels/ScorecardHistogram.py +75 -107
  180. validmind/tests/ongoing_monitoring/FeatureDrift.py +10 -6
  181. validmind/tests/ongoing_monitoring/PredictionAcrossEachFeature.py +31 -25
  182. validmind/tests/ongoing_monitoring/PredictionCorrelation.py +29 -21
  183. validmind/tests/ongoing_monitoring/TargetPredictionDistributionPlot.py +31 -23
  184. validmind/tests/prompt_validation/Bias.py +14 -11
  185. validmind/tests/prompt_validation/Clarity.py +16 -14
  186. validmind/tests/prompt_validation/Conciseness.py +7 -5
  187. validmind/tests/prompt_validation/Delimitation.py +23 -22
  188. validmind/tests/prompt_validation/NegativeInstruction.py +7 -5
  189. validmind/tests/prompt_validation/Robustness.py +12 -10
  190. validmind/tests/prompt_validation/Specificity.py +13 -11
  191. validmind/tests/prompt_validation/ai_powered_test.py +6 -0
  192. validmind/tests/run.py +68 -23
  193. validmind/unit_metrics/__init__.py +81 -144
  194. validmind/unit_metrics/classification/{sklearn/Accuracy.py → Accuracy.py} +1 -1
  195. validmind/unit_metrics/classification/{sklearn/F1.py → F1.py} +1 -1
  196. validmind/unit_metrics/classification/{sklearn/Precision.py → Precision.py} +1 -1
  197. validmind/unit_metrics/classification/{sklearn/ROC_AUC.py → ROC_AUC.py} +1 -2
  198. validmind/unit_metrics/classification/{sklearn/Recall.py → Recall.py} +1 -1
  199. validmind/unit_metrics/regression/{sklearn/AdjustedRSquaredScore.py → AdjustedRSquaredScore.py} +1 -1
  200. validmind/unit_metrics/regression/GiniCoefficient.py +1 -1
  201. validmind/unit_metrics/regression/HuberLoss.py +1 -1
  202. validmind/unit_metrics/regression/KolmogorovSmirnovStatistic.py +1 -1
  203. validmind/unit_metrics/regression/{sklearn/MeanAbsoluteError.py → MeanAbsoluteError.py} +1 -1
  204. validmind/unit_metrics/regression/MeanAbsolutePercentageError.py +1 -1
  205. validmind/unit_metrics/regression/MeanBiasDeviation.py +1 -1
  206. validmind/unit_metrics/regression/{sklearn/MeanSquaredError.py → MeanSquaredError.py} +1 -1
  207. validmind/unit_metrics/regression/QuantileLoss.py +1 -1
  208. validmind/unit_metrics/regression/{sklearn/RSquaredScore.py → RSquaredScore.py} +1 -1
  209. validmind/unit_metrics/regression/{sklearn/RootMeanSquaredError.py → RootMeanSquaredError.py} +1 -1
  210. validmind/utils.py +4 -0
  211. validmind/vm_models/dataset/dataset.py +2 -0
  212. validmind/vm_models/figure.py +5 -0
  213. validmind/vm_models/test/metric.py +1 -0
  214. validmind/vm_models/test/result_wrapper.py +143 -158
  215. validmind/vm_models/test/threshold_test.py +1 -0
  216. {validmind-2.5.8.dist-info → validmind-2.5.18.dist-info}/METADATA +4 -3
  217. validmind-2.5.18.dist-info/RECORD +324 -0
  218. validmind/tests/data_validation/ANOVAOneWayTable.py +0 -138
  219. validmind/tests/data_validation/BivariateFeaturesBarPlots.py +0 -142
  220. validmind/tests/data_validation/BivariateHistograms.py +0 -117
  221. validmind/tests/data_validation/HeatmapFeatureCorrelations.py +0 -124
  222. validmind/tests/data_validation/MissingValuesRisk.py +0 -88
  223. validmind/tests/model_validation/ModelMetadataComparison.py +0 -59
  224. validmind/tests/model_validation/sklearn/FeatureImportanceComparison.py +0 -83
  225. validmind/tests/model_validation/statsmodels/JarqueBera.py +0 -73
  226. validmind/tests/model_validation/statsmodels/LJungBox.py +0 -66
  227. validmind/tests/model_validation/statsmodels/RegressionCoeffsPlot.py +0 -135
  228. validmind/tests/model_validation/statsmodels/RegressionModelsCoeffs.py +0 -103
  229. validmind/tests/model_validation/statsmodels/RunsTest.py +0 -71
  230. validmind-2.5.8.dist-info/RECORD +0 -318
  231. {validmind-2.5.8.dist-info → validmind-2.5.18.dist-info}/LICENSE +0 -0
  232. {validmind-2.5.8.dist-info → validmind-2.5.18.dist-info}/WHEEL +0 -0
  233. {validmind-2.5.8.dist-info → validmind-2.5.18.dist-info}/entry_points.txt +0 -0
validmind/__version__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "2.5.8"
1
+ __version__ = "2.5.18"
@@ -3,13 +3,19 @@
3
3
  # SPDX-License-Identifier: AGPL-3.0 AND ValidMind Commercial
4
4
 
5
5
  import os
6
+ import re
6
7
  from concurrent.futures import ThreadPoolExecutor
8
+ from typing import Union
9
+
10
+ from jinja2 import Template
7
11
 
8
12
  from validmind.utils import md_to_html
9
13
 
14
+ from ..client_config import client_config
10
15
  from ..logging import get_logger
11
16
 
12
17
  __executor = ThreadPoolExecutor()
18
+ __prompt = None
13
19
 
14
20
  logger = get_logger(__name__)
15
21
 
@@ -17,71 +23,48 @@ logger = get_logger(__name__)
17
23
  AI_REVISION_NAME = "Generated by ValidMind AI"
18
24
  DEFAULT_REVISION_NAME = "Default Description"
19
25
 
20
- SYSTEM_PROMPT = """ # noqa
21
- You are an expert data scientist and MRM specialist.
22
- You are tasked with analyzing the results of a quantitative test run on some model or dataset.
23
- Your goal is to create a test description that will act as part of the model documentation.
24
- You will provide both the developer and other consumers of the documentation with a clear and concise "interpretation" of the results they will see.
25
- The overarching theme to maintain is MRM documentation.
26
-
27
- Examine the provided statistical test results and compose a description of the results.
28
- The results are either in the form of serialized tables or images of plots.
29
- Compose a description and interpretation of the result to accompany it in MRM documentation.
30
- It will be read by other data scientists and developers and by validators and stakeholders.
31
-
32
- Use valid Markdown syntax to format the response.
33
- Avoid long sentences and complex vocabulary.
34
- Avoid overly verbose explanations - the goal is to explain to a user what they are seeing in the results.
35
- Structure the response clearly and logically.
36
- Respond only with your analysis and insights, not the verbatim test results.
37
- Respond only with the markdown content, no explanation or context for your response is necessary.
38
- Use the Test ID that is provided to form the Test Name e.g. "ClassImbalance" -> "Class Imbalance".
39
-
40
- Explain the test, its purpose, its mechanism/formula etc and why it is useful.
41
- If relevant, provide a very brief description of the way this test is used in model/dataset evaluation and how it is interpreted.
42
- Highlight the key insights from the test results. The key insights should be concise and easily understood.
43
- An insight should only be included if it is something not entirely obvious from the test results.
44
- End the response with any closing remarks, summary or additional useful information.
45
26
 
46
- Use the following format for the response (feel free to stray from it if necessary - this is a suggested starting point):
27
+ def _load_prompt():
28
+ global __prompt
47
29
 
48
- <ResponseFormat>
49
- **<Test Name>** calculates the xyz <continue to explain what it does in detail>...
30
+ if not __prompt:
31
+ folder_path = os.path.join(os.path.dirname(__file__), "test_result_description")
32
+ with open(os.path.join(folder_path, "system.jinja"), "r") as f:
33
+ system_prompt = f.read()
34
+ with open(os.path.join(folder_path, "user.jinja"), "r") as f:
35
+ user_prompt = f.read()
50
36
 
51
- This test is useful for <explain why and for what this test is useful>...
37
+ __prompt = (Template(system_prompt), Template(user_prompt))
52
38
 
53
- **Key Insights:**
39
+ return __prompt
54
40
 
55
- The following key insights can be identified in the test results:
56
41
 
57
- - **<key insight 1 - title>**: <concise explanation of key insight 1>
58
- - ...<continue with any other key insights using the same format>
59
- </ResponseFormat>
60
- """.strip()
42
+ def prompt_to_message(role, prompt):
43
+ if "[[IMAGE:" not in prompt:
44
+ return {"role": role, "content": prompt}
61
45
 
46
+ content = []
62
47
 
63
- USER_PROMPT = """
64
- Test ID: `{test_name}`
48
+ # Regex pattern to find [[IMAGE:<b64-data>]] markers
49
+ pattern = re.compile(r"\[\[IMAGE:(.*?)\]\]", re.DOTALL)
65
50
 
66
- <Test Docstring>
67
- {test_description}
68
- </Test Docstring>
51
+ last_index = 0
52
+ for match in pattern.finditer(prompt):
53
+ # Text before the image marker
54
+ start, end = match.span()
55
+ if start > last_index:
56
+ content.append({"type": "text", "text": prompt[last_index:start]})
69
57
 
70
- <Test Results Table(s)>
71
- {test_summary}
72
- </Test Results Table(s)>
73
- """.strip()
58
+ # Image
59
+ content.append({"type": "image_url", "image_url": {"url": match.group(1)}})
74
60
 
61
+ last_index = end
75
62
 
76
- USER_PROMPT_FIGURES = """
77
- Test ID: `{test_name}`
63
+ # Text after the last image
64
+ if last_index < len(prompt):
65
+ content.append({"type": "text", "text": prompt[last_index:]})
78
66
 
79
- <Test Docstring>
80
- {test_description}
81
- </Test Docstring>
82
-
83
- The attached plots show the results of the test.
84
- """.strip()
67
+ return {"role": role, "content": content}
85
68
 
86
69
 
87
70
  class DescriptionFuture:
@@ -110,11 +93,14 @@ def generate_description(
110
93
  test_id: str,
111
94
  test_description: str,
112
95
  test_summary: str,
96
+ metric: Union[float, int] = None,
113
97
  figures: list = None,
114
98
  ):
115
99
  """Generate the description for the test results"""
116
- if not test_summary and not figures:
117
- raise ValueError("No summary or figures provided - cannot generate description")
100
+ if not test_summary and not figures and not metric:
101
+ raise ValueError(
102
+ "No summary, unit metric or figures provided - cannot generate description"
103
+ )
118
104
 
119
105
  # TODO: fix circular import
120
106
  from validmind.ai.utils import get_client_and_model
@@ -130,79 +116,50 @@ def generate_description(
130
116
  else test_description
131
117
  )
132
118
 
133
- if test_summary:
134
- logger.debug(
135
- f"Generating description for test {test_name} with stringified summary"
136
- )
137
- return (
138
- client.chat.completions.create(
139
- model=model,
140
- temperature=0,
141
- seed=42,
142
- messages=[
143
- {"role": "system", "content": SYSTEM_PROMPT},
144
- {
145
- "role": "user",
146
- "content": USER_PROMPT.format(
147
- test_name=test_name,
148
- test_description=test_description,
149
- test_summary=test_summary,
150
- ),
151
- },
152
- ],
153
- )
154
- .choices[0]
155
- .message.content.strip()
156
- )
119
+ if metric:
120
+ metric_summary = f"**Metric Value**: {metric}"
121
+ if test_summary:
122
+ test_summary = metric_summary + "\n" + test_summary
123
+ else:
124
+ test_summary = metric_summary
157
125
 
158
- logger.debug(
159
- f"Generating description for test {test_name} with {len(figures)} figures"
160
- )
161
- return (
162
- client.chat.completions.create(
163
- model=model,
164
- temperature=0,
165
- seed=42,
166
- messages=[
167
- {"role": "system", "content": SYSTEM_PROMPT},
168
- {
169
- "role": "user",
170
- "content": [
171
- {
172
- "type": "text",
173
- "text": USER_PROMPT_FIGURES.format(
174
- test_name=test_name,
175
- test_description=test_description,
176
- ),
177
- },
178
- *[
179
- {
180
- "type": "image_url",
181
- "image_url": {
182
- "url": figure._get_b64_url(),
183
- },
184
- }
185
- for figure in figures
186
- ],
187
- ],
188
- },
189
- ],
190
- )
191
- .choices[0]
192
- .message.content.strip()
126
+ figures = [] if test_summary else figures
127
+
128
+ input_data = {
129
+ "test_name": test_name,
130
+ "test_description": test_description,
131
+ "summary": test_summary,
132
+ "figures": [figure._get_b64_url() for figure in figures],
133
+ }
134
+ system, user = _load_prompt()
135
+
136
+ response = client.chat.completions.create(
137
+ model=model,
138
+ temperature=0.0,
139
+ messages=[
140
+ prompt_to_message("system", system.render(input_data)),
141
+ prompt_to_message("user", user.render(input_data)),
142
+ ],
193
143
  )
194
144
 
145
+ return response.choices[0].message.content
146
+
195
147
 
196
148
  def background_generate_description(
197
149
  test_id: str,
198
150
  test_description: str,
199
151
  test_summary: str,
200
152
  figures: list = None,
153
+ metric: Union[int, float] = None,
201
154
  ):
202
155
  def wrapped():
203
156
  try:
204
157
  return generate_description(
205
- test_id, test_description, test_summary, figures
158
+ test_id=test_id,
159
+ test_description=test_description,
160
+ test_summary=test_summary,
161
+ figures=figures,
162
+ metric=metric,
206
163
  )
207
164
  except Exception as e:
208
165
  logger.error(f"Failed to generate description: {e}")
@@ -217,6 +174,7 @@ def get_description_metadata(
217
174
  default_description,
218
175
  summary=None,
219
176
  figures=None,
177
+ metric=None,
220
178
  prefix="metric_description",
221
179
  should_generate=True,
222
180
  ):
@@ -238,16 +196,18 @@ def get_description_metadata(
238
196
  default_description (str): The default description for the test
239
197
  summary (Any): The test summary or results to interpret
240
198
  figures (List[Figure]): The figures to attach to the test suite result
199
+ metric (Union[int, float]): Unit metrics attached to the test result
241
200
  prefix (str): The prefix to use for the content ID (Default: "metric_description")
242
201
  should_generate (bool): Whether to generate the description or not (Default: True)
243
202
 
244
203
  Returns:
245
204
  dict: The metadata object to be logged with the test results
246
205
  """
247
- env_disabled = os.getenv("VALIDMIND_LLM_DESCRIPTIONS_ENABLED", "1") in [
248
- "0",
249
- "false",
250
- ]
206
+ # Check the feature flag first, then the environment variable
207
+ llm_descriptions_enabled = (
208
+ client_config.can_generate_llm_test_descriptions()
209
+ and os.getenv("VALIDMIND_LLM_DESCRIPTIONS_ENABLED", "1") not in ["0", "false"]
210
+ )
251
211
 
252
212
  # TODO: fix circular import
253
213
  from validmind.ai.utils import is_configured
@@ -255,7 +215,7 @@ def get_description_metadata(
255
215
  if (
256
216
  should_generate
257
217
  and (summary or figures)
258
- and not env_disabled
218
+ and llm_descriptions_enabled
259
219
  and is_configured()
260
220
  ):
261
221
  revision_name = AI_REVISION_NAME
@@ -267,6 +227,7 @@ def get_description_metadata(
267
227
  test_description=default_description,
268
228
  test_summary=summary,
269
229
  figures=figures,
230
+ metric=metric,
270
231
  )
271
232
 
272
233
  else:
@@ -0,0 +1,29 @@
1
+ id: test_result_description
2
+ name: Test Result Description
3
+ description: Generate a description for a test result
4
+ version: 0.1.0
5
+ model: gpt-4o
6
+ temperature: 0.0
7
+ output_type: markdown
8
+ prompts:
9
+ system:
10
+ role: system
11
+ path: system.jinja
12
+ user:
13
+ role: user
14
+ path: user.jinja
15
+ inputs:
16
+ test_name:
17
+ description: The name of the test that produced the result (usually the last part of the test ID)
18
+ type: string
19
+ test_description:
20
+ description: The description (docstring) of the test that was run
21
+ type: string
22
+ summary:
23
+ description: The json result summary (i.e. the table(s) returned by the test)
24
+ type: list
25
+ optional: true
26
+ figures:
27
+ description: A list of base64 encoded images of the figures returned by the test
28
+ type: list
29
+ optional: true
@@ -0,0 +1,73 @@
1
+ # Copyright © 2023-2024 ValidMind Inc. All rights reserved.
2
+ # See the LICENSE file in the root of this repository for details.
3
+ # SPDX-License-Identifier: AGPL-3.0 AND ValidMind Commercial
4
+
5
+ import multiprocessing
6
+
7
+ MIN_IMAGES_FOR_PARALLEL = 4
8
+ MAX_WORKERS = multiprocessing.cpu_count()
9
+
10
+
11
+ def parallel_downsample_images(base64_strings):
12
+ import os
13
+ import sys
14
+
15
+ sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
16
+ from test_result_description.image_processing import (
17
+ downsample_image, # type: ignore
18
+ )
19
+
20
+ num_images = len(base64_strings)
21
+
22
+ if num_images < MIN_IMAGES_FOR_PARALLEL:
23
+ return [downsample_image(img) for img in base64_strings]
24
+
25
+ num_workers = min(num_images, MAX_WORKERS)
26
+
27
+ with multiprocessing.Pool(processes=num_workers) as pool:
28
+ results = pool.map(downsample_image, base64_strings)
29
+
30
+ sys.path.pop(0)
31
+
32
+ return results
33
+
34
+
35
+ class Context:
36
+ def __init__(self, mode="local"):
37
+ pass
38
+
39
+ def load(self, input_data):
40
+ # this task can accept a dict or a test result object from the dev framework
41
+ if isinstance(input_data, dict):
42
+ return input_data
43
+
44
+ # we are likely running outside of the dev framework and need to convert
45
+ # the test result object to a dictionary
46
+ test_result = input_data
47
+
48
+ try:
49
+ from markdownify import markdownify as md
50
+ except ImportError as e:
51
+ raise ImportError(
52
+ "Failed to import markdownify. Please install the package to use this task."
53
+ ) from e
54
+
55
+ input_data = {
56
+ "test_name": test_result.result_id.split(".")[-1],
57
+ "test_description": md(test_result.result_metadata[0]["text"]),
58
+ }
59
+
60
+ if hasattr(test_result, "metric") and test_result.metric.summary is not None:
61
+ input_data["summary"] = test_result.metric.summary.serialize()
62
+ elif (
63
+ hasattr(test_result, "test_results")
64
+ and test_result.test_results.summary is not None
65
+ ):
66
+ input_data["summary"] = test_result.test_results.summary.serialize()
67
+
68
+ if test_result.figures:
69
+ input_data["figures"] = parallel_downsample_images(
70
+ [figure._get_b64_url() for figure in test_result.figures]
71
+ )
72
+
73
+ return input_data
@@ -0,0 +1,124 @@
1
+ # Copyright © 2023-2024 ValidMind Inc. All rights reserved.
2
+ # See the LICENSE file in the root of this repository for details.
3
+ # SPDX-License-Identifier: AGPL-3.0 AND ValidMind Commercial
4
+
5
+ import base64
6
+ import io
7
+
8
+ import numpy as np
9
+ from PIL import Image, ImageEnhance, ImageFilter
10
+
11
+ DOWNSAMPLE_PERCENTAGE = 50
12
+
13
+
14
+ def open_base64_image(base64_string):
15
+ if base64_string.startswith("data:image/png;base64,"):
16
+ base64_string = base64_string.split(",")[1]
17
+
18
+ image_data = base64.b64decode(base64_string)
19
+ image_buffer = io.BytesIO(image_data)
20
+ image = Image.open(image_buffer)
21
+
22
+ return image
23
+
24
+
25
+ def downsample_image(base64_string):
26
+ image = open_base64_image(base64_string)
27
+
28
+ # Calculate the target dimensions based on the reduction percentage
29
+ target_width = int(image.width * (1 - DOWNSAMPLE_PERCENTAGE / 100))
30
+ target_height = int(image.height * (1 - DOWNSAMPLE_PERCENTAGE / 100))
31
+
32
+ # If the image is already smaller than the target size, return the original
33
+ if image.width <= target_width and image.height <= target_height:
34
+ return base64_string
35
+
36
+ # remove any margins from the image
37
+ # Find the bounding box of non-uniform pixels (margin detection)
38
+ width, height = image.size
39
+ background = image.getpixel((0, 0)) # Assume top-left pixel is background color
40
+
41
+ def is_different(pixel):
42
+ return pixel != background
43
+
44
+ left = next(
45
+ x
46
+ for x in range(width)
47
+ if any(is_different(image.getpixel((x, y))) for y in range(height))
48
+ )
49
+ right = next(
50
+ x
51
+ for x in range(width - 1, -1, -1)
52
+ if any(is_different(image.getpixel((x, y))) for y in range(height))
53
+ )
54
+ top = next(
55
+ y
56
+ for y in range(height)
57
+ if any(is_different(image.getpixel((x, y))) for x in range(width))
58
+ )
59
+ bottom = next(
60
+ y
61
+ for y in range(height - 1, -1, -1)
62
+ if any(is_different(image.getpixel((x, y))) for x in range(width))
63
+ )
64
+
65
+ # Crop the image to remove the uniform margin (with some padding)
66
+ bbox = (left - 5, top - 5, right + 6, bottom + 6)
67
+ image = image.crop(bbox)
68
+
69
+ # If the image has an alpha channel, remove any transparent margins
70
+ if image.mode in ("RGBA", "LA"):
71
+ alpha = image.getchannel("A")
72
+ bbox = alpha.getbbox()
73
+ if bbox:
74
+ image = image.crop(bbox)
75
+
76
+ # Apply unsharp mask to enhance edges
77
+ image = image.filter(ImageFilter.UnsharpMask(radius=2, percent=150, threshold=3))
78
+
79
+ # Calculate new dimensions
80
+ aspect_ratio = image.width / image.height
81
+ new_height = target_height
82
+ new_width = int(new_height * aspect_ratio)
83
+
84
+ # print(f"downsampling from {width}x{height} to {new_width}x{new_height}")
85
+
86
+ # Ensure we don't exceed the target width
87
+ if new_width > target_width:
88
+ new_width = target_width
89
+ new_height = int(new_width / aspect_ratio)
90
+
91
+ # print(f"downsampling from {image.width}x{image.height} to {new_width}x{new_height}")
92
+
93
+ # Convert to numpy array for custom downsampling
94
+ img_array = np.array(image)
95
+
96
+ # Optimized area interpolation
97
+ h_factor = img_array.shape[0] / new_height
98
+ w_factor = img_array.shape[1] / new_width
99
+
100
+ h_indices = (np.arange(new_height).reshape(-1, 1) * h_factor).astype(int)
101
+ w_indices = (np.arange(new_width).reshape(1, -1) * w_factor).astype(int)
102
+
103
+ h_indices = np.minimum(h_indices, img_array.shape[0] - 1)
104
+ w_indices = np.minimum(w_indices, img_array.shape[1] - 1)
105
+
106
+ # Convert back to PIL Image
107
+ image = Image.fromarray(img_array[h_indices, w_indices].astype(np.uint8))
108
+
109
+ # Enhance contrast slightly
110
+ enhancer = ImageEnhance.Contrast(image)
111
+ image = enhancer.enhance(1.2)
112
+
113
+ # Sharpen the image
114
+ image = image.filter(ImageFilter.SHARPEN)
115
+
116
+ # Convert the image to bytes in PNG format
117
+ buffered = io.BytesIO()
118
+ image.save(buffered, format="PNG")
119
+ img_bytes = buffered.getvalue()
120
+
121
+ # Encode the bytes to base64
122
+ b64_encoded = base64.b64encode(img_bytes).decode("utf-8")
123
+
124
+ return f"data:image/png;base64,{b64_encoded}"
@@ -0,0 +1,39 @@
1
+ You are an expert data scientist and MRM specialist.
2
+ You are tasked with analyzing the results of a quantitative test run on some model or dataset.
3
+ Your goal is to create a test description that will act as part of the model documentation.
4
+ You will provide both the developer and other consumers of the documentation with a clear and concise "interpretation" of the results they will see.
5
+ The overarching theme to maintain is MRM documentation.
6
+
7
+ Examine the provided statistical test results and compose a description of the results.
8
+ The results are either in the form of serialized tables or images of plots.
9
+ Compose a description and interpretation of the result to accompany it in MRM documentation.
10
+ It will be read by other data scientists and developers and by validators and stakeholders.
11
+
12
+ Use valid Markdown syntax to format the response.
13
+ Avoid long sentences and complex vocabulary.
14
+ Avoid overly verbose explanations - the goal is to explain to a user what they are seeing in the results.
15
+ Structure the response clearly and logically.
16
+ Respond only with your analysis and insights, not the verbatim test results.
17
+ Respond only with the markdown content, no explanation or context for your response is necessary.
18
+ Use the Test ID that is provided to form the Test Name e.g. "ClassImbalance" -> "Class Imbalance".
19
+
20
+ Explain the test, its purpose, its mechanism/formula etc and why it is useful.
21
+ If relevant, provide a very brief description of the way this test is used in model/dataset evaluation and how it is interpreted.
22
+ Highlight the key insights from the test results. The key insights should be concise and easily understood.
23
+ An insight should only be included if it is something not entirely obvious from the test results.
24
+ End the response with any closing remarks, summary or additional useful information.
25
+
26
+ Use the following format for the response (feel free to stray from it if necessary - this is a suggested starting point):
27
+
28
+ <ResponseFormat>
29
+ **<Test Name>** calculates the xyz <continue to explain what it does in detail>...
30
+
31
+ This test is useful for <explain why and for what this test is useful>...
32
+
33
+ **Key Insights:**
34
+
35
+ The following key insights can be identified in the test results:
36
+
37
+ - **<key insight 1 - title>**: <concise explanation of key insight 1>
38
+ - ...<continue with any other key insights using the same format>
39
+ </ResponseFormat>
@@ -0,0 +1,25 @@
1
+ **Test ID**: `{{ test_name }}`
2
+
3
+ **Test Description**:
4
+
5
+ {{ test_description }}
6
+
7
+ ---
8
+
9
+ Generate a description of the following result of the test using the instructions given in your system prompt.
10
+
11
+ {%- if summary %}
12
+ **Test Result Tables** *(Raw Data)*:
13
+ {{ summary }}
14
+ {%- endif %}
15
+
16
+ {%- if figures %}
17
+ The following images make up the results of the test.
18
+ {%- for b64_image_url in figures %}
19
+ [[IMAGE:{{ b64_image_url }}]]
20
+ {%- endfor %}
21
+ {%- endif %}
22
+
23
+ Keep your response concise and to the point!
24
+ Only include content in your response if its something truly insightful or interesting!
25
+ DO NOT VERBOSELY EXPLAIN THE TEST OR THE RESULTS!!!