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,298 @@
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 pathlib import Path
6
+ from tempfile import TemporaryDirectory
7
+
8
+ import inspect
9
+ import pandas as pd
10
+ import streamlit as st
11
+ from loguru import logger
12
+ from streamlit.runtime.uploaded_file_manager import UploadedFile
13
+
14
+ from bertrend.services.summary.chatgpt_summarizer import GPTSummarizer
15
+ from bertrend.train import train_BERTopic
16
+ from bertrend.utils.data_loading import (
17
+ load_data,
18
+ TIMESTAMP_COLUMN,
19
+ enhanced_split_df_by_paragraphs,
20
+ clean_dataset,
21
+ )
22
+
23
+ from bertrend_apps.data_provider.curebot_provider import CurebotProvider
24
+ from bertrend.llm_utils.newsletter_features import generate_newsletter, md2html
25
+
26
+ COLUMN_URL = "url"
27
+ MIN_TEXT_LENGTH = 150
28
+ EMBEDDING_MODEL_NAME = "dangvantuan/sentence-camembert-large"
29
+ TOP_N_WORDS = 5
30
+ # EMBEDDING_MODEL_NAME = "antoinelouis/biencoder-camembert-base-mmarcoFR"
31
+
32
+ css_style = Path(inspect.getfile(generate_newsletter)).parent / "newsletter.css"
33
+
34
+ if "topic_detection_disabled" not in st.session_state:
35
+ st.session_state.topic_detection_disabled = False
36
+ if "newsletter_disabled" not in st.session_state:
37
+ st.session_state.newsletter_disabled = False
38
+ if "import_expanded" not in st.session_state:
39
+ st.session_state.import_expanded = True
40
+ if "st.session_state.topic_expanded" not in st.session_state:
41
+ st.session_state.topic_expanded = True
42
+
43
+
44
+ @st.cache_data
45
+ def parse_data_from_files(files: list[UploadedFile]) -> pd.DataFrame:
46
+ """Read a list of Excel files and return a single dataframe containing the data"""
47
+ dataframes = []
48
+
49
+ with TemporaryDirectory() as tmpdir:
50
+ for f in files:
51
+ with open(tmpdir + "/" + f.name, "wb") as tmp_file:
52
+ tmp_file.write(f.getvalue())
53
+ print(tmp_file.name)
54
+
55
+ if tmp_file is not None:
56
+ with st.spinner(f"Analyse des articles de: {f.name}"):
57
+ provider = CurebotProvider(curebot_export_file=Path(tmp_file.name))
58
+ articles = provider.get_articles()
59
+ articles_path = Path(tmpdir) / (f.name + ".jsonl")
60
+ provider.store_articles(articles, articles_path)
61
+ df = (
62
+ load_data(articles_path)
63
+ .sort_values(by=TIMESTAMP_COLUMN, ascending=False)
64
+ .reset_index(drop=True)
65
+ .reset_index()
66
+ )
67
+ dataframes.append(df)
68
+
69
+ # Concat all dataframes
70
+ df_concat = pd.concat(dataframes, ignore_index=True)
71
+ df_concat = df_concat.drop_duplicates(subset=COLUMN_URL, keep="first")
72
+ return df_concat
73
+
74
+
75
+ @st.cache_data
76
+ def parse_data_from_feed(feed_url):
77
+ """Return a single dataframe containing the data obtained from the feed"""
78
+ with TemporaryDirectory() as tmpdir:
79
+ with st.spinner(f"Analyse des articles de: {feed_url}"):
80
+ provider = CurebotProvider(feed_url=feed_url)
81
+ articles = provider.get_articles()
82
+ articles_path = Path(tmpdir) / "feed.jsonl"
83
+ provider.store_articles(articles, articles_path)
84
+ return (
85
+ load_data(articles_path)
86
+ .sort_values(by=TIMESTAMP_COLUMN, ascending=False)
87
+ .reset_index(drop=True)
88
+ .reset_index()
89
+ )
90
+
91
+
92
+ def split_data():
93
+ st.session_state["df_split"] = (
94
+ enhanced_split_df_by_paragraphs(st.session_state["df"])
95
+ .drop("index", axis=1)
96
+ .sort_values(
97
+ by=TIMESTAMP_COLUMN,
98
+ ascending=False,
99
+ )
100
+ .reset_index(drop=True)
101
+ .reset_index()
102
+ )
103
+
104
+ # Clean dataset using min_text_length
105
+ st.session_state["df_split"] = clean_dataset(
106
+ st.session_state["df_split"],
107
+ MIN_TEXT_LENGTH,
108
+ )
109
+
110
+
111
+ def train_model():
112
+ (
113
+ st.session_state["topic_model"],
114
+ st.session_state["topics"],
115
+ _,
116
+ _,
117
+ _,
118
+ _,
119
+ ) = train_BERTopic(
120
+ full_dataset=st.session_state["df_split"],
121
+ indices=None,
122
+ embedding_model_name=EMBEDDING_MODEL_NAME,
123
+ use_cache=False,
124
+ top_n_words=TOP_N_WORDS,
125
+ )
126
+
127
+
128
+ @st.experimental_dialog("Newsletter preview", width="large")
129
+ def preview_newsletter():
130
+ content = md2html(st.session_state["final_newsletter"], css_style=css_style)
131
+ st.html(content)
132
+
133
+
134
+ def import_data():
135
+ with st.expander(
136
+ "**Import des données de Curebot**", expanded=st.session_state.import_expanded
137
+ ):
138
+ # uploader
139
+ st.text_input(
140
+ "URL du flux ATOM / RSS",
141
+ value=(
142
+ st.session_state["curebot_rss_url"]
143
+ if "curebot_rss_url" in st.session_state
144
+ else ""
145
+ ),
146
+ key="curebot_rss_url",
147
+ help="Saisir le l'URL complète du flux Curebot à importer (par ex. https://api-a1.beta.curebot.io/v1/atom-feed/smartfolder/a5b14e159caa4cb5967f94e84640f602)",
148
+ )
149
+ uploaded_files = st.file_uploader(
150
+ "Fichiers Excel (format Curebot .xlsx)",
151
+ accept_multiple_files=True,
152
+ help="Glisser/déposer dans cette zone les exports Curebot au format Excel",
153
+ )
154
+
155
+ # check content
156
+ if uploaded_files or "curebot_rss_url" in st.session_state:
157
+ if uploaded_files:
158
+ st.session_state["df"] = parse_data_from_files(uploaded_files)
159
+ elif st.session_state["curebot_rss_url"]:
160
+ st.session_state["df"] = parse_data_from_feed(
161
+ st.session_state["curebot_rss_url"]
162
+ )
163
+
164
+ # split and clean data
165
+ if "df" in st.session_state:
166
+ split_data()
167
+ logger.info(f"Size of dataset: {len(st.session_state['df_split'])}")
168
+
169
+
170
+ def display_data():
171
+ if "df" in st.session_state:
172
+ st.session_state.import_expanded = False
173
+ # Affichage du contenu du fichier Excel
174
+ with st.expander("**Contenu des données**", expanded=False):
175
+ st.dataframe(st.session_state["df"])
176
+
177
+
178
+ def detect_topics():
179
+ if "df_split" in st.session_state:
180
+ st.session_state.import_expanded = False
181
+ with st.expander(
182
+ "**Détection de topics**", expanded=st.session_state.topic_expanded
183
+ ):
184
+ # Topic detection
185
+ col1, col2 = st.columns(2)
186
+ with col1:
187
+ st.button(
188
+ "Détection des topics",
189
+ on_click=train_model,
190
+ key="topic_detection",
191
+ type="primary",
192
+ disabled=st.session_state.topic_detection_disabled,
193
+ )
194
+ with col2:
195
+ if "topic_model" in st.session_state:
196
+ st.info(
197
+ f"Nombre de topics: {len(st.session_state['topic_model'].get_topic_info()) - 1}"
198
+ )
199
+
200
+
201
+ def newsletter_creation():
202
+ # Newsletter creation
203
+ if "topic_model" in st.session_state.keys():
204
+ st.session_state.topic_expanded = False
205
+ with st.expander("**Création de la newsletters**", expanded=True):
206
+ # st.session_state.topic_detection_disabled = True
207
+ generation_button = st.button(
208
+ "Génération de newsletters",
209
+ on_click=create_newsletter,
210
+ type="primary",
211
+ disabled=st.session_state.newsletter_disabled,
212
+ )
213
+
214
+ # Edit manually newsletters
215
+ if "newsletters" in st.session_state.keys():
216
+ st.text_area(
217
+ "Contenu éditable de la newsletters (faire CTRL+ENTREE pour prendre en compte les modifications)",
218
+ value=(
219
+ st.session_state["newsletters"]
220
+ if (
221
+ "final_newsletter" not in st.session_state
222
+ or generation_button
223
+ )
224
+ else st.session_state["final_newsletter"]
225
+ ),
226
+ height=400,
227
+ key="final_newsletter",
228
+ )
229
+
230
+ if "final_newsletter" in st.session_state:
231
+ col1, col2 = st.columns(2)
232
+ with col1:
233
+ st.button("Preview", on_click=preview_newsletter)
234
+ with col2:
235
+ # Newsletter download
236
+ st.download_button(
237
+ "Téléchargement",
238
+ file_name="newsletters.html",
239
+ data=md2html(
240
+ st.session_state["final_newsletter"],
241
+ css_style=css_style,
242
+ ),
243
+ type="primary",
244
+ )
245
+
246
+
247
+ def main_page():
248
+ """Main page rendering"""
249
+ # title
250
+ st.title("Wattelse Veille & Analyse")
251
+ import_data()
252
+ display_data()
253
+ detect_topics()
254
+ newsletter_creation()
255
+
256
+
257
+ def options():
258
+ with st.sidebar:
259
+ st.title("Réglages")
260
+
261
+ st.slider(
262
+ "Nombre max de topics",
263
+ min_value=1,
264
+ max_value=10,
265
+ value=5,
266
+ key="newsletter_nb_topics",
267
+ )
268
+
269
+ st.slider(
270
+ "Nombre max d'articles par topics",
271
+ min_value=1,
272
+ max_value=10,
273
+ value=5,
274
+ key="newsletter_nb_docs",
275
+ )
276
+
277
+ st.slider(
278
+ "Longueur des synthèses (# phrases)",
279
+ min_value=1,
280
+ max_value=10,
281
+ value=4,
282
+ key="nb_sentences",
283
+ )
284
+
285
+ st.selectbox(
286
+ "Moteur de résumé",
287
+ ("gpt-4o-mini", "gpt-4o", "gpt-3.5-turbo"),
288
+ key="openai_model_name",
289
+ )
290
+
291
+
292
+ def main():
293
+ options()
294
+ main_page()
295
+
296
+
297
+ # Main
298
+ main()
@@ -0,0 +1,24 @@
1
+ TOPIC_DESCRIPTION_SYSTEM_PROMPT = """
2
+ Vous êtes expert en veille d'actualité et en anaylse thématique.
3
+ Dans le contexte d'une analyse d'articles de presse, plusieurs articles ont été regroupés en un même thème.
4
+ Votre tâche est de générer un titre pour ce thème sur la base des articles qui appartiennent à ce thème.
5
+ A partir de la liste des articles fournie (titre et contenu), rédigez un titre pour le thème.
6
+ Le titre doit être concis (maximum 5 mots) et représenter au mieux la spécificité du thème.
7
+ Répondez sous la forme d'un JSON suivant le format ci-dessous :
8
+ {
9
+ "titre": "<votre titre du thème>"
10
+ }
11
+ """
12
+
13
+ TOPIC_SUMMARY_SYSTEM_PROMPT = """
14
+ Vous êtes expert en veille d'actualité et en anaylse thématique.
15
+ Dans le contexte d'une analyse d'articles de presse, plusieurs articles ont été regroupés en un même thème.
16
+ Votre tâche est de générer un résumé pour ce thème sur la base des articles qui appartiennent à ce thème.
17
+ A partir de la liste des articles fournie (titre et contenu), rédigez un résumé pour le thème.
18
+ Le résumé doit être concis (maximum 100 mots) et représenter au mieux la spécificité du thème.
19
+ Il doit pas commencer par "Les articles parlent de..." ou équivalent et doit être écrit dans un style journalistique.
20
+ Répondez sous la forme d'un JSON suivant le format ci-dessous :
21
+ {
22
+ "résumé": "<votre résumé du thème>"
23
+ }
24
+ """
@@ -0,0 +1,8 @@
1
+ #
2
+ # Copyright (c) 2024, RTE (https://www.rte-france.com)
3
+ # See AUTHORS.txt
4
+ # SPDX-License-Identifier: MPL-2.0
5
+ # This file is part of BERTrend.
6
+ #
7
+
8
+ OPENAI_API_KEY=$OPENAI_API_KEY_VEILLE CUDA_VISIBLE_DEVICES=0 streamlit run --theme.primaryColor royalblue --server.port 8686 veille_analyse.py
@@ -0,0 +1,150 @@
1
+ import pandas as pd
2
+ from loguru import logger
3
+ import streamlit as st
4
+
5
+ from bertrend_apps.exploration.curebot.app_utils import (
6
+ TAGS_COLUMN,
7
+ TEXT_COLUMN,
8
+ TIMESTAMP_COLUMN,
9
+ TITLE_COLUMN,
10
+ TOP_N_WORDS,
11
+ concat_data_from_files,
12
+ fit_bertopic,
13
+ get_embeddings,
14
+ get_improved_topic_description,
15
+ )
16
+
17
+
18
+ def show() -> None:
19
+ # Data uploading component
20
+ upload_data()
21
+
22
+ # Load data into dataframe
23
+ if st.session_state.get("uploaded_files"):
24
+ try:
25
+ preprocess_data()
26
+ except Exception as e:
27
+ logger.error(e)
28
+ st.error(
29
+ f"Erreur lors du chargement des données. Vérifiez que vos données respectent le format Curebot attendu."
30
+ )
31
+
32
+ # If data is loaded
33
+ if "df" in st.session_state:
34
+ # Show data
35
+ with st.expander("Voir les données", expanded=False):
36
+ st.write(st.session_state["df"])
37
+
38
+ # Button to train model
39
+ if st.button("Détecter les sujets", type="primary"):
40
+ with st.spinner("Détection des sujets..."):
41
+ # Train model
42
+ train_model()
43
+
44
+ # If topic model is trained, update df with topics and llm description
45
+ if "topic_model" in st.session_state:
46
+ st.session_state["df"]["topics"] = st.session_state["topics"]
47
+ with st.spinner("Génération des titres des sujets..."):
48
+ st.session_state["topics_info"]["llm_description"] = (
49
+ get_improved_topic_description(
50
+ st.session_state["df"], st.session_state["topics_info"]
51
+ )
52
+ )
53
+ st.success("Sujets détectés, voir l'onglet résultats.")
54
+
55
+
56
+ def upload_data() -> None:
57
+ """
58
+ Data uploading component for Curebot format.
59
+ Sets in sessions_state:
60
+ - "uploaded_files": list of uploaded files
61
+ """
62
+ # Excel files input
63
+ st.session_state["uploaded_files"] = st.file_uploader(
64
+ "Fichiers Excel au format rapport Curebot `.xlsx`",
65
+ accept_multiple_files=True,
66
+ help="Glisser/déposer dans cette zone les exports Curebot au format Excel",
67
+ )
68
+
69
+
70
+ def preprocess_data() -> None:
71
+ """
72
+ Preprocess data from uploaded files.
73
+ Sets in session_state:
74
+ - "df": dataframe with all data
75
+ """
76
+ # Concatenate uploaded Excel files into a single dataframe
77
+ df = concat_data_from_files(st.session_state["uploaded_files"])
78
+
79
+ # Remove duplicates based on title and text columns
80
+ df = df.drop_duplicates(subset=[TITLE_COLUMN, TEXT_COLUMN]).reset_index(drop=True)
81
+
82
+ # Remove rows where text is empty
83
+ df = df[df[TEXT_COLUMN].notna()].reset_index(drop=True)
84
+
85
+ # Sort df by date
86
+ df[TIMESTAMP_COLUMN] = pd.to_datetime(df[TIMESTAMP_COLUMN])
87
+ df = df.sort_values(by=TIMESTAMP_COLUMN, ascending=False).reset_index(drop=True)
88
+
89
+ st.session_state["df"] = df
90
+
91
+
92
+ def train_model() -> None:
93
+ """
94
+ Train a BERTopic model based on provided data.
95
+ Sets in session_state:
96
+ - "embeddings": embeddings of the dataset
97
+ - "topic_model": trained BERTopic model
98
+ - "topics": topics extracted by the model
99
+ - "topics_info": information about the topics
100
+ """
101
+ # Get texts list and embeddings
102
+ texts_list = st.session_state["df"][TEXT_COLUMN].tolist()
103
+ embeddings = get_embeddings(texts_list)
104
+
105
+ # If use_tags is True, get tags from dataframe
106
+ if st.session_state["use_tags"]:
107
+ # Convert tags to string
108
+ st.session_state["df"][TAGS_COLUMN] = st.session_state["df"][
109
+ TAGS_COLUMN
110
+ ].astype(str)
111
+
112
+ # Get zeroshot_topic_list from tags
113
+ zeroshot_topic_list = (
114
+ st.session_state["df"][TAGS_COLUMN]
115
+ .fillna("")
116
+ .str.findall(r"#\w+")
117
+ .explode()
118
+ .unique()
119
+ )
120
+
121
+ # Remove # and _ from tags and convert to string
122
+ zeroshot_topic_list = [
123
+ str(tag).replace("#", "").replace("_", " ")
124
+ for tag in zeroshot_topic_list
125
+ if tag
126
+ ]
127
+ # Else, set zeroshot_topic_list to None
128
+ else:
129
+ zeroshot_topic_list = None
130
+
131
+ # Train topic model
132
+ bertopic, topics = fit_bertopic(
133
+ texts_list,
134
+ embeddings,
135
+ st.session_state["min_articles_per_topic"],
136
+ zeroshot_topic_list=zeroshot_topic_list,
137
+ )
138
+
139
+ # Set session_state
140
+ st.session_state["topic_model"] = bertopic
141
+ st.session_state["topics"] = topics
142
+
143
+ topic_info = bertopic.get_topic_info()
144
+ topic_info["Representation"] = topic_info["Representation"].apply(
145
+ lambda x: x[:TOP_N_WORDS]
146
+ )
147
+
148
+ st.session_state["topics_info"] = topic_info[
149
+ topic_info["Topic"] != -1
150
+ ] # exclude -1 topic from topic list
@@ -0,0 +1,95 @@
1
+ import streamlit as st
2
+
3
+ from bertrend_apps.exploration.curebot.app_utils import (
4
+ URL_COLUMN,
5
+ display_representative_documents,
6
+ display_source_distribution,
7
+ get_website_name,
8
+ )
9
+
10
+
11
+ def show() -> None:
12
+ # Check if a model is trained
13
+ if "topic_model" not in st.session_state:
14
+ st.warning("Veuillez ajouter des données et lancer la détection des sujets.")
15
+ else:
16
+ # Show sidebar with topic list
17
+ show_topic_list()
18
+
19
+ # Display selected topic info
20
+ display_topic_info(st.session_state.get("selected_topic_number", 0))
21
+
22
+
23
+ def show_topic_list():
24
+ """
25
+ Show topic list in the sidebar.
26
+ Each topic is a button that, when clicked, sets session_state:
27
+ - "selected_topic_number": topic number, this topic is displayed in results tab.
28
+ """
29
+ with st.sidebar:
30
+ with st.expander("Sujets détectés", expanded=True):
31
+ # Topics list
32
+ for _, topic in st.session_state["topics_info"].iterrows():
33
+ # Display button for each topic
34
+ topic_number = topic["Topic"]
35
+ button_title = topic["llm_description"]
36
+ st.button(
37
+ str(topic_number + 1) + " - " + button_title,
38
+ use_container_width=True,
39
+ on_click=set_topic_selection,
40
+ args=(topic_number,),
41
+ )
42
+
43
+
44
+ def set_topic_selection(topic_number: int):
45
+ """Set "selected_topic_number" in the session state."""
46
+ st.session_state["selected_topic_number"] = topic_number
47
+
48
+
49
+ def display_topic_info(topic_number: int):
50
+ """Display "selected_topic_number" associated topic information."""
51
+ # Filter df to get only the selected topic
52
+ selected_topic_df = st.session_state["df"][
53
+ st.session_state["df"]["topics"] == topic_number
54
+ ]
55
+
56
+ # Get topic info
57
+ docs_count = len(selected_topic_df)
58
+ key_words = st.session_state["topics_info"].iloc[topic_number]["Representation"]
59
+ llm_description = st.session_state["topics_info"].iloc[topic_number][
60
+ "llm_description"
61
+ ]
62
+
63
+ # Display topic info
64
+ st.write(f"# {llm_description}")
65
+ st.write(f"### {docs_count} documents")
66
+ st.markdown(f"### #{' #'.join(key_words)}")
67
+
68
+ # Get unique sources
69
+ sources = selected_topic_df[URL_COLUMN].apply(get_website_name).unique()
70
+
71
+ # Multi-select for sources
72
+ selected_sources = st.multiselect(
73
+ "Sélectionner les sources à afficher :",
74
+ options=["All"] + sorted(list(sources)),
75
+ default=["All"],
76
+ )
77
+
78
+ # Create two columns
79
+ col21, col22 = st.columns([0.3, 0.7])
80
+
81
+ with col21:
82
+ # Pass the full representative_df to display_source_distribution
83
+ display_source_distribution(selected_topic_df, selected_sources)
84
+
85
+ with col22:
86
+ # Filter the dataframe only for document display
87
+ if "All" not in selected_sources:
88
+ filtered_df = selected_topic_df[
89
+ selected_topic_df[URL_COLUMN]
90
+ .apply(get_website_name)
91
+ .isin(selected_sources)
92
+ ]
93
+ else:
94
+ filtered_df = selected_topic_df
95
+ display_representative_documents(filtered_df)
@@ -0,0 +1,125 @@
1
+ import locale
2
+ from pathlib import Path
3
+ import jinja2
4
+ import streamlit as st
5
+ from bertrend.llm_utils.newsletter_features import md2html
6
+ from bertrend_apps.exploration.curebot.app_utils import (
7
+ NEWSLETTER_TEMPLATE,
8
+ create_newsletter,
9
+ )
10
+
11
+ # Set french locale
12
+ locale.setlocale(locale.LC_ALL, "fr_FR.UTF-8")
13
+
14
+ CSS_STYLE = (
15
+ Path(__file__).parent.parent.parent.parent.parent
16
+ / "bertrend/llm_utils/newsletter.css"
17
+ )
18
+
19
+
20
+ def show() -> None:
21
+ if "topic_model" not in st.session_state:
22
+ st.warning("Veuillez ajouter des données et lancer la détection des sujets.")
23
+ else:
24
+ # Show newsletter parameters
25
+ newsletter_parameters()
26
+
27
+ # Columns for buttons
28
+ col1, col2, col3 = st.columns([1, 1, 1])
29
+
30
+ # Show newsletter creation button
31
+ with col1:
32
+ if st.button("Créer la newsletter"):
33
+ with st.spinner("Création de la newsletter..."):
34
+ get_newsletter()
35
+
36
+ # Show newsletter edition button
37
+ with col2:
38
+ st.button(
39
+ "Éditer",
40
+ on_click=edit_newsletter,
41
+ disabled=not "newsletter_text" in st.session_state,
42
+ )
43
+
44
+ # Show newsletter download button
45
+ with col3:
46
+ # If newsletter created, set it as data to download
47
+ if "newsletter_text" in st.session_state:
48
+ data = md2html(st.session_state["newsletter_text"], css_style=CSS_STYLE)
49
+ # Else, set data to empty string
50
+ else:
51
+ data = ""
52
+
53
+ # Download button
54
+ st.download_button(
55
+ "Télécharger",
56
+ file_name="newsletters.html",
57
+ mime="text/html",
58
+ data=data,
59
+ disabled=not "newsletter_text" in st.session_state,
60
+ )
61
+
62
+ # Show newsletter
63
+ if "newsletter_text" in st.session_state:
64
+ st.write(st.session_state["newsletter_text"], unsafe_allow_html=True)
65
+
66
+
67
+ def newsletter_parameters() -> None:
68
+ """
69
+ Show newsletter parameters:
70
+ - Number of topics to include in the newsletter
71
+ - Number of articles per topic to include in the newsletter
72
+ """
73
+ col31, col32 = st.columns([1, 1])
74
+ with col31:
75
+ # Select number of topics to include in the newsletter
76
+ st.slider(
77
+ "Nombre de sujets",
78
+ 1,
79
+ len(st.session_state["topics_info"]),
80
+ value=min(len(st.session_state["topics"]), 3),
81
+ key="newsletter_nb_topics",
82
+ )
83
+ with col32:
84
+ # Select number of articles per topic to include in the newsletter
85
+ st.slider(
86
+ "Nombre d'articles par sujet",
87
+ 1,
88
+ 10,
89
+ value=4,
90
+ key="newsletter_nb_articles_per_topic",
91
+ )
92
+
93
+
94
+ def get_newsletter() -> None:
95
+ """
96
+ Create a newsletter based on the selected topics and articles.
97
+ Sets in sessions_state:
98
+ - newsletter_dict: a dictionary containing the newsletter data
99
+ - newsletter_text: the newsletter text
100
+ """
101
+ # Newsletter dict
102
+ st.session_state["newsletter_dict"] = create_newsletter(
103
+ st.session_state["df"],
104
+ st.session_state["topics_info"],
105
+ st.session_state["newsletter_nb_topics"],
106
+ st.session_state["newsletter_nb_articles_per_topic"],
107
+ )
108
+
109
+ # Newsletter text
110
+ template = jinja2.Template(NEWSLETTER_TEMPLATE)
111
+ st.session_state["newsletter_text"] = template.render(
112
+ st.session_state["newsletter_dict"]
113
+ )
114
+
115
+
116
+ @st.dialog("Éditer la newsletter", width="large")
117
+ def edit_newsletter() -> None:
118
+ edited_newsltter = st.text_area(
119
+ "",
120
+ value=st.session_state["newsletter_text"],
121
+ height=500,
122
+ )
123
+ st.session_state["newsletter_text"] = edited_newsltter
124
+ if st.button("Enregistrer", type="primary"):
125
+ st.rerun()