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