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.
- bertrend/BERTopicModel.py +320 -0
- bertrend/BERTrend.py +1211 -0
- bertrend/__init__.py +59 -0
- bertrend/config/bertopic_default_config.toml +48 -0
- bertrend/config/bertrend_default_config.toml +16 -0
- bertrend/config/parameters.py +92 -0
- bertrend/config/services_default_config.toml +32 -0
- bertrend/demos/__init__.py +4 -0
- bertrend/demos/demos_utils/__init__.py +13 -0
- bertrend/demos/demos_utils/data_loading_component.py +267 -0
- bertrend/demos/demos_utils/embed_documents_component.py +52 -0
- bertrend/demos/demos_utils/icons.py +41 -0
- bertrend/demos/demos_utils/messages.py +11 -0
- bertrend/demos/demos_utils/parameters_component.py +162 -0
- bertrend/demos/demos_utils/state_utils.py +99 -0
- bertrend/demos/start_summarizer_app.sh +9 -0
- bertrend/demos/start_topic_analysis_app.sh +9 -0
- bertrend/demos/start_weak_signals_app.sh +9 -0
- bertrend/demos/summarization/__init__.py +4 -0
- bertrend/demos/summarization/summarizer_app.py +98 -0
- bertrend/demos/topic_analysis/__init__.py +4 -0
- bertrend/demos/topic_analysis/app.py +87 -0
- bertrend/demos/topic_analysis/app_utils.py +69 -0
- bertrend/demos/topic_analysis/demo_pages/__init__.py +4 -0
- bertrend/demos/topic_analysis/demo_pages/explore_topics.py +455 -0
- bertrend/demos/topic_analysis/demo_pages/newsletters_generation.py +178 -0
- bertrend/demos/topic_analysis/demo_pages/temporal_visualizations.py +573 -0
- bertrend/demos/topic_analysis/demo_pages/topic_visualizations.py +280 -0
- bertrend/demos/topic_analysis/demo_pages/training_page.py +219 -0
- bertrend/demos/topic_analysis/messages.py +13 -0
- bertrend/demos/weak_signals/__init__.py +4 -0
- bertrend/demos/weak_signals/app.py +510 -0
- bertrend/demos/weak_signals/messages.py +32 -0
- bertrend/demos/weak_signals/visualizations_utils.py +403 -0
- bertrend/llm_utils/__init__.py +4 -0
- bertrend/llm_utils/newsletter.css +131 -0
- bertrend/llm_utils/newsletter_features.py +272 -0
- bertrend/llm_utils/openai_client.py +235 -0
- bertrend/llm_utils/prompts.py +94 -0
- bertrend/metrics/__init__.py +4 -0
- bertrend/metrics/temporal_metrics_embedding.py +861 -0
- bertrend/metrics/topic_metrics.py +199 -0
- bertrend/resources/common_ngrams.json +23 -0
- bertrend/resources/stopwords-en.json +3 -0
- bertrend/resources/stopwords-fr.json +1 -0
- bertrend/resources/stopwords-rte.json +15 -0
- bertrend/services/__init__.py +4 -0
- bertrend/services/authentication.py +66 -0
- bertrend/services/embedding_client.py +154 -0
- bertrend/services/embedding_service.py +380 -0
- bertrend/services/summarizer.py +45 -0
- bertrend/services/summary/__init__.py +4 -0
- bertrend/services/summary/abstractive_summarizer.py +74 -0
- bertrend/services/summary/chatgpt_summarizer.py +67 -0
- bertrend/services/summary/extractive_summarizer.py +368 -0
- bertrend/services/summary/lexrank.py +126 -0
- bertrend/services/summary/prompts.py +43 -0
- bertrend/tests/__init__.py +4 -0
- bertrend/tests/test_abstractive_summarizer.py +140 -0
- bertrend/tests/test_bertrend.py +1091 -0
- bertrend/tests/test_cache_utils.py +111 -0
- bertrend/tests/test_chatgpt_summarizer.py +150 -0
- bertrend/tests/test_config_utils.py +54 -0
- bertrend/tests/test_data_loading.py +326 -0
- bertrend/tests/test_data_structure.py +53 -0
- bertrend/tests/test_embedding_client.py +546 -0
- bertrend/tests/test_extractive_summarizer.py +250 -0
- bertrend/tests/test_installation.py +183 -0
- bertrend/tests/test_newsletter_features.py +354 -0
- bertrend/tests/test_openai_client.py +210 -0
- bertrend/tests/test_representative_docs.py +125 -0
- bertrend/tests/test_summarizer.py +115 -0
- bertrend/tests/test_temporal_metrics_embedding.py +349 -0
- bertrend/tests/test_topic_description.py +159 -0
- bertrend/tests/test_topic_metrics.py +233 -0
- bertrend/tests/test_topic_model.py +285 -0
- bertrend/tests/test_trend_analysis_data_structure.py +174 -0
- bertrend/tests/test_trend_analysis_prompts.py +225 -0
- bertrend/tests/test_trend_analysis_visualizations.py +307 -0
- bertrend/tests/test_trend_analysis_weak_signals.py +598 -0
- bertrend/tests/test_utils_init.py +70 -0
- bertrend/tests/test_visualizations.py +306 -0
- bertrend/topic_analysis/__init__.py +4 -0
- bertrend/topic_analysis/data_structure.py +13 -0
- bertrend/topic_analysis/prompts.py +42 -0
- bertrend/topic_analysis/representative_docs.py +57 -0
- bertrend/topic_analysis/topic_description.py +66 -0
- bertrend/topic_analysis/visualizations.py +315 -0
- bertrend/trend_analysis/__init__.py +4 -0
- bertrend/trend_analysis/data_structure.py +78 -0
- bertrend/trend_analysis/prompts.py +229 -0
- bertrend/trend_analysis/signal_llm_template_en.html +181 -0
- bertrend/trend_analysis/signal_llm_template_fr.html +181 -0
- bertrend/trend_analysis/visualizations.py +605 -0
- bertrend/trend_analysis/weak_signals.py +425 -0
- bertrend/utils/__init__.py +31 -0
- bertrend/utils/cache_utils.py +31 -0
- bertrend/utils/config_utils.py +41 -0
- bertrend/utils/data_loading.py +329 -0
- bertrend-0.3.1.dist-info/AUTHORS.txt +15 -0
- bertrend-0.3.1.dist-info/LICENSE.md +373 -0
- bertrend-0.3.1.dist-info/METADATA +400 -0
- bertrend-0.3.1.dist-info/RECORD +181 -0
- bertrend-0.3.1.dist-info/WHEEL +4 -0
- bertrend_apps/__init__.py +4 -0
- bertrend_apps/common/__init__.py +4 -0
- bertrend_apps/common/crontab_utils.py +113 -0
- bertrend_apps/common/mail_utils.py +91 -0
- bertrend_apps/config/feeds/arxiv_feed.toml +15 -0
- bertrend_apps/config/feeds/decarbonation_feed.toml +15 -0
- bertrend_apps/config/feeds/llm_en_feed.toml +15 -0
- bertrend_apps/config/feeds/llm_feed.toml +16 -0
- bertrend_apps/config/feeds/nlp_feed.toml +15 -0
- bertrend_apps/config/feeds/rte_feed.toml +15 -0
- bertrend_apps/config/gmail_credentials.json +1 -0
- bertrend_apps/config/newsletters/arxiv_newsletter_from_last.toml +39 -0
- bertrend_apps/config/newsletters/arxiv_newsletter_from_scratch.toml +39 -0
- bertrend_apps/config/newsletters/decarbonation_newsletter_from_last.toml +38 -0
- bertrend_apps/config/newsletters/decarbonation_newsletter_from_scratch.toml +38 -0
- bertrend_apps/config/newsletters/llm_en_newsletter_from_last.toml +38 -0
- bertrend_apps/config/newsletters/llm_newsletter_from_last.toml +39 -0
- bertrend_apps/config/newsletters/nlp_newsletter_from_last.toml +38 -0
- bertrend_apps/config/newsletters/rte_newsletter_from_last.toml +38 -0
- bertrend_apps/config/newsletters/rte_newsletter_from_scratch.toml +38 -0
- bertrend_apps/data_provider/__init__.py +9 -0
- bertrend_apps/data_provider/__main__.py +237 -0
- bertrend_apps/data_provider/arxiv_provider.py +161 -0
- bertrend_apps/data_provider/bing_news_provider.py +82 -0
- bertrend_apps/data_provider/curebot_provider.py +105 -0
- bertrend_apps/data_provider/data_provider.py +194 -0
- bertrend_apps/data_provider/google_news_provider.py +94 -0
- bertrend_apps/data_provider/newscatcher_provider.py +74 -0
- bertrend_apps/data_provider/utils.py +165 -0
- bertrend_apps/exploration/__init__.py +4 -0
- bertrend_apps/exploration/curebot/__init__.py +4 -0
- bertrend_apps/exploration/curebot/app.py +45 -0
- bertrend_apps/exploration/curebot/app_utils.py +317 -0
- bertrend_apps/exploration/curebot/config.toml +33 -0
- bertrend_apps/exploration/curebot/import_extraction_article.ipynb +108 -0
- bertrend_apps/exploration/curebot/old_veille_analyse.py +298 -0
- bertrend_apps/exploration/curebot/prompts.py +24 -0
- bertrend_apps/exploration/curebot/start_newsletter_generator.sh +8 -0
- bertrend_apps/exploration/curebot/tabs/tab1.py +150 -0
- bertrend_apps/exploration/curebot/tabs/tab2.py +95 -0
- bertrend_apps/exploration/curebot/tabs/tab3.py +125 -0
- bertrend_apps/exploration/dataviz/Main_page.py +10 -0
- bertrend_apps/exploration/dataviz/__init__.py +4 -0
- bertrend_apps/exploration/dataviz/pages/1_Data_feeds.py +105 -0
- bertrend_apps/exploration/geolocalization/__init__.py +4 -0
- bertrend_apps/exploration/geolocalization/entity_utils.py +118 -0
- bertrend_apps/exploration/geolocalization/spacy/__init__.py +4 -0
- bertrend_apps/exploration/geolocalization/spacy/common_factory.py +142 -0
- bertrend_apps/exploration/geolocalization/spacy/dbpedia.py +206 -0
- bertrend_apps/exploration/geolocalization/spacy/nlp.config +37 -0
- bertrend_apps/exploration/geolocalization/spacy/utils.py +39 -0
- bertrend_apps/install_feeds_and_newsletters.py +29 -0
- bertrend_apps/newsletters/__init__.py +4 -0
- bertrend_apps/newsletters/__main__.py +277 -0
- bertrend_apps/prospective_demo/.streamlit/config.toml +3 -0
- bertrend_apps/prospective_demo/.streamlit/secrets.toml +9 -0
- bertrend_apps/prospective_demo/__init__.py +70 -0
- bertrend_apps/prospective_demo/app.py +111 -0
- bertrend_apps/prospective_demo/authentication.py +44 -0
- bertrend_apps/prospective_demo/dashboard_analysis.py +132 -0
- bertrend_apps/prospective_demo/dashboard_common.py +79 -0
- bertrend_apps/prospective_demo/dashboard_signals.py +226 -0
- bertrend_apps/prospective_demo/feeds_common.py +38 -0
- bertrend_apps/prospective_demo/feeds_config.py +261 -0
- bertrend_apps/prospective_demo/feeds_data.py +95 -0
- bertrend_apps/prospective_demo/i18n.py +136 -0
- bertrend_apps/prospective_demo/llm_utils.py +34 -0
- bertrend_apps/prospective_demo/models_info.py +361 -0
- bertrend_apps/prospective_demo/perf_utils.py +23 -0
- bertrend_apps/prospective_demo/process_new_data.py +348 -0
- bertrend_apps/prospective_demo/report_generation.py +214 -0
- bertrend_apps/prospective_demo/report_template.html +148 -0
- bertrend_apps/prospective_demo/report_utils.py +132 -0
- bertrend_apps/prospective_demo/start_demo.sh +9 -0
- bertrend_apps/prospective_demo/streamlit_utils.py +37 -0
- bertrend_apps/start_apps.sh +15 -0
- 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()
|