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,455 @@
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
+
6
+ import io
7
+ import re
8
+ import zipfile
9
+
10
+ import pandas as pd
11
+ import streamlit as st
12
+ import plotly.graph_objects as go
13
+ from datetime import timedelta
14
+ from urllib.parse import urlparse
15
+
16
+ from bertopic import BERTopic
17
+
18
+ from bertrend import OUTPUT_PATH
19
+ from bertrend.demos.demos_utils.icons import ERROR_ICON, WARNING_ICON
20
+ from bertrend.demos.demos_utils.state_utils import (
21
+ restore_widget_state,
22
+ SessionStateManager,
23
+ )
24
+ from bertrend.demos.topic_analysis.messages import (
25
+ NO_DOCUMENT_FOR_TOPIC,
26
+ TRAIN_MODEL_FIRST_ERROR,
27
+ )
28
+ from bertrend.demos.weak_signals.visualizations_utils import PLOTLY_BUTTON_SAVE_CONFIG
29
+ from bertrend.topic_analysis.topic_description import generate_topic_description
30
+ from bertrend.utils.data_loading import TIMESTAMP_COLUMN, TEXT_COLUMN, URL_COLUMN
31
+ from bertrend.topic_analysis.representative_docs import get_most_representative_docs
32
+ from bertrend.demos.topic_analysis.app_utils import (
33
+ compute_topics_over_time,
34
+ print_docs_for_specific_topic,
35
+ )
36
+ from bertrend.topic_analysis.visualizations import plot_topics_over_time
37
+
38
+ # Constants
39
+ EXPORT_BASE_FOLDER = OUTPUT_PATH / "exported_topics"
40
+ EXPORT_BASE_FOLDER.mkdir(parents=True, exist_ok=True)
41
+
42
+
43
+ def check_model_and_prepare_topics():
44
+ """
45
+ Check if a model is trained and prepare topics over time if necessary.
46
+ """
47
+ if "topic_model" not in st.session_state:
48
+ st.error(TRAIN_MODEL_FIRST_ERROR, icon=ERROR_ICON)
49
+ st.stop()
50
+
51
+ if (
52
+ "topics_over_time" not in st.session_state
53
+ and TIMESTAMP_COLUMN in st.session_state["time_filtered_df"]
54
+ ):
55
+ st.session_state["topics_over_time"] = compute_topics_over_time(
56
+ st.session_state["topic_model"],
57
+ st.session_state["time_filtered_df"],
58
+ nr_bins=10,
59
+ )
60
+
61
+
62
+ def set_topic_selection(selected_topic_number: int):
63
+ """Set the selected topic number in the session state."""
64
+ st.session_state["selected_topic_number"] = selected_topic_number
65
+
66
+
67
+ def find_similar_topic():
68
+ """Find a topic similar to the search terms."""
69
+ similar_topics, _ = st.session_state["topic_model"].find_topics(
70
+ st.session_state["search_terms"], top_n=1
71
+ )
72
+ st.session_state["selected_topic_number"] = similar_topics[0]
73
+
74
+
75
+ def display_sidebar():
76
+ """Display the sidebar with search functionality and topic list."""
77
+ with st.sidebar:
78
+ # Search bar
79
+ search_terms = st.text_input(
80
+ "Search topic", on_change=find_similar_topic, key="search_terms"
81
+ )
82
+
83
+ # Topics list
84
+ for index, topic in st.session_state["topics_info"].iterrows():
85
+ topic_number = topic["Topic"]
86
+ topic_words = topic["Representation"][:3]
87
+ button_title = f"{topic_number} - {' | '.join(topic_words)}"
88
+ if "new_topics" in st.session_state:
89
+ new_docs_number = st.session_state["new_topics"].count(topic_number)
90
+ button_title += f" :red[+{new_docs_number}]"
91
+ st.button(
92
+ button_title,
93
+ use_container_width=True,
94
+ on_click=set_topic_selection,
95
+ args=(topic_number,),
96
+ )
97
+
98
+
99
+ def display_topic_info():
100
+ """Display the main topic information."""
101
+ topic_docs_number = st.session_state["topics_info"].iloc[
102
+ st.session_state["selected_topic_number"]
103
+ ]["Count"]
104
+ topic_words = st.session_state["topics_info"].iloc[
105
+ st.session_state["selected_topic_number"]
106
+ ]["Representation"]
107
+
108
+ st.write(
109
+ f"## Topic {st.session_state['selected_topic_number']} : {topic_docs_number} documents"
110
+ )
111
+ st.markdown(f"### #{' #'.join(topic_words)}")
112
+
113
+
114
+ def plot_topic_over_time():
115
+ """Plot the topic evolution over time."""
116
+ if TIMESTAMP_COLUMN in st.session_state["time_filtered_df"]:
117
+ if "new_topics_over_time" not in st.session_state:
118
+ st.plotly_chart(
119
+ plot_topics_over_time(
120
+ st.session_state["topics_over_time"],
121
+ str(st.session_state["selected_topic_number"]),
122
+ st.session_state["topic_model"],
123
+ ),
124
+ config=PLOTLY_BUTTON_SAVE_CONFIG,
125
+ use_container_width=True,
126
+ )
127
+ else:
128
+ st.plotly_chart(
129
+ plot_topics_over_time(
130
+ st.session_state["new_topics_over_time"],
131
+ str(st.session_state["selected_topic_number"]),
132
+ st.session_state["topic_model"],
133
+ time_split=st.session_state["time_filtered_df"][
134
+ TIMESTAMP_COLUMN
135
+ ].max(),
136
+ ),
137
+ config=PLOTLY_BUTTON_SAVE_CONFIG,
138
+ use_container_width=True,
139
+ )
140
+
141
+
142
+ def get_representative_documents(top_n_docs: int):
143
+ """Get representative documents for the selected topic."""
144
+ if st.session_state["split_type"] in ["yes", "enhanced"]:
145
+ return get_most_representative_docs(
146
+ st.session_state["topic_model"],
147
+ st.session_state["initial_df"],
148
+ st.session_state["topics"],
149
+ df_split=st.session_state["time_filtered_df"],
150
+ topic_number=st.session_state["selected_topic_number"],
151
+ top_n_docs=top_n_docs,
152
+ )
153
+ else:
154
+ docs_by_cluster_probability = get_most_representative_docs(
155
+ st.session_state["topic_model"],
156
+ st.session_state["time_filtered_df"],
157
+ st.session_state["topics"],
158
+ topic_number=st.session_state["selected_topic_number"],
159
+ mode="cluster_probability",
160
+ top_n_docs=top_n_docs,
161
+ )
162
+ docs_by_ctfidf_similarity = get_most_representative_docs(
163
+ st.session_state["topic_model"],
164
+ st.session_state["time_filtered_df"],
165
+ st.session_state["topics"],
166
+ topic_number=st.session_state["selected_topic_number"],
167
+ mode="ctfidf_representation",
168
+ top_n_docs=top_n_docs,
169
+ )
170
+ return pd.concat(
171
+ [docs_by_cluster_probability, docs_by_ctfidf_similarity]
172
+ ).drop_duplicates()
173
+
174
+
175
+ def display_source_distribution(
176
+ representative_df: pd.DataFrame, selected_sources: list[str]
177
+ ):
178
+ """Display the distribution of sources in a pie chart."""
179
+
180
+ source_counts = representative_df[URL_COLUMN].apply(get_website_name).value_counts()
181
+
182
+ # Create a list to store the 'pull' values for each slice
183
+ pull = []
184
+
185
+ # Determine which slices should be pulled out
186
+ for source in source_counts.index:
187
+ if source in selected_sources and "All" not in selected_sources:
188
+ pull.append(0.2)
189
+ else:
190
+ pull.append(0)
191
+
192
+ fig = go.Figure(
193
+ data=[
194
+ go.Pie(
195
+ labels=source_counts.index,
196
+ values=source_counts.values,
197
+ pull=pull,
198
+ textposition="inside",
199
+ textinfo="percent+label",
200
+ hole=0.3,
201
+ )
202
+ ]
203
+ )
204
+
205
+ fig.update_layout(
206
+ showlegend=False, height=600, width=500, margin=dict(t=0, b=0, l=0, r=0)
207
+ )
208
+
209
+ st.plotly_chart(fig, config=PLOTLY_BUTTON_SAVE_CONFIG, use_container_width=True)
210
+
211
+
212
+ def get_website_name(url):
213
+ """Extract website name from URL, handling None, NaN, and invalid URLs."""
214
+ if pd.isna(url) or url is None or isinstance(url, float):
215
+ return "Unknown Source"
216
+ try:
217
+ return (
218
+ urlparse(str(url)).netloc.replace("www.", "").split(".")[0]
219
+ or "Unknown Source"
220
+ )
221
+ except:
222
+ return "Unknown Source"
223
+
224
+
225
+ def display_representative_documents(filtered_df: pd.DataFrame):
226
+ """Display representative documents for the selected topic."""
227
+ with st.container(border=False, height=600):
228
+ for _, doc in filtered_df.iterrows():
229
+ website_name = get_website_name(doc.url)
230
+ date = doc.timestamp.strftime("%A %d %b %Y %H:%M:%S")
231
+ snippet = doc.text[:200] + "..." if len(doc.text) > 150 else doc.text
232
+
233
+ content = f"""**{doc.title}**\n\n{date} | {'Unknown Source' if website_name == 'Unknown Source' else website_name}\n\n{snippet}"""
234
+
235
+ if website_name != "Unknown Source":
236
+ st.link_button(content, doc.url)
237
+ else:
238
+ st.markdown(content)
239
+
240
+
241
+ def display_new_documents():
242
+ """Display new documents if remaining data is processed."""
243
+ if "new_topics_over_time" in st.session_state:
244
+ st.write("## New documents")
245
+ print_docs_for_specific_topic(
246
+ st.session_state["remaining_df"],
247
+ st.session_state["new_topics"],
248
+ st.session_state["selected_topic_number"],
249
+ )
250
+
251
+
252
+ def create_topic_documents(
253
+ filtered_df: pd.DataFrame, topic_model: BERTopic, granularity_days: int
254
+ ):
255
+ """Create topic documents grouped by a given time granularity."""
256
+ topic_docs = filtered_df.sort_values(by=TIMESTAMP_COLUMN)
257
+ documents = []
258
+ folder_name = None
259
+ if not topic_docs.empty:
260
+ topic_number = st.session_state["selected_topic_number"]
261
+ topic_words = [word for word, _ in topic_model.get_topic(topic_number)]
262
+
263
+ folder_name = (
264
+ f"topic_{topic_number}_{' '.join(topic_words[:3])}_{granularity_days}days"
265
+ )
266
+ folder_name = re.sub(r"[^\w\-_\. ]", "_", folder_name)
267
+
268
+ start_date = topic_docs[TIMESTAMP_COLUMN].min()
269
+ end_date = topic_docs[TIMESTAMP_COLUMN].max()
270
+ current_date = start_date
271
+
272
+ while current_date <= end_date:
273
+ next_date = current_date + timedelta(days=granularity_days)
274
+ period_docs = topic_docs[
275
+ (topic_docs[TIMESTAMP_COLUMN] >= current_date)
276
+ & (topic_docs[TIMESTAMP_COLUMN] < next_date)
277
+ ]
278
+
279
+ if not period_docs.empty:
280
+ file_name = f"{current_date.strftime('%Y%m%d')}-{(next_date - timedelta(days=1)).strftime('%Y%m%d')}.txt"
281
+ content = "\n\n".join(period_docs[TEXT_COLUMN])
282
+ documents.append((file_name, content))
283
+
284
+ current_date = next_date
285
+
286
+ return folder_name, documents
287
+
288
+
289
+ def _display_topic_description(filtered_df: pd.DataFrame):
290
+ """Display a human-readable description of the selected topic using a LLM."""
291
+ if st.button(
292
+ "Generate a short description of the topic",
293
+ type="primary",
294
+ use_container_width=True,
295
+ ):
296
+ with st.spinner("Génération de la description en cours..."):
297
+ language_code = (
298
+ "fr" if SessionStateManager.get("language") == "French" else "en"
299
+ )
300
+ gpt_description = generate_topic_description(
301
+ st.session_state["topic_model"],
302
+ st.session_state["selected_topic_number"],
303
+ filtered_df,
304
+ language_code=language_code,
305
+ )
306
+ with st.container(border=True):
307
+ st.markdown(f"### {gpt_description.title}\n{gpt_description.description}")
308
+
309
+
310
+ def main():
311
+ """Main function to run the Streamlit topic_analysis."""
312
+ check_model_and_prepare_topics()
313
+
314
+ st.title("Topics exploration")
315
+
316
+ display_sidebar()
317
+
318
+ if "selected_topic_number" not in st.session_state:
319
+ st.stop()
320
+
321
+ col1, col2 = st.columns([0.3, 0.7])
322
+ with col1:
323
+ display_topic_info()
324
+ with col2:
325
+ plot_topic_over_time()
326
+
327
+ col1, col2 = st.columns(2)
328
+ with col1:
329
+ # Number of articles to display
330
+ top_n_docs = st.number_input(
331
+ "Number of articles to display",
332
+ min_value=1,
333
+ max_value=st.session_state["topics_info"].iloc[
334
+ st.session_state["selected_topic_number"]
335
+ ]["Count"],
336
+ value=st.session_state["topics_info"].iloc[
337
+ st.session_state["selected_topic_number"]
338
+ ]["Count"],
339
+ step=1,
340
+ )
341
+ with col2:
342
+ representative_df = get_representative_documents(top_n_docs)
343
+ representative_df = representative_df.sort_values(
344
+ by="timestamp", ascending=False
345
+ )
346
+
347
+ # Get unique sources
348
+ sources = representative_df[URL_COLUMN].apply(get_website_name).unique()
349
+
350
+ # Multi-select for sources
351
+ selected_sources = st.multiselect(
352
+ "Select the sources to display",
353
+ options=["All"] + list(sources),
354
+ default=["All"],
355
+ )
356
+
357
+ # Create two columns
358
+ col1, col2 = st.columns([0.3, 0.7])
359
+
360
+ with col1:
361
+ # Pass the full representative_df to display_source_distribution
362
+ display_source_distribution(representative_df, selected_sources)
363
+
364
+ with col2:
365
+ # Filter the dataframe only for document display
366
+ if "All" not in selected_sources:
367
+ filtered_df = representative_df[
368
+ representative_df[URL_COLUMN]
369
+ .apply(get_website_name)
370
+ .isin(selected_sources)
371
+ ]
372
+ else:
373
+ filtered_df = representative_df
374
+ display_representative_documents(filtered_df)
375
+
376
+ display_new_documents()
377
+
378
+ # GPT generated topic description
379
+ _display_topic_description(filtered_df)
380
+
381
+ st.divider()
382
+
383
+ # Export configuration
384
+ st.subheader("Export Configuration")
385
+ export_method = st.radio(
386
+ "Choose export method:",
387
+ ("Download as ZIP", "Save to folder"),
388
+ index=0,
389
+ help="Select whether to download documents as a ZIP file or save them directly to a folder on the server.",
390
+ )
391
+
392
+ granularity_days = st.number_input(
393
+ "Granularity (number of days)", min_value=1, value=3, step=1
394
+ )
395
+
396
+ if export_method == "Download as ZIP":
397
+ # Prepare ZIP file
398
+ folder_name, documents = create_topic_documents(
399
+ filtered_df,
400
+ st.session_state["topic_model"],
401
+ granularity_days,
402
+ )
403
+
404
+ if documents:
405
+ zip_buffer = io.BytesIO()
406
+ with zipfile.ZipFile(
407
+ zip_buffer, "a", zipfile.ZIP_DEFLATED, False
408
+ ) as zip_file:
409
+ for file_name, content in documents:
410
+ zip_file.writestr(file_name, content)
411
+
412
+ zip_buffer.seek(0)
413
+ zip_filename = f"{folder_name}.zip"
414
+
415
+ st.download_button(
416
+ label="Export Topic Documents",
417
+ data=zip_buffer,
418
+ file_name=zip_filename,
419
+ mime="application/zip",
420
+ )
421
+ else:
422
+ st.warning(NO_DOCUMENT_FOR_TOPIC, icon=WARNING_ICON)
423
+ else:
424
+ if st.button("Export Topic Documents"):
425
+ folder_name, documents = create_topic_documents(
426
+ filtered_df,
427
+ st.session_state["topic_model"],
428
+ granularity_days,
429
+ )
430
+
431
+ if documents:
432
+ export_folder = EXPORT_BASE_FOLDER / folder_name
433
+ export_folder.mkdir(parents=True, exist_ok=True)
434
+
435
+ for file_name, content in documents:
436
+ file_path = export_folder / file_name
437
+ with open(file_path, "w", encoding="utf-8") as f:
438
+ f.write(content)
439
+
440
+ st.success(
441
+ f"Successfully exported documents to folder: {export_folder}"
442
+ )
443
+ else:
444
+ st.warning(
445
+ NO_DOCUMENT_FOR_TOPIC,
446
+ icon=WARNING_ICON,
447
+ )
448
+
449
+
450
+ # Restore widget state
451
+ restore_widget_state()
452
+ main()
453
+
454
+ # FIXME: The number of documents being displayed per topic corresponds to the paragraphs, it should instead correspond to the number of original articles before splitting.
455
+ # TODO: Granularity to export multiple .txt files in order to use them later with Text2KG hasn't been properly checked (Scenario where granularity is bigger than number of days a topic spans over, or bigger than half... etc)
@@ -0,0 +1,178 @@
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 inspect
6
+
7
+ import pandas as pd
8
+ import streamlit as st
9
+ from pathlib import Path
10
+
11
+ from bertrend.demos.demos_utils.icons import ERROR_ICON
12
+ from bertrend.demos.demos_utils.state_utils import (
13
+ register_widget,
14
+ save_widget_state,
15
+ restore_widget_state,
16
+ register_multiple_widget,
17
+ )
18
+ from bertrend.demos.topic_analysis.messages import TRAIN_MODEL_FIRST_ERROR
19
+ from bertrend.services.summary.abstractive_summarizer import AbstractiveSummarizer
20
+ from bertrend.services.summary.chatgpt_summarizer import GPTSummarizer
21
+ from bertrend.services.summary.extractive_summarizer import (
22
+ ExtractiveSummarizer,
23
+ EnhancedExtractiveSummarizer,
24
+ )
25
+ from bertrend.llm_utils.newsletter_features import generate_newsletter, md2html
26
+
27
+
28
+ # Define summarizer options
29
+ SUMMARIZER_OPTIONS_MAPPER = {
30
+ "GPTSummarizer": GPTSummarizer,
31
+ "AbstractiveSummarizer": AbstractiveSummarizer,
32
+ "ExtractiveSummarizer": ExtractiveSummarizer,
33
+ "EnhancedExtractiveSummarizer": EnhancedExtractiveSummarizer,
34
+ }
35
+
36
+
37
+ def generate_newsletter_wrapper(
38
+ df: pd.DataFrame, df_split: pd.DataFrame
39
+ ) -> tuple[str, str, str]:
40
+ """Wrapper function to generate newsletter based on user settings."""
41
+ top_n_topics = (
42
+ None
43
+ if st.session_state["newsletter_all_topics"]
44
+ else st.session_state["newsletter_nb_topics"]
45
+ )
46
+ top_n_docs = (
47
+ None
48
+ if st.session_state["newsletter_all_docs"]
49
+ else st.session_state["newsletter_nb_docs"]
50
+ )
51
+
52
+ return generate_newsletter(
53
+ topic_model=st.session_state["topic_model"],
54
+ df=df,
55
+ topics=st.session_state["topics"],
56
+ df_split=df_split,
57
+ top_n_topics=top_n_topics,
58
+ top_n_docs=top_n_docs,
59
+ improve_topic_description=st.session_state["newsletter_improve_description"],
60
+ summarizer_class=SUMMARIZER_OPTIONS_MAPPER[
61
+ st.session_state["summarizer_classname"]
62
+ ],
63
+ summary_mode=st.session_state["summary_mode"],
64
+ )
65
+
66
+
67
+ def main():
68
+ # Check if a topic model exists
69
+ if "topic_model" not in st.session_state:
70
+ st.error(TRAIN_MODEL_FIRST_ERROR, icon=ERROR_ICON)
71
+ st.stop()
72
+
73
+ # Title
74
+ st.title("Automatic newsletters generation")
75
+
76
+ # Initialize session state variables
77
+ default_values = {
78
+ "newsletter_nb_topics": 4,
79
+ "newsletter_nb_docs": 3,
80
+ "summarizer_classname": list(SUMMARIZER_OPTIONS_MAPPER.keys())[0],
81
+ "summary_mode": "topic",
82
+ "newsletter_all_topics": False,
83
+ "newsletter_all_docs": False,
84
+ }
85
+
86
+ for key, value in default_values.items():
87
+ if key not in st.session_state:
88
+ st.session_state[key] = value
89
+
90
+ # Newsletter parameters sidebar
91
+ with st.sidebar:
92
+ register_multiple_widget(
93
+ "newsletter_all_topics",
94
+ "newsletter_all_docs",
95
+ "newsletter_nb_topics",
96
+ "newsletter_nb_docs",
97
+ "newsletter_improve_description",
98
+ "summarizer_classname",
99
+ "summary_mode",
100
+ )
101
+ all_topics = st.checkbox(
102
+ "Include all topics",
103
+ on_change=save_widget_state,
104
+ key="newsletter_all_topics",
105
+ )
106
+ st.slider(
107
+ "Number of topics",
108
+ min_value=1,
109
+ max_value=20,
110
+ on_change=save_widget_state,
111
+ key="newsletter_nb_topics",
112
+ disabled=all_topics,
113
+ )
114
+
115
+ all_documents = st.checkbox(
116
+ "Include all documents per topic",
117
+ on_change=save_widget_state,
118
+ key="newsletter_all_docs",
119
+ )
120
+ st.slider(
121
+ "Number of docs per topic",
122
+ min_value=1,
123
+ max_value=10,
124
+ on_change=save_widget_state,
125
+ key="newsletter_nb_docs",
126
+ disabled=all_documents,
127
+ )
128
+
129
+ st.toggle(
130
+ "Improve topic description",
131
+ value=True,
132
+ on_change=save_widget_state,
133
+ key="newsletter_improve_description",
134
+ )
135
+ st.selectbox(
136
+ "Summary mode",
137
+ ["topic", "document", "none"],
138
+ on_change=save_widget_state,
139
+ key="summary_mode",
140
+ )
141
+ st.selectbox(
142
+ "Summarizer class",
143
+ list(SUMMARIZER_OPTIONS_MAPPER.keys()),
144
+ on_change=save_widget_state,
145
+ key="summarizer_classname",
146
+ )
147
+
148
+ generate_newsletter_clicked = st.button(
149
+ "Generate newsletter", type="primary", use_container_width=True
150
+ )
151
+
152
+ # Generate newsletters when button is clicked
153
+ if generate_newsletter_clicked:
154
+ if st.session_state["split_type"] in ["yes", "enhanced"]:
155
+ df = st.session_state["initial_df"]
156
+ df_split = st.session_state["time_filtered_df"]
157
+ else:
158
+ df = st.session_state["time_filtered_df"]
159
+ df_split = None
160
+
161
+ with st.spinner("Generating newsletters..."):
162
+ st.session_state["newsletters"] = generate_newsletter_wrapper(df, df_split)
163
+
164
+ # Display generated newsletters
165
+ if "newsletters" in st.session_state:
166
+ st.components.v1.html(
167
+ md2html(
168
+ st.session_state["newsletters"][0],
169
+ Path(inspect.getfile(generate_newsletter)).parent / "newsletter.css",
170
+ ),
171
+ height=800,
172
+ scrolling=True,
173
+ )
174
+
175
+
176
+ # Restore widget state
177
+ restore_widget_state()
178
+ main()