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,79 @@
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 uuid
7
+ from typing import Any
8
+
9
+ import pandas as pd
10
+ import streamlit as st
11
+
12
+ from bertrend.demos.demos_utils.icons import WARNING_ICON
13
+ from bertrend_apps.prospective_demo import NOISE, WEAK_SIGNALS, STRONG_SIGNALS
14
+ from bertrend_apps.prospective_demo.models_info import get_models_info
15
+
16
+ COLS_RATIO_ID_TS = [2 / 7, 5 / 7]
17
+
18
+
19
+ @st.cache_data
20
+ def get_df_topics(model_interpretation_path=None) -> dict[str, pd.DataFrame]:
21
+ dfs_topics = {}
22
+ for df_id in [NOISE, WEAK_SIGNALS, STRONG_SIGNALS]:
23
+ df_path = model_interpretation_path / f"{df_id}.parquet"
24
+ dfs_topics[df_id] = (
25
+ pd.read_parquet(df_path) if df_path.exists() else pd.DataFrame()
26
+ )
27
+ return dfs_topics
28
+
29
+
30
+ def update_key(key: str, new_value: Any):
31
+ st.session_state[key] = new_value
32
+ # reset ts value in order to avoid errors if a previous ts value is not available for the new key value
33
+
34
+
35
+ def update_key_and_ts(key: str, new_value: Any):
36
+ update_key(key, new_value)
37
+ # reset ts value in order to avoid errors if a previous ts value is not available for the new key value
38
+ if "reference_ts" in st.session_state:
39
+ del st.session_state["reference_ts"]
40
+
41
+
42
+ def choose_id_and_ts():
43
+ col1, col2 = st.columns(COLS_RATIO_ID_TS)
44
+ with col1:
45
+ options = sorted(st.session_state.user_feeds.keys())
46
+ if "model_id" not in st.session_state:
47
+ st.session_state.model_id = options[0]
48
+ model_id_key = uuid.uuid4()
49
+ model_id = st.selectbox(
50
+ "Sélection de la veille",
51
+ options=options,
52
+ index=options.index(st.session_state.model_id),
53
+ key=model_id_key, # to avoid pb of unicity if displayed on several places
54
+ on_change=lambda: update_key_and_ts(
55
+ "model_id", st.session_state[model_id_key]
56
+ ),
57
+ )
58
+ with col2:
59
+ list_models = get_models_info(model_id)
60
+ if not list_models:
61
+ st.warning(f"{WARNING_ICON} Pas de modèle disponible")
62
+ st.stop()
63
+ elif len(list_models) < 2:
64
+ st.warning(
65
+ f"{WARNING_ICON} 2 modèles minimum pour analyser les tendances !"
66
+ )
67
+ st.stop()
68
+ if "reference_ts" not in st.session_state:
69
+ st.session_state.reference_ts = list_models[-1]
70
+ ts_key = uuid.uuid4()
71
+ st.select_slider(
72
+ "Date d'analyse",
73
+ options=list_models,
74
+ value=st.session_state.reference_ts,
75
+ format_func=lambda ts: ts.strftime("%d/%m/%Y"),
76
+ help="Sélection de la date d'analyse parmi celles disponibles",
77
+ key=ts_key, # to avoid pb of unicity if displayed on several places
78
+ on_change=lambda: update_key("reference_ts", st.session_state[ts_key]),
79
+ )
@@ -0,0 +1,226 @@
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 pandas as pd
6
+ import streamlit as st
7
+
8
+ from bertrend.demos.demos_utils.icons import (
9
+ WARNING_ICON,
10
+ WEAK_SIGNAL_ICON,
11
+ STRONG_SIGNAL_ICON,
12
+ NOISE_ICON,
13
+ )
14
+ from bertrend_apps.prospective_demo import (
15
+ LLM_TOPIC_DESCRIPTION_COLUMN,
16
+ NOISE,
17
+ WEAK_SIGNALS,
18
+ STRONG_SIGNALS,
19
+ get_model_interpretation_path,
20
+ LLM_TOPIC_TITLE_COLUMN,
21
+ URLS_COLUMN,
22
+ )
23
+ from bertrend_apps.prospective_demo.dashboard_common import (
24
+ choose_id_and_ts,
25
+ get_df_topics,
26
+ )
27
+ from bertrend_apps.prospective_demo.i18n import translate
28
+
29
+ COLS_RATIO = [4 / 7, 3 / 7]
30
+
31
+
32
+ def signal_analysis():
33
+ # ID and timestamp selection
34
+ choose_id_and_ts()
35
+ model_id = st.session_state.model_id
36
+ reference_ts = st.session_state.reference_ts
37
+
38
+ model_interpretation_path = get_model_interpretation_path(
39
+ user_name=st.session_state.username,
40
+ model_id=model_id,
41
+ reference_ts=reference_ts,
42
+ )
43
+
44
+ # TODO: decide which column to display
45
+ columns = [
46
+ "Topic",
47
+ LLM_TOPIC_TITLE_COLUMN,
48
+ LLM_TOPIC_DESCRIPTION_COLUMN,
49
+ # "Representation",
50
+ URLS_COLUMN,
51
+ "Latest_Popularity",
52
+ "Docs_Count",
53
+ # "Paragraphs_Count",
54
+ "Documents",
55
+ "Sources",
56
+ "Source_Diversity",
57
+ "Latest_Timestamp",
58
+ ]
59
+ column_config = {
60
+ "Topic": st.column_config.NumberColumn(
61
+ "Topic",
62
+ pinned=True,
63
+ ),
64
+ LLM_TOPIC_TITLE_COLUMN: st.column_config.TextColumn(
65
+ translate("title"), pinned=True, width="large"
66
+ ),
67
+ "Latest_Popularity": st.column_config.ProgressColumn(
68
+ format="%i",
69
+ max_value=50,
70
+ ),
71
+ "Source_Diversity": st.column_config.ProgressColumn(
72
+ format="%i",
73
+ max_value=50,
74
+ ),
75
+ "Latest_Timestamp": st.column_config.DateColumn(
76
+ format="DD/MM/YYYY",
77
+ ),
78
+ URLS_COLUMN: st.column_config.LinkColumn(),
79
+ }
80
+
81
+ dfs_topics = get_df_topics(model_interpretation_path)
82
+
83
+ col1, col2 = st.columns(COLS_RATIO)
84
+ with col1:
85
+ # Display dataframes for weak_signals, strong, etc
86
+ display_translated_signal_categories(
87
+ dfs_topics[NOISE],
88
+ dfs_topics[WEAK_SIGNALS],
89
+ dfs_topics[STRONG_SIGNALS],
90
+ reference_ts,
91
+ columns=columns,
92
+ column_config=column_config,
93
+ )
94
+
95
+ with col2:
96
+ st.info(translate("todo_message"))
97
+ explore_topic_sources(dfs_topics)
98
+
99
+
100
+ @st.fragment
101
+ def explore_topic_sources(dfs_topics):
102
+ st.write(f"**{translate('explore_sources_by_topic')}**")
103
+ selected_signal_type = st.pills(
104
+ translate("signal_type"),
105
+ label_visibility="hidden",
106
+ options=[translate("emerging_topics"), translate("strong_topics")],
107
+ selection_mode="single",
108
+ default=translate("emerging_topics"),
109
+ )
110
+ if selected_signal_type == translate("strong_topics"):
111
+ selected_df = dfs_topics.get(STRONG_SIGNALS)
112
+ else:
113
+ selected_df = dfs_topics.get(WEAK_SIGNALS)
114
+ if selected_df is None or selected_df.empty:
115
+ st.warning(f"{WARNING_ICON} {translate('no_data')}")
116
+ else:
117
+ selected_df = selected_df.sort_values(by=["Latest_Popularity"], ascending=False)
118
+ options = selected_df["Topic"].tolist()
119
+ topic_id = st.selectbox(
120
+ index=None,
121
+ label=translate("topic_selection"),
122
+ label_visibility="hidden",
123
+ options=options,
124
+ format_func=lambda x: f"{translate('topic')} {x}: "
125
+ + selected_df[selected_df["Topic"] == x][LLM_TOPIC_TITLE_COLUMN].values[0],
126
+ )
127
+ if topic_id is None:
128
+ return
129
+ if selected_signal_type == translate("strong_topics"):
130
+ color = "green"
131
+ else:
132
+ color = "orange"
133
+ row = selected_df[selected_df["Topic"] == topic_id]
134
+ display_topic_links(
135
+ title=f":{color}[**{row[LLM_TOPIC_TITLE_COLUMN].values[0]}**]",
136
+ desc=row[LLM_TOPIC_DESCRIPTION_COLUMN].values[0],
137
+ df=list(selected_df.query(f"Topic == {topic_id}")[URLS_COLUMN])[0],
138
+ )
139
+
140
+
141
+ def display_translated_signal_categories(
142
+ noise_topics_df: pd.DataFrame,
143
+ weak_signal_topics_df: pd.DataFrame,
144
+ strong_signal_topics_df: pd.DataFrame,
145
+ window_end: pd.Timestamp,
146
+ columns=None,
147
+ column_order=None,
148
+ column_config=None,
149
+ ):
150
+ """Wrapper around display_signal_categories_df that uses translated text."""
151
+ # Weak Signals
152
+ with st.expander(
153
+ f":orange[{WEAK_SIGNAL_ICON} {translate('weak_signals')}]", expanded=True
154
+ ):
155
+ st.subheader(f":orange[{translate('weak_signals')}]")
156
+ if not weak_signal_topics_df.empty:
157
+ displayed_df = weak_signal_topics_df[columns].sort_values(
158
+ by=["Latest_Popularity"], ascending=False
159
+ )
160
+ displayed_df["Documents"] = displayed_df["Documents"].astype(str)
161
+ st.dataframe(
162
+ displayed_df,
163
+ column_order=column_order if column_order else columns,
164
+ column_config=column_config,
165
+ hide_index=True,
166
+ )
167
+ else:
168
+ st.info(
169
+ translate("no_weak_signals").format(timestamp=window_end),
170
+ icon=WARNING_ICON,
171
+ )
172
+
173
+ # Strong Signals
174
+ with st.expander(
175
+ f":green[{STRONG_SIGNAL_ICON} {translate('strong_signals')}]", expanded=True
176
+ ):
177
+ st.subheader(f":green[{translate('strong_signals')}]")
178
+ if not strong_signal_topics_df.empty:
179
+ displayed_df = strong_signal_topics_df[columns].sort_values(
180
+ by=["Latest_Popularity"], ascending=False
181
+ )
182
+ displayed_df["Documents"] = displayed_df["Documents"].astype(str)
183
+ st.dataframe(
184
+ displayed_df,
185
+ column_order=column_order if column_order else columns,
186
+ column_config=column_config,
187
+ hide_index=True,
188
+ )
189
+ else:
190
+ st.info(
191
+ translate("no_strong_signals").format(timestamp=window_end),
192
+ icon=WARNING_ICON,
193
+ )
194
+
195
+ # Noise
196
+ with st.expander(f":grey[{NOISE_ICON} {translate('noise')}]", expanded=True):
197
+ st.subheader(f":grey[{translate('noise')}]")
198
+ if not noise_topics_df.empty:
199
+ displayed_df = noise_topics_df[columns].sort_values(
200
+ by=["Latest_Popularity"], ascending=False
201
+ )
202
+ displayed_df["Documents"] = displayed_df["Documents"].astype(str)
203
+ st.dataframe(
204
+ displayed_df,
205
+ column_order=column_order if column_order else columns,
206
+ column_config=column_config,
207
+ hide_index=True,
208
+ )
209
+ else:
210
+ st.info(
211
+ translate("no_noise_signals").format(timestamp=window_end),
212
+ icon=WARNING_ICON,
213
+ )
214
+
215
+
216
+ @st.dialog(translate("explore_sources"), width="large")
217
+ def display_topic_links(title: str, desc: str, df: pd.DataFrame):
218
+ st.subheader(title)
219
+ st.write(desc)
220
+ st.dataframe(
221
+ df,
222
+ use_container_width=True,
223
+ column_config={
224
+ "value": st.column_config.LinkColumn(translate("reference_articles")),
225
+ },
226
+ )
@@ -0,0 +1,38 @@
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
+
7
+ from loguru import logger
8
+
9
+ from bertrend import FEED_BASE_PATH, load_toml_config
10
+ from bertrend_apps.prospective_demo import CONFIG_FEEDS_BASE_PATH
11
+
12
+
13
+ def read_user_feeds(username: str) -> tuple[dict[str, dict], dict[str, Path]]:
14
+ """Read user feed config files"""
15
+ user_feed_dir = CONFIG_FEEDS_BASE_PATH / username
16
+ user_feed_dir.mkdir(parents=True, exist_ok=True)
17
+ logger.debug(f"Reading user feeds from: {user_feed_dir}")
18
+ matching_files = user_feed_dir.rglob("*_feed.toml")
19
+
20
+ user_feeds = {}
21
+ feed_files = {}
22
+ for f in matching_files:
23
+ feed_id = f.name.split("_feed.toml")[0]
24
+ user_feeds[feed_id] = load_toml_config(f)
25
+ feed_files[feed_id] = f
26
+
27
+ return user_feeds, feed_files
28
+
29
+
30
+ def get_all_files_for_feed(user_feeds: dict[str, dict], feed_id: str) -> list[Path]:
31
+ """Returns the paths of all files associated to a feed for the current user."""
32
+ feed_base_dir = user_feeds[feed_id]["data-feed"]["feed_dir_path"]
33
+ list_all_files = list(
34
+ Path(FEED_BASE_PATH, feed_base_dir).glob(
35
+ f"*{user_feeds[feed_id]['data-feed'].get('id')}*.jsonl*"
36
+ )
37
+ )
38
+ return list_all_files
@@ -0,0 +1,261 @@
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 re
6
+ import time
7
+ from pathlib import Path
8
+
9
+ import pandas as pd
10
+ import streamlit as st
11
+ import toml
12
+ from loguru import logger
13
+
14
+ from bertrend.config.parameters import LANGUAGES
15
+ from bertrend.demos.demos_utils.icons import (
16
+ INFO_ICON,
17
+ ERROR_ICON,
18
+ ADD_ICON,
19
+ EDIT_ICON,
20
+ DELETE_ICON,
21
+ WARNING_ICON,
22
+ TOGGLE_ON_ICON,
23
+ TOGGLE_OFF_ICON,
24
+ )
25
+ from bertrend_apps.common.crontab_utils import (
26
+ get_understandable_cron_description,
27
+ check_if_scrapping_active_for_user,
28
+ remove_scrapping_for_user,
29
+ schedule_scrapping,
30
+ )
31
+ from bertrend_apps.data_provider import URL_PATTERN
32
+ from bertrend_apps.prospective_demo.feeds_common import (
33
+ read_user_feeds,
34
+ )
35
+ from bertrend_apps.prospective_demo import CONFIG_FEEDS_BASE_PATH
36
+ from bertrend_apps.prospective_demo.models_info import (
37
+ remove_scheduled_training_for_user,
38
+ )
39
+ from bertrend_apps.prospective_demo.streamlit_utils import clickable_df
40
+
41
+ # Default feed configs
42
+ DEFAULT_GNEWS_CRONTAB_EXPRESSION = "1 0 * * 1"
43
+ DEFAULT_CUREBOT_CRONTAB_EXPRESSION = "42 0,6,12,18 * * *" # 4 times a day
44
+ DEFAULT_MAX_RESULTS = 20
45
+ DEFAULT_NUMBER_OF_DAYS = 14
46
+ FEED_SOURCES = ["google", "curebot"]
47
+ TRANSLATION = {"English": "Anglais", "French": "Français"}
48
+
49
+
50
+ @st.dialog("Configuration d'un nouveau flux de données")
51
+ def edit_feed_monitoring(config: dict | None = None):
52
+ """Create or update a feed monitoring configuration."""
53
+ chosen_id = st.text_input(
54
+ "ID :red[*]",
55
+ help="Identifiant du flux de données",
56
+ value=None if not config else config["id"],
57
+ )
58
+
59
+ provider = st.segmented_control(
60
+ "Source",
61
+ selection_mode="single",
62
+ options=FEED_SOURCES,
63
+ default=FEED_SOURCES[0] if not config else config["provider"],
64
+ help="Sélection de la source de données",
65
+ )
66
+ if provider == "google":
67
+ query = st.text_input(
68
+ "Requête :red[*]",
69
+ value="" if not config else config["query"],
70
+ help="Saisir ici la requête qui sera faite sur Google News",
71
+ )
72
+ language = st.segmented_control(
73
+ "Langue",
74
+ selection_mode="single",
75
+ options=LANGUAGES,
76
+ default=LANGUAGES[0],
77
+ format_func=lambda lang: TRANSLATION[lang],
78
+ help="Choix de la langue",
79
+ )
80
+ if "update_frequency" not in st.session_state:
81
+ st.session_state.update_frequency = (
82
+ DEFAULT_GNEWS_CRONTAB_EXPRESSION
83
+ if not config
84
+ else config["update_frequency"]
85
+ )
86
+ new_freq = st.text_input(
87
+ f"Fréquence d'exécution",
88
+ value=st.session_state.update_frequency,
89
+ help=f"Fréquence de collecte des données",
90
+ )
91
+ st.session_state.update_frequency = new_freq
92
+ st.write(display_crontab_description(st.session_state.update_frequency))
93
+
94
+ elif provider == "curebot":
95
+ query = st.text_input(
96
+ "ATOM feed :red[*]",
97
+ value="" if not config else config["query"],
98
+ help="URL du flux de données Curebot",
99
+ )
100
+
101
+ try:
102
+ get_understandable_cron_description(st.session_state.update_frequency)
103
+ valid_cron = True
104
+ except:
105
+ valid_cron = False
106
+
107
+ if st.button(
108
+ "OK",
109
+ disabled=not chosen_id
110
+ or not query
111
+ or (query and provider == "curebot" and not re.match(URL_PATTERN, query)),
112
+ ):
113
+ if not config:
114
+ config = {}
115
+ config["id"] = "feed_" + chosen_id
116
+ config["feed_dir_path"] = (
117
+ "users/" + st.session_state.username + "/feed_" + chosen_id
118
+ )
119
+ config["query"] = query
120
+ config["provider"] = provider
121
+ if not config.get("max_results"):
122
+ config["max_results"] = DEFAULT_MAX_RESULTS
123
+ if not config.get("number_of_days"):
124
+ config["number_of_days"] = DEFAULT_NUMBER_OF_DAYS
125
+ if provider == "google":
126
+ config["language"] = "fr" if language == "French" else "en"
127
+ config["update_frequency"] = (
128
+ st.session_state.update_frequency
129
+ if valid_cron
130
+ else DEFAULT_GNEWS_CRONTAB_EXPRESSION
131
+ )
132
+ elif provider == "curebot":
133
+ config["language"] = "fr"
134
+ config["update_frequency"] = DEFAULT_CUREBOT_CRONTAB_EXPRESSION
135
+
136
+ if "update_frequency" in st.session_state:
137
+ del st.session_state["update_frequency"] # to avoid memory effect
138
+
139
+ # Remove prevous crontab if any
140
+ remove_scrapping_for_user(feed_id=chosen_id, user=st.session_state.username)
141
+
142
+ # Save feed config and update crontab
143
+ save_feed_config(chosen_id, config)
144
+
145
+
146
+ def save_feed_config(chosen_id, feed_config: dict):
147
+ """Save the feed configuration to disk as a TOML file."""
148
+ feed_path = (
149
+ CONFIG_FEEDS_BASE_PATH / st.session_state.username / f"{chosen_id}_feed.toml"
150
+ )
151
+ # Save the dictionary to a TOML file
152
+ with open(feed_path, "w") as toml_file:
153
+ toml.dump({"data-feed": feed_config}, toml_file)
154
+ logger.debug(f"Saved feed config {feed_config} to {feed_path}")
155
+ schedule_scrapping(feed_path, user=st.session_state.username)
156
+ st.rerun()
157
+
158
+
159
+ def display_crontab_description(crontab_expr: str) -> str:
160
+ try:
161
+ return f":blue[{INFO_ICON} {get_understandable_cron_description(crontab_expr)}]"
162
+ except Exception:
163
+ return f":red[{ERROR_ICON} Expression mal écrite !]"
164
+
165
+
166
+ def configure_information_sources():
167
+ """Configure Information Sources."""
168
+ # if "user_feeds" not in st.session_state:
169
+ st.session_state.user_feeds, st.session_state.feed_files = read_user_feeds(
170
+ st.session_state.username
171
+ )
172
+
173
+ displayed_list = []
174
+ for k, v in st.session_state.user_feeds.items():
175
+ displayed_list.append(
176
+ {
177
+ "id": k,
178
+ "provider": v["data-feed"]["provider"],
179
+ "query": v["data-feed"]["query"],
180
+ "language": v["data-feed"]["language"],
181
+ "update_frequency": v["data-feed"]["update_frequency"],
182
+ }
183
+ )
184
+ df = pd.DataFrame(displayed_list)
185
+ if not df.empty:
186
+ df = df.sort_values(by="id", inplace=False).reset_index(drop=True)
187
+
188
+ if st.button(f":green[{ADD_ICON}]", type="tertiary", help="Nouveau flux de veille"):
189
+ edit_feed_monitoring()
190
+
191
+ clickable_df_buttons = [
192
+ (EDIT_ICON, edit_feed_monitoring, "secondary"),
193
+ (lambda x: toggle_icon(df, x), toggle_feed, "secondary"),
194
+ (DELETE_ICON, handle_delete, "primary"),
195
+ ]
196
+ clickable_df(df, clickable_df_buttons)
197
+
198
+
199
+ def toggle_icon(df: pd.DataFrame, index: int) -> str:
200
+ """Switch the toggle icon depending on the statis of the scrapping feed in the crontab"""
201
+ feed_id = df["id"][index]
202
+ return (
203
+ f":green[{TOGGLE_ON_ICON}]"
204
+ if check_if_scrapping_active_for_user(
205
+ feed_id=feed_id, user=st.session_state.username
206
+ )
207
+ else f":red[{TOGGLE_OFF_ICON}]"
208
+ )
209
+
210
+
211
+ def toggle_feed(cfg: dict):
212
+ """Activate / deactivate the feed from the crontab"""
213
+ feed_id = cfg["id"]
214
+ if check_if_scrapping_active_for_user(
215
+ feed_id=feed_id, user=st.session_state.username
216
+ ):
217
+ if remove_scrapping_for_user(feed_id=feed_id, user=st.session_state.username):
218
+ st.toast(f"Le flux **{feed_id}** est déactivé !", icon=INFO_ICON)
219
+ logger.info(f"Flux {feed_id} désactivé !")
220
+ else:
221
+ schedule_scrapping(
222
+ st.session_state.feed_files[feed_id], user=st.session_state.username
223
+ )
224
+ st.toast(f"Le flux **{feed_id}** est activé !", icon=WARNING_ICON)
225
+ logger.info(f"Flux {feed_id} activé !")
226
+ time.sleep(0.2)
227
+ st.rerun()
228
+
229
+
230
+ def delete_feed_config(feed_id: str):
231
+ # remove config file
232
+ file_path: Path = st.session_state.feed_files[feed_id]
233
+ try:
234
+ file_path.unlink()
235
+ logger.debug(f"Feed file {file_path} has been removed.")
236
+ except Exception as e:
237
+ logger.error(f"An error occurred: {e}")
238
+
239
+
240
+ @st.dialog("Confirmation")
241
+ def handle_delete(row_dict: dict):
242
+ """Function to handle remove click events"""
243
+ feed_id = row_dict["id"]
244
+ st.write(
245
+ f":orange[{WARNING_ICON}] Voulez-vous vraiment supprimer le flux de veille **{feed_id}** ?"
246
+ )
247
+ col1, col2, _ = st.columns([2, 2, 8])
248
+ with col1:
249
+ if st.button("Oui", type="primary"):
250
+ remove_scrapping_for_user(feed_id=feed_id, user=st.session_state.username)
251
+ delete_feed_config(feed_id)
252
+ logger.info(f"Flux {feed_id} supprimé !")
253
+ # Remove from crontab associated training
254
+ remove_scheduled_training_for_user(
255
+ model_id=feed_id, user=st.session_state.username
256
+ )
257
+ time.sleep(0.2)
258
+ st.rerun()
259
+ with col2:
260
+ if st.button("Non"):
261
+ st.rerun()
@@ -0,0 +1,95 @@
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 datetime
6
+ from pathlib import Path
7
+
8
+ import pandas as pd
9
+ import streamlit as st
10
+
11
+ from bertrend import FEED_BASE_PATH
12
+ from bertrend.utils.data_loading import (
13
+ load_data,
14
+ TIMESTAMP_COLUMN,
15
+ TITLE_COLUMN,
16
+ URL_COLUMN,
17
+ TEXT_COLUMN,
18
+ )
19
+ from bertrend_apps.prospective_demo.feeds_common import get_all_files_for_feed
20
+
21
+
22
+ def display_data_status():
23
+ if not st.session_state.user_feeds:
24
+ return
25
+
26
+ col1, col2 = st.columns(2)
27
+ with col1:
28
+ st.selectbox(
29
+ "Sélection de la veille",
30
+ options=sorted(st.session_state.user_feeds.keys()),
31
+ key="id_data",
32
+ )
33
+
34
+ with col2:
35
+ if "data_time_window" not in st.session_state:
36
+ st.session_state.data_time_window = 7
37
+ st.slider(
38
+ "Fenêtre temporelle (jours)",
39
+ min_value=1,
40
+ max_value=60,
41
+ step=1,
42
+ key="data_time_window",
43
+ )
44
+
45
+ display_data_info_for_feed(st.session_state.id_data)
46
+
47
+
48
+ def display_data_info_for_feed(feed_id: str):
49
+ all_files = get_all_files_for_feed(st.session_state.user_feeds, feed_id)
50
+ df = get_all_data(files=all_files)
51
+
52
+ if df.empty:
53
+ df_filtered = pd.DataFrame()
54
+ else:
55
+ df = df[
56
+ [TITLE_COLUMN, URL_COLUMN, TEXT_COLUMN, TIMESTAMP_COLUMN]
57
+ ] # filter useful columns
58
+
59
+ cutoff_date = datetime.datetime.now() - datetime.timedelta(
60
+ days=st.session_state.data_time_window
61
+ )
62
+ df_filtered = df[df[TIMESTAMP_COLUMN] >= cutoff_date]
63
+
64
+ stats = {
65
+ "ID": feed_id,
66
+ "# Fichiers": len(all_files),
67
+ "Date début": df[TIMESTAMP_COLUMN].min() if not df.empty else None,
68
+ "Date fin": df[TIMESTAMP_COLUMN].max() if not df.empty else None,
69
+ "# Articles": len(df),
70
+ f"# Articles ({st.session_state.data_time_window} derniers jours)": len(
71
+ df_filtered
72
+ ),
73
+ }
74
+
75
+ st.dataframe(pd.DataFrame([stats]))
76
+
77
+ st.write(f"#### Données des derniers {st.session_state.data_time_window} jours")
78
+ st.dataframe(
79
+ df_filtered,
80
+ use_container_width=True,
81
+ hide_index=True,
82
+ column_config={"url": st.column_config.LinkColumn("url")},
83
+ )
84
+
85
+
86
+ @st.cache_data
87
+ def get_all_data(files: list[Path]) -> pd.DataFrame:
88
+ """Returns the data contained in the provided files as a single DataFrame."""
89
+ if not files:
90
+ return pd.DataFrame()
91
+ dfs = [load_data(Path(f)) for f in files]
92
+ new_df = pd.concat(dfs).drop_duplicates(
93
+ subset=["title"], keep="first", inplace=False
94
+ )
95
+ return new_df