bertrend 0.3.1__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 (181) hide show
  1. bertrend/BERTopicModel.py +320 -0
  2. bertrend/BERTrend.py +1211 -0
  3. bertrend/__init__.py +59 -0
  4. bertrend/config/bertopic_default_config.toml +48 -0
  5. bertrend/config/bertrend_default_config.toml +16 -0
  6. bertrend/config/parameters.py +92 -0
  7. bertrend/config/services_default_config.toml +32 -0
  8. bertrend/demos/__init__.py +4 -0
  9. bertrend/demos/demos_utils/__init__.py +13 -0
  10. bertrend/demos/demos_utils/data_loading_component.py +267 -0
  11. bertrend/demos/demos_utils/embed_documents_component.py +52 -0
  12. bertrend/demos/demos_utils/icons.py +41 -0
  13. bertrend/demos/demos_utils/messages.py +11 -0
  14. bertrend/demos/demos_utils/parameters_component.py +162 -0
  15. bertrend/demos/demos_utils/state_utils.py +99 -0
  16. bertrend/demos/start_summarizer_app.sh +9 -0
  17. bertrend/demos/start_topic_analysis_app.sh +9 -0
  18. bertrend/demos/start_weak_signals_app.sh +9 -0
  19. bertrend/demos/summarization/__init__.py +4 -0
  20. bertrend/demos/summarization/summarizer_app.py +98 -0
  21. bertrend/demos/topic_analysis/__init__.py +4 -0
  22. bertrend/demos/topic_analysis/app.py +87 -0
  23. bertrend/demos/topic_analysis/app_utils.py +69 -0
  24. bertrend/demos/topic_analysis/demo_pages/__init__.py +4 -0
  25. bertrend/demos/topic_analysis/demo_pages/explore_topics.py +455 -0
  26. bertrend/demos/topic_analysis/demo_pages/newsletters_generation.py +178 -0
  27. bertrend/demos/topic_analysis/demo_pages/temporal_visualizations.py +573 -0
  28. bertrend/demos/topic_analysis/demo_pages/topic_visualizations.py +280 -0
  29. bertrend/demos/topic_analysis/demo_pages/training_page.py +219 -0
  30. bertrend/demos/topic_analysis/messages.py +13 -0
  31. bertrend/demos/weak_signals/__init__.py +4 -0
  32. bertrend/demos/weak_signals/app.py +510 -0
  33. bertrend/demos/weak_signals/messages.py +32 -0
  34. bertrend/demos/weak_signals/visualizations_utils.py +403 -0
  35. bertrend/llm_utils/__init__.py +4 -0
  36. bertrend/llm_utils/newsletter.css +131 -0
  37. bertrend/llm_utils/newsletter_features.py +272 -0
  38. bertrend/llm_utils/openai_client.py +235 -0
  39. bertrend/llm_utils/prompts.py +94 -0
  40. bertrend/metrics/__init__.py +4 -0
  41. bertrend/metrics/temporal_metrics_embedding.py +861 -0
  42. bertrend/metrics/topic_metrics.py +199 -0
  43. bertrend/resources/common_ngrams.json +23 -0
  44. bertrend/resources/stopwords-en.json +3 -0
  45. bertrend/resources/stopwords-fr.json +1 -0
  46. bertrend/resources/stopwords-rte.json +15 -0
  47. bertrend/services/__init__.py +4 -0
  48. bertrend/services/authentication.py +66 -0
  49. bertrend/services/embedding_client.py +154 -0
  50. bertrend/services/embedding_service.py +380 -0
  51. bertrend/services/summarizer.py +45 -0
  52. bertrend/services/summary/__init__.py +4 -0
  53. bertrend/services/summary/abstractive_summarizer.py +74 -0
  54. bertrend/services/summary/chatgpt_summarizer.py +67 -0
  55. bertrend/services/summary/extractive_summarizer.py +368 -0
  56. bertrend/services/summary/lexrank.py +126 -0
  57. bertrend/services/summary/prompts.py +43 -0
  58. bertrend/tests/__init__.py +4 -0
  59. bertrend/tests/test_abstractive_summarizer.py +140 -0
  60. bertrend/tests/test_bertrend.py +1091 -0
  61. bertrend/tests/test_cache_utils.py +111 -0
  62. bertrend/tests/test_chatgpt_summarizer.py +150 -0
  63. bertrend/tests/test_config_utils.py +54 -0
  64. bertrend/tests/test_data_loading.py +326 -0
  65. bertrend/tests/test_data_structure.py +53 -0
  66. bertrend/tests/test_embedding_client.py +546 -0
  67. bertrend/tests/test_extractive_summarizer.py +250 -0
  68. bertrend/tests/test_installation.py +183 -0
  69. bertrend/tests/test_newsletter_features.py +354 -0
  70. bertrend/tests/test_openai_client.py +210 -0
  71. bertrend/tests/test_representative_docs.py +125 -0
  72. bertrend/tests/test_summarizer.py +115 -0
  73. bertrend/tests/test_temporal_metrics_embedding.py +349 -0
  74. bertrend/tests/test_topic_description.py +159 -0
  75. bertrend/tests/test_topic_metrics.py +233 -0
  76. bertrend/tests/test_topic_model.py +285 -0
  77. bertrend/tests/test_trend_analysis_data_structure.py +174 -0
  78. bertrend/tests/test_trend_analysis_prompts.py +225 -0
  79. bertrend/tests/test_trend_analysis_visualizations.py +307 -0
  80. bertrend/tests/test_trend_analysis_weak_signals.py +598 -0
  81. bertrend/tests/test_utils_init.py +70 -0
  82. bertrend/tests/test_visualizations.py +306 -0
  83. bertrend/topic_analysis/__init__.py +4 -0
  84. bertrend/topic_analysis/data_structure.py +13 -0
  85. bertrend/topic_analysis/prompts.py +42 -0
  86. bertrend/topic_analysis/representative_docs.py +57 -0
  87. bertrend/topic_analysis/topic_description.py +66 -0
  88. bertrend/topic_analysis/visualizations.py +315 -0
  89. bertrend/trend_analysis/__init__.py +4 -0
  90. bertrend/trend_analysis/data_structure.py +78 -0
  91. bertrend/trend_analysis/prompts.py +229 -0
  92. bertrend/trend_analysis/signal_llm_template_en.html +181 -0
  93. bertrend/trend_analysis/signal_llm_template_fr.html +181 -0
  94. bertrend/trend_analysis/visualizations.py +605 -0
  95. bertrend/trend_analysis/weak_signals.py +425 -0
  96. bertrend/utils/__init__.py +31 -0
  97. bertrend/utils/cache_utils.py +31 -0
  98. bertrend/utils/config_utils.py +41 -0
  99. bertrend/utils/data_loading.py +329 -0
  100. bertrend-0.3.1.dist-info/AUTHORS.txt +15 -0
  101. bertrend-0.3.1.dist-info/LICENSE.md +373 -0
  102. bertrend-0.3.1.dist-info/METADATA +400 -0
  103. bertrend-0.3.1.dist-info/RECORD +181 -0
  104. bertrend-0.3.1.dist-info/WHEEL +4 -0
  105. bertrend_apps/__init__.py +4 -0
  106. bertrend_apps/common/__init__.py +4 -0
  107. bertrend_apps/common/crontab_utils.py +113 -0
  108. bertrend_apps/common/mail_utils.py +91 -0
  109. bertrend_apps/config/feeds/arxiv_feed.toml +15 -0
  110. bertrend_apps/config/feeds/decarbonation_feed.toml +15 -0
  111. bertrend_apps/config/feeds/llm_en_feed.toml +15 -0
  112. bertrend_apps/config/feeds/llm_feed.toml +16 -0
  113. bertrend_apps/config/feeds/nlp_feed.toml +15 -0
  114. bertrend_apps/config/feeds/rte_feed.toml +15 -0
  115. bertrend_apps/config/gmail_credentials.json +1 -0
  116. bertrend_apps/config/newsletters/arxiv_newsletter_from_last.toml +39 -0
  117. bertrend_apps/config/newsletters/arxiv_newsletter_from_scratch.toml +39 -0
  118. bertrend_apps/config/newsletters/decarbonation_newsletter_from_last.toml +38 -0
  119. bertrend_apps/config/newsletters/decarbonation_newsletter_from_scratch.toml +38 -0
  120. bertrend_apps/config/newsletters/llm_en_newsletter_from_last.toml +38 -0
  121. bertrend_apps/config/newsletters/llm_newsletter_from_last.toml +39 -0
  122. bertrend_apps/config/newsletters/nlp_newsletter_from_last.toml +38 -0
  123. bertrend_apps/config/newsletters/rte_newsletter_from_last.toml +38 -0
  124. bertrend_apps/config/newsletters/rte_newsletter_from_scratch.toml +38 -0
  125. bertrend_apps/data_provider/__init__.py +9 -0
  126. bertrend_apps/data_provider/__main__.py +237 -0
  127. bertrend_apps/data_provider/arxiv_provider.py +161 -0
  128. bertrend_apps/data_provider/bing_news_provider.py +82 -0
  129. bertrend_apps/data_provider/curebot_provider.py +105 -0
  130. bertrend_apps/data_provider/data_provider.py +194 -0
  131. bertrend_apps/data_provider/google_news_provider.py +94 -0
  132. bertrend_apps/data_provider/newscatcher_provider.py +74 -0
  133. bertrend_apps/data_provider/utils.py +165 -0
  134. bertrend_apps/exploration/__init__.py +4 -0
  135. bertrend_apps/exploration/curebot/__init__.py +4 -0
  136. bertrend_apps/exploration/curebot/app.py +45 -0
  137. bertrend_apps/exploration/curebot/app_utils.py +317 -0
  138. bertrend_apps/exploration/curebot/config.toml +33 -0
  139. bertrend_apps/exploration/curebot/import_extraction_article.ipynb +108 -0
  140. bertrend_apps/exploration/curebot/old_veille_analyse.py +298 -0
  141. bertrend_apps/exploration/curebot/prompts.py +24 -0
  142. bertrend_apps/exploration/curebot/start_newsletter_generator.sh +8 -0
  143. bertrend_apps/exploration/curebot/tabs/tab1.py +150 -0
  144. bertrend_apps/exploration/curebot/tabs/tab2.py +95 -0
  145. bertrend_apps/exploration/curebot/tabs/tab3.py +125 -0
  146. bertrend_apps/exploration/dataviz/Main_page.py +10 -0
  147. bertrend_apps/exploration/dataviz/__init__.py +4 -0
  148. bertrend_apps/exploration/dataviz/pages/1_Data_feeds.py +105 -0
  149. bertrend_apps/exploration/geolocalization/__init__.py +4 -0
  150. bertrend_apps/exploration/geolocalization/entity_utils.py +118 -0
  151. bertrend_apps/exploration/geolocalization/spacy/__init__.py +4 -0
  152. bertrend_apps/exploration/geolocalization/spacy/common_factory.py +142 -0
  153. bertrend_apps/exploration/geolocalization/spacy/dbpedia.py +206 -0
  154. bertrend_apps/exploration/geolocalization/spacy/nlp.config +37 -0
  155. bertrend_apps/exploration/geolocalization/spacy/utils.py +39 -0
  156. bertrend_apps/install_feeds_and_newsletters.py +29 -0
  157. bertrend_apps/newsletters/__init__.py +4 -0
  158. bertrend_apps/newsletters/__main__.py +277 -0
  159. bertrend_apps/prospective_demo/.streamlit/config.toml +3 -0
  160. bertrend_apps/prospective_demo/.streamlit/secrets.toml +9 -0
  161. bertrend_apps/prospective_demo/__init__.py +70 -0
  162. bertrend_apps/prospective_demo/app.py +111 -0
  163. bertrend_apps/prospective_demo/authentication.py +44 -0
  164. bertrend_apps/prospective_demo/dashboard_analysis.py +132 -0
  165. bertrend_apps/prospective_demo/dashboard_common.py +79 -0
  166. bertrend_apps/prospective_demo/dashboard_signals.py +226 -0
  167. bertrend_apps/prospective_demo/feeds_common.py +38 -0
  168. bertrend_apps/prospective_demo/feeds_config.py +261 -0
  169. bertrend_apps/prospective_demo/feeds_data.py +95 -0
  170. bertrend_apps/prospective_demo/i18n.py +136 -0
  171. bertrend_apps/prospective_demo/llm_utils.py +34 -0
  172. bertrend_apps/prospective_demo/models_info.py +361 -0
  173. bertrend_apps/prospective_demo/perf_utils.py +23 -0
  174. bertrend_apps/prospective_demo/process_new_data.py +348 -0
  175. bertrend_apps/prospective_demo/report_generation.py +214 -0
  176. bertrend_apps/prospective_demo/report_template.html +148 -0
  177. bertrend_apps/prospective_demo/report_utils.py +132 -0
  178. bertrend_apps/prospective_demo/start_demo.sh +9 -0
  179. bertrend_apps/prospective_demo/streamlit_utils.py +37 -0
  180. bertrend_apps/start_apps.sh +15 -0
  181. bertrend_apps/stop_apps.sh +11 -0
@@ -0,0 +1,348 @@
1
+ # Copyright (c) 2024, RTE (https://www.rte-france.com)
2
+ # See AUTHORS.txt
3
+ # SPDX-License-Identifier: MPL-2.0
4
+ # This file is part of BERTrend.
5
+ from datetime import timedelta
6
+ from pathlib import Path
7
+
8
+ import pandas as pd
9
+ import typer
10
+ from jsonlines import jsonlines
11
+ from loguru import logger
12
+
13
+ from bertrend import load_toml_config, FEED_BASE_PATH
14
+ from bertrend.BERTopicModel import BERTopicModel
15
+ from bertrend.BERTrend import train_new_data, BERTrend
16
+ from bertrend.services.embedding_service import EmbeddingService
17
+ from bertrend.trend_analysis.weak_signals import analyze_signal
18
+ from bertrend.utils.data_loading import (
19
+ load_data,
20
+ split_data,
21
+ group_by_days,
22
+ TEXT_COLUMN,
23
+ )
24
+ from bertrend_apps.prospective_demo import (
25
+ get_user_feed_path,
26
+ get_user_models_path,
27
+ NOISE,
28
+ WEAK_SIGNALS,
29
+ STRONG_SIGNALS,
30
+ LLM_TOPIC_DESCRIPTION_COLUMN,
31
+ LLM_TOPIC_TITLE_COLUMN,
32
+ DEFAULT_ANALYSIS_CFG,
33
+ get_model_cfg_path,
34
+ URLS_COLUMN,
35
+ get_model_interpretation_path,
36
+ )
37
+ from bertrend_apps.prospective_demo.llm_utils import generate_bertrend_topic_description
38
+
39
+ DEFAULT_TOP_K = 5
40
+
41
+
42
+ def load_all_data(model_id: str, user: str, language: str):
43
+ # TODO: to be improved
44
+ cfg_file = get_user_feed_path(user, model_id)
45
+ if not cfg_file.exists():
46
+ logger.error(f"Cannot find/process config file: {cfg_file}")
47
+ return
48
+ cfg = load_toml_config(cfg_file)
49
+ feed_base_dir = cfg["data-feed"]["feed_dir_path"]
50
+ files = list(
51
+ Path(FEED_BASE_PATH, feed_base_dir).glob(
52
+ f"*{cfg['data-feed'].get('id')}*.jsonl*"
53
+ )
54
+ )
55
+ if not files:
56
+ logger.warning(f"No new data for '{model_id}', nothing to do")
57
+ return
58
+
59
+ dfs = [load_data(Path(f), language=language) for f in files]
60
+ new_data = pd.concat(dfs).drop_duplicates(
61
+ subset=["title"], keep="first", inplace=False
62
+ )
63
+ return new_data
64
+
65
+
66
+ def get_relevant_model_config(
67
+ model_id: str,
68
+ user: str,
69
+ ):
70
+ # Load model & analysis config
71
+ model_cfg_path = get_model_cfg_path(user, model_id)
72
+ try:
73
+ model_analysis_cfg = load_toml_config(model_cfg_path)
74
+ except Exception:
75
+ model_analysis_cfg = DEFAULT_ANALYSIS_CFG
76
+ # Extract relevant values
77
+ granularity = model_analysis_cfg["model_config"]["granularity"]
78
+ window_size = model_analysis_cfg["model_config"]["window_size"]
79
+ language = model_analysis_cfg["model_config"]["language"]
80
+ if language not in ["French", "English"]:
81
+ language = "French"
82
+ return granularity, window_size, language
83
+
84
+
85
+ def generate_llm_interpretation(
86
+ bertrend: BERTrend,
87
+ reference_timestamp: pd.Timestamp,
88
+ df: pd.DataFrame,
89
+ df_name: str,
90
+ output_path: Path,
91
+ top_k: int = DEFAULT_TOP_K,
92
+ ):
93
+ """
94
+ Generate detailed analysis for the top k topics using parallel processing.
95
+
96
+ Args:
97
+ bertrend: BERTrend instance
98
+ reference_timestamp: Reference timestamp for analysis
99
+ df: Input DataFrame
100
+ df_name: Name of the DataFrame for output
101
+ output_path: Path to save the results
102
+ top_k: Number of top topics to analyze
103
+ """
104
+
105
+ interpretation = []
106
+ for topic in df.sort_values(by=["Latest_Popularity"], ascending=False).head(top_k)[
107
+ "Topic"
108
+ ]:
109
+ summary, analysis = analyze_signal(bertrend, topic, reference_timestamp)
110
+ if not summary or not analysis:
111
+ logger.warning(f"Skipping topic {topic} as analysis of signal failed.")
112
+ continue
113
+ interpretation.append(
114
+ {
115
+ "topic": topic,
116
+ "summary": summary.model_dump_json(),
117
+ "analysis": analysis.model_dump_json(),
118
+ }
119
+ )
120
+
121
+ # Save interpretation
122
+ output_file_name = output_path / f"{df_name}_interpretation.jsonl"
123
+ with jsonlines.open(
124
+ output_file_name,
125
+ mode="w",
126
+ ) as writer:
127
+ for item in interpretation:
128
+ writer.write(item)
129
+ logger.success(f"Interpretation saved to: {output_file_name}")
130
+
131
+
132
+ def train_new_model_for_period(
133
+ model_id: str,
134
+ user_name: str,
135
+ new_data: pd.DataFrame,
136
+ reference_timestamp: pd.Timestamp,
137
+ ):
138
+ logger.info(
139
+ f"Training BERTrend model with new data - user: {user_name}, model_id: {model_id}, reference_timestamp: {reference_timestamp}..."
140
+ )
141
+ # Initialization of embedding service
142
+ # TODO: customize service (lang, etc)
143
+ embedding_service = EmbeddingService(local=False)
144
+
145
+ # Path to previously saved models for those data and this user
146
+ bertrend_models_path = get_user_models_path(user_name, model_id)
147
+
148
+ # Get relevant model info from config
149
+ granularity, window_size, language = get_relevant_model_config(
150
+ model_id=model_id, user=user_name
151
+ )
152
+ language_code = "fr" if language == "French" else "en"
153
+
154
+ # Process new data
155
+ bertrend = train_new_data(
156
+ reference_timestamp=reference_timestamp,
157
+ new_data=new_data,
158
+ bertrend_models_path=bertrend_models_path,
159
+ embedding_service=embedding_service,
160
+ language=language,
161
+ granularity=granularity,
162
+ )
163
+
164
+ if len(bertrend.doc_groups) < 2:
165
+ # This is generally the case when we have only one model
166
+ return
167
+
168
+ # Compute popularities
169
+ bertrend.calculate_signal_popularity()
170
+
171
+ # classify last signals
172
+ cut_off_date = new_data["timestamp"].max() - timedelta(days=granularity)
173
+ noise_topics_df, weak_signal_topics_df, strong_signal_topics_df = (
174
+ bertrend.classify_signals(window_size, cut_off_date)
175
+ )
176
+
177
+ # LLM-based interpretation
178
+ interpretation_path = get_model_interpretation_path(
179
+ user_name, model_id, reference_timestamp
180
+ )
181
+ interpretation_path.mkdir(parents=True, exist_ok=True)
182
+ for df, df_name in zip(
183
+ [noise_topics_df, weak_signal_topics_df, strong_signal_topics_df],
184
+ [NOISE, WEAK_SIGNALS, STRONG_SIGNALS],
185
+ ):
186
+ if not df.empty:
187
+ # enrich signal description with LLM-based topic description
188
+ df[[LLM_TOPIC_TITLE_COLUMN, LLM_TOPIC_DESCRIPTION_COLUMN]] = df.apply(
189
+ lambda row: pd.Series(
190
+ generate_bertrend_topic_description(
191
+ topic_words=row["Representation"],
192
+ topic_number=row["Topic"],
193
+ texts=row["Documents"],
194
+ language_code=language_code,
195
+ )
196
+ ),
197
+ axis=1,
198
+ )
199
+
200
+ # Add documents URL
201
+ df = pd.merge(
202
+ df,
203
+ bertrend.merged_df[["Topic", URLS_COLUMN]],
204
+ on="Topic",
205
+ how="left",
206
+ )
207
+ df[URLS_COLUMN] = df[URLS_COLUMN].apply(
208
+ lambda x: list(set(x))
209
+ ) # Removes duplicates within each list
210
+
211
+ # FIXME: for some unknown reasons, a few elements in the Documents column are not a str but a
212
+ # timestamp (the identifier of current model); this generates errors when trying to serialize the
213
+ # df to parquet. The code snippet below is a workaround to avoid this issue.
214
+ df["Documents"] = df["Documents"].apply(
215
+ lambda l: [x if isinstance(x, str) else "" for x in l]
216
+ )
217
+
218
+ output_path = interpretation_path / f"{df_name}.parquet"
219
+ df.to_parquet(output_path)
220
+ logger.success(f"{df_name} saved to: {output_path}")
221
+
222
+ # Obtain detailed LLM-based interpretion for signals
223
+ generate_llm_interpretation(
224
+ bertrend,
225
+ reference_timestamp=reference_timestamp,
226
+ df=df,
227
+ df_name=df_name,
228
+ output_path=interpretation_path,
229
+ )
230
+
231
+
232
+ def regenerate_models(model_id: str, user: str, with_analysis: bool = True):
233
+ """Regenerate from scratch (method retrospective) the models associated with the specified model identifier
234
+ for the specified user."""
235
+
236
+ # Get relevant model info from config
237
+ granularity, window_size, language = get_relevant_model_config(
238
+ model_id=model_id, user=user
239
+ )
240
+
241
+ # Load model config
242
+ df = load_all_data(model_id=model_id, user=user, language=language)
243
+ logger.info(f"Size of dataset: {len(df)}")
244
+
245
+ # Split data by paragraphs
246
+ df = split_data(df)
247
+
248
+ # Process new data and save models
249
+ # - Group data based on granularity
250
+ grouped_data = group_by_days(df=df, day_granularity=granularity)
251
+
252
+ if not with_analysis:
253
+ # Path to saved models
254
+ bertrend_models_path = get_user_models_path(user, model_id)
255
+
256
+ # Initialization of embedding service
257
+ # TODO: customize service (lang, etc)
258
+ embedding_service = EmbeddingService(local=False)
259
+
260
+ # Train BERTrend
261
+ bertrend = BERTrend(
262
+ topic_model=BERTopicModel({"global": {"language": language}})
263
+ )
264
+ embeddings, _, _ = embedding_service.embed(
265
+ texts=df[TEXT_COLUMN],
266
+ )
267
+ bertrend.train_topic_models(
268
+ grouped_data=grouped_data,
269
+ embedding_model=embedding_service.embedding_model_name,
270
+ embeddings=embeddings,
271
+ bertrend_models_path=bertrend_models_path,
272
+ save_topic_models=True,
273
+ )
274
+ bertrend.save_model(models_path=bertrend_models_path)
275
+
276
+ logger.success(
277
+ f"Regenerated models for '{model_id}' from scratch. BERTrend model was built using {len(bertrend.doc_groups)} models/time periods."
278
+ )
279
+
280
+ else: # with analysis
281
+ for ts, df in sorted(grouped_data.items()):
282
+ train_new_model_for_period(
283
+ model_id=model_id,
284
+ user_name=user,
285
+ new_data=df.reset_index(drop=True),
286
+ reference_timestamp=ts,
287
+ )
288
+
289
+ logger.success(
290
+ f"Regenerated models for '{model_id}' from scratch with LLM-based analysis. BERTrend model was built using {len(grouped_data)} models/time periods."
291
+ )
292
+
293
+
294
+ if __name__ == "__main__":
295
+ app = typer.Typer()
296
+
297
+ @app.command("train-new-model")
298
+ def train_new_model(
299
+ user_name: str = typer.Argument(help="Identifier of the user"),
300
+ model_id: str = typer.Argument(help="ID of the model/data to train"),
301
+ ):
302
+ """Incrementally enrich the BERTrend model with new data"""
303
+ logger.info(f'Processing new data for user "{user_name}" about "{model_id}"...')
304
+
305
+ # Get relevant model info from config
306
+ granularity, window_size, language = get_relevant_model_config(
307
+ model_id=model_id, user=user_name
308
+ )
309
+
310
+ # Load data for last period
311
+ new_data = load_all_data(model_id=model_id, user=user_name, language=language)
312
+ # filter data according to granularity
313
+ # Calculate the date X days ago
314
+ reference_timestamp = pd.Timestamp(
315
+ new_data["timestamp"].max().date()
316
+ ) # used to identify the last model
317
+ cut_off_date = new_data["timestamp"].max() - timedelta(days=granularity)
318
+ # Filter the DataFrame to keep only the rows within the last X days
319
+ filtered_df = new_data[new_data["timestamp"] >= cut_off_date]
320
+
321
+ # Split data by paragraphs
322
+ filtered_df = split_data(filtered_df)
323
+
324
+ train_new_model_for_period(
325
+ model_id=model_id,
326
+ user_name=user_name,
327
+ new_data=filtered_df,
328
+ reference_timestamp=reference_timestamp,
329
+ )
330
+
331
+ @app.command("regenerate")
332
+ def regenerate(
333
+ user: str = typer.Argument(help="identifier of the user"),
334
+ model_id: str = typer.Argument(
335
+ help="ID of the model to be regenerated from scratch"
336
+ ),
337
+ with_analysis: bool = typer.Option(
338
+ default=True, help="Regenerate LLM analysis (may take time)"
339
+ ),
340
+ ):
341
+ """Regenerate past models from scratch"""
342
+ logger.info(
343
+ f"Regenerating models for user '{user}' about '{model_id}', with analysis: {with_analysis}..."
344
+ )
345
+ regenerate_models(model_id=model_id, user=user, with_analysis=with_analysis)
346
+
347
+ # Main app
348
+ app()
@@ -0,0 +1,214 @@
1
+ # Copyright (c) 2024, RTE (https://www.rte-france.com)
2
+ # See AUTHORS.txt
3
+ # SPDX-License-Identifier: MPL-2.0
4
+ # This file is part of BERTrend.
5
+ import tempfile
6
+ import re
7
+
8
+ import pandas as pd
9
+ import streamlit as st
10
+ from pathlib import Path
11
+
12
+ from google.auth.exceptions import RefreshError
13
+ from loguru import logger
14
+
15
+ from bertrend.demos.demos_utils.icons import (
16
+ NEWSLETTER_ICON,
17
+ TOPIC_ICON,
18
+ ERROR_ICON,
19
+ DOWNLOAD_ICON,
20
+ EMAIL_ICON,
21
+ )
22
+ from bertrend_apps.common.mail_utils import get_credentials, send_email
23
+ from bertrend_apps.prospective_demo import (
24
+ WEAK_SIGNALS,
25
+ STRONG_SIGNALS,
26
+ LLM_TOPIC_DESCRIPTION_COLUMN,
27
+ LLM_TOPIC_TITLE_COLUMN,
28
+ URLS_COLUMN,
29
+ )
30
+ from bertrend_apps.prospective_demo.dashboard_common import choose_id_and_ts
31
+ from bertrend_apps.prospective_demo.report_utils import generate_html_report
32
+
33
+ WEAK_SIGNAL_NB = 3
34
+ STRONG_SIGNAL_NB = 5
35
+
36
+
37
+ def reporting():
38
+ choose_id_and_ts()
39
+
40
+ tab1, tab2 = st.tabs(
41
+ [
42
+ TOPIC_ICON + " Etape 1: Sélection des sujets à retenir",
43
+ NEWSLETTER_ICON + " Etape 2: Export",
44
+ ]
45
+ )
46
+ with tab1:
47
+ selected_weak_topics_df, selected_strong_topics_df = choose_topics()
48
+ with tab2:
49
+ configure_export(selected_weak_topics_df, selected_strong_topics_df)
50
+
51
+ # generate_newsletter()
52
+
53
+
54
+ def choose_topics():
55
+ st.subheader("Etape 1: Sélection des sujets à retenir")
56
+ model_id = st.session_state.model_id
57
+ dfs_interpretation = st.session_state.signal_interpretations
58
+ if model_id not in dfs_interpretation:
59
+ st.error(f"{ERROR_ICON} Pas de données")
60
+ st.stop()
61
+ cols = st.columns(2)
62
+ with cols[0]:
63
+ st.write("#### :orange[Sujets émergents]")
64
+ if WEAK_SIGNALS not in dfs_interpretation[model_id]:
65
+ st.error(f"{ERROR_ICON} Pas de données")
66
+ filtered_weak_signals = None
67
+ else:
68
+ df_w = dfs_interpretation[model_id][WEAK_SIGNALS]
69
+ weak_topics_list = choose_from_df(df_w)
70
+ filtered_weak_signals = df_w[df_w["Topic"].isin(weak_topics_list)]
71
+
72
+ with cols[1]:
73
+ st.write("#### :green[Sujets forts]")
74
+ if STRONG_SIGNALS not in dfs_interpretation[model_id]:
75
+ st.error(f"{ERROR_ICON} Pas de données")
76
+ filtered_strong_signals = None
77
+ else:
78
+ df_w = dfs_interpretation[model_id][STRONG_SIGNALS]
79
+ strong_topics_list = choose_from_df(df_w)
80
+ filtered_strong_signals = df_w[df_w["Topic"].isin(strong_topics_list)]
81
+
82
+ return filtered_weak_signals, filtered_strong_signals
83
+
84
+
85
+ def choose_from_df(df: pd.DataFrame):
86
+ df["A retenir"] = True
87
+ df["Sujet"] = df[LLM_TOPIC_TITLE_COLUMN]
88
+ df["Description"] = df[LLM_TOPIC_DESCRIPTION_COLUMN]
89
+ columns = ["Topic", "A retenir", "Sujet", "Description"]
90
+ pd.DataFrame(
91
+ [
92
+ {"command": "st.selectbox", "rating": 4, "is_widget": True},
93
+ {"command": "st.balloons", "rating": 5, "is_widget": False},
94
+ {"command": "st.time_input", "rating": 3, "is_widget": True},
95
+ ]
96
+ )
97
+ edited_df = st.data_editor(df[columns], num_rows="dynamic", column_order=columns)
98
+ selection = edited_df[edited_df["A retenir"] == True]["Topic"].tolist()
99
+ return selection
100
+
101
+
102
+ def configure_export(weak_signals: pd.DataFrame, strong_signals: pd.DataFrame):
103
+ st.subheader("Etape 2: Configuration de l'export")
104
+ st.write(
105
+ "TODO: séléction des parties à inclure dans l'export par topic: résumé global, liens, évolution par période, analyse détaillée, etc."
106
+ )
107
+
108
+ st.button(
109
+ "Générer",
110
+ type="primary",
111
+ on_click=lambda: generate_report(weak_signals, strong_signals),
112
+ )
113
+
114
+
115
+ @st.dialog("Rapport (aperçu)", width="large")
116
+ def generate_report(weak_signals: pd.DataFrame, strong_signals: pd.DataFrame):
117
+ model_id = st.session_state.model_id
118
+
119
+ # TODO: output à configurer selon les infos choisies
120
+ # rendering parameters
121
+ number_links = 2
122
+
123
+ report_template = Path(__file__).parent / "report_template.html"
124
+ report_title = f"Actu <font color='royal_blue'>{model_id}</font>"
125
+ data_list = []
126
+ for df, color in zip([weak_signals, strong_signals], ["orange", "green"]):
127
+ # Iterate over the filtered DataFrame rows
128
+ for _, row in df.iterrows():
129
+ # For each row, iterate over each column
130
+ data_list.append(
131
+ {
132
+ "topic_title": row[LLM_TOPIC_TITLE_COLUMN],
133
+ "topic_description": row[LLM_TOPIC_DESCRIPTION_COLUMN],
134
+ "color": color,
135
+ "links": list(set(row[URLS_COLUMN]))[:number_links],
136
+ "analysis": row["analysis"],
137
+ }
138
+ )
139
+
140
+ # Generate the HTML
141
+ output_html = generate_html_report(report_template, report_title, data_list)
142
+
143
+ # Render HTML
144
+ with st.container(height=475):
145
+ st.html(output_html)
146
+
147
+ # Save report to temp file
148
+ temp_report_path = create_temp_report(output_html) # Create the file in temp dir
149
+
150
+ cols = st.columns([2, 3])
151
+ with cols[0]:
152
+ download(temp_report_path, model_id)
153
+ with cols[1]:
154
+ email(temp_report_path, mail_title=f"Rapport veille {model_id}") #
155
+
156
+
157
+ def create_temp_report(html_content) -> Path:
158
+ with tempfile.NamedTemporaryFile(
159
+ mode="w", suffix=".html", delete=False
160
+ ) as temp_file:
161
+ temp_file.write(html_content)
162
+ return Path(temp_file.name)
163
+
164
+
165
+ def download(temp_path: Path, model_id: str):
166
+ with open(temp_path, "r", encoding="utf-8") as file:
167
+ st.download_button(
168
+ label=f"{DOWNLOAD_ICON} Télécharger (html)",
169
+ type="primary",
170
+ data=file.read(),
171
+ file_name=f"rapport_{model_id}.html",
172
+ mime="text/html",
173
+ )
174
+
175
+
176
+ def is_valid_email(email: str) -> bool:
177
+ """Checks if an email address is valid using a regular expression."""
178
+ regex = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
179
+ return re.match(regex, email) is not None
180
+
181
+
182
+ def email(temp_path: Path, mail_title: str) -> None:
183
+ col1, col2 = st.columns([0.6, 0.4])
184
+ with col1:
185
+ user_email = st.text_input("@", label_visibility="collapsed")
186
+
187
+ with col2:
188
+ if st.button(f"{EMAIL_ICON} Envoyer", type="primary"):
189
+ if not user_email:
190
+ return
191
+ if not is_valid_email(user_email):
192
+ st.error(f"{ERROR_ICON} Adresse mail incorrecte")
193
+ return
194
+
195
+ try:
196
+ # Send newsletter by email
197
+ # string to list conversion for recipients
198
+ recipients = [user_email]
199
+ try:
200
+ if recipients:
201
+ credentials = get_credentials()
202
+ with open(temp_path, "r") as file:
203
+ # Read the entire contents of the file into a string
204
+ content = file.read()
205
+ send_email(credentials, mail_title, recipients, content, "html")
206
+ except RefreshError as re:
207
+ logger.error(
208
+ f"Problem with token for email, please regenerate it: {re}"
209
+ )
210
+
211
+ st.success("Email sent successfully!")
212
+
213
+ except Exception as e:
214
+ st.error(f"Error sending email: {e}")