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,348 @@
|
|
|
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 datetime import timedelta
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import typer
|
|
10
|
+
from jsonlines import jsonlines
|
|
11
|
+
from loguru import logger
|
|
12
|
+
|
|
13
|
+
from bertrend import load_toml_config, FEED_BASE_PATH
|
|
14
|
+
from bertrend.BERTopicModel import BERTopicModel
|
|
15
|
+
from bertrend.BERTrend import train_new_data, BERTrend
|
|
16
|
+
from bertrend.services.embedding_service import EmbeddingService
|
|
17
|
+
from bertrend.trend_analysis.weak_signals import analyze_signal
|
|
18
|
+
from bertrend.utils.data_loading import (
|
|
19
|
+
load_data,
|
|
20
|
+
split_data,
|
|
21
|
+
group_by_days,
|
|
22
|
+
TEXT_COLUMN,
|
|
23
|
+
)
|
|
24
|
+
from bertrend_apps.prospective_demo import (
|
|
25
|
+
get_user_feed_path,
|
|
26
|
+
get_user_models_path,
|
|
27
|
+
NOISE,
|
|
28
|
+
WEAK_SIGNALS,
|
|
29
|
+
STRONG_SIGNALS,
|
|
30
|
+
LLM_TOPIC_DESCRIPTION_COLUMN,
|
|
31
|
+
LLM_TOPIC_TITLE_COLUMN,
|
|
32
|
+
DEFAULT_ANALYSIS_CFG,
|
|
33
|
+
get_model_cfg_path,
|
|
34
|
+
URLS_COLUMN,
|
|
35
|
+
get_model_interpretation_path,
|
|
36
|
+
)
|
|
37
|
+
from bertrend_apps.prospective_demo.llm_utils import generate_bertrend_topic_description
|
|
38
|
+
|
|
39
|
+
DEFAULT_TOP_K = 5
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def load_all_data(model_id: str, user: str, language: str):
|
|
43
|
+
# TODO: to be improved
|
|
44
|
+
cfg_file = get_user_feed_path(user, model_id)
|
|
45
|
+
if not cfg_file.exists():
|
|
46
|
+
logger.error(f"Cannot find/process config file: {cfg_file}")
|
|
47
|
+
return
|
|
48
|
+
cfg = load_toml_config(cfg_file)
|
|
49
|
+
feed_base_dir = cfg["data-feed"]["feed_dir_path"]
|
|
50
|
+
files = list(
|
|
51
|
+
Path(FEED_BASE_PATH, feed_base_dir).glob(
|
|
52
|
+
f"*{cfg['data-feed'].get('id')}*.jsonl*"
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
if not files:
|
|
56
|
+
logger.warning(f"No new data for '{model_id}', nothing to do")
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
dfs = [load_data(Path(f), language=language) for f in files]
|
|
60
|
+
new_data = pd.concat(dfs).drop_duplicates(
|
|
61
|
+
subset=["title"], keep="first", inplace=False
|
|
62
|
+
)
|
|
63
|
+
return new_data
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_relevant_model_config(
|
|
67
|
+
model_id: str,
|
|
68
|
+
user: str,
|
|
69
|
+
):
|
|
70
|
+
# Load model & analysis config
|
|
71
|
+
model_cfg_path = get_model_cfg_path(user, model_id)
|
|
72
|
+
try:
|
|
73
|
+
model_analysis_cfg = load_toml_config(model_cfg_path)
|
|
74
|
+
except Exception:
|
|
75
|
+
model_analysis_cfg = DEFAULT_ANALYSIS_CFG
|
|
76
|
+
# Extract relevant values
|
|
77
|
+
granularity = model_analysis_cfg["model_config"]["granularity"]
|
|
78
|
+
window_size = model_analysis_cfg["model_config"]["window_size"]
|
|
79
|
+
language = model_analysis_cfg["model_config"]["language"]
|
|
80
|
+
if language not in ["French", "English"]:
|
|
81
|
+
language = "French"
|
|
82
|
+
return granularity, window_size, language
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def generate_llm_interpretation(
|
|
86
|
+
bertrend: BERTrend,
|
|
87
|
+
reference_timestamp: pd.Timestamp,
|
|
88
|
+
df: pd.DataFrame,
|
|
89
|
+
df_name: str,
|
|
90
|
+
output_path: Path,
|
|
91
|
+
top_k: int = DEFAULT_TOP_K,
|
|
92
|
+
):
|
|
93
|
+
"""
|
|
94
|
+
Generate detailed analysis for the top k topics using parallel processing.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
bertrend: BERTrend instance
|
|
98
|
+
reference_timestamp: Reference timestamp for analysis
|
|
99
|
+
df: Input DataFrame
|
|
100
|
+
df_name: Name of the DataFrame for output
|
|
101
|
+
output_path: Path to save the results
|
|
102
|
+
top_k: Number of top topics to analyze
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
interpretation = []
|
|
106
|
+
for topic in df.sort_values(by=["Latest_Popularity"], ascending=False).head(top_k)[
|
|
107
|
+
"Topic"
|
|
108
|
+
]:
|
|
109
|
+
summary, analysis = analyze_signal(bertrend, topic, reference_timestamp)
|
|
110
|
+
if not summary or not analysis:
|
|
111
|
+
logger.warning(f"Skipping topic {topic} as analysis of signal failed.")
|
|
112
|
+
continue
|
|
113
|
+
interpretation.append(
|
|
114
|
+
{
|
|
115
|
+
"topic": topic,
|
|
116
|
+
"summary": summary.model_dump_json(),
|
|
117
|
+
"analysis": analysis.model_dump_json(),
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Save interpretation
|
|
122
|
+
output_file_name = output_path / f"{df_name}_interpretation.jsonl"
|
|
123
|
+
with jsonlines.open(
|
|
124
|
+
output_file_name,
|
|
125
|
+
mode="w",
|
|
126
|
+
) as writer:
|
|
127
|
+
for item in interpretation:
|
|
128
|
+
writer.write(item)
|
|
129
|
+
logger.success(f"Interpretation saved to: {output_file_name}")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def train_new_model_for_period(
|
|
133
|
+
model_id: str,
|
|
134
|
+
user_name: str,
|
|
135
|
+
new_data: pd.DataFrame,
|
|
136
|
+
reference_timestamp: pd.Timestamp,
|
|
137
|
+
):
|
|
138
|
+
logger.info(
|
|
139
|
+
f"Training BERTrend model with new data - user: {user_name}, model_id: {model_id}, reference_timestamp: {reference_timestamp}..."
|
|
140
|
+
)
|
|
141
|
+
# Initialization of embedding service
|
|
142
|
+
# TODO: customize service (lang, etc)
|
|
143
|
+
embedding_service = EmbeddingService(local=False)
|
|
144
|
+
|
|
145
|
+
# Path to previously saved models for those data and this user
|
|
146
|
+
bertrend_models_path = get_user_models_path(user_name, model_id)
|
|
147
|
+
|
|
148
|
+
# Get relevant model info from config
|
|
149
|
+
granularity, window_size, language = get_relevant_model_config(
|
|
150
|
+
model_id=model_id, user=user_name
|
|
151
|
+
)
|
|
152
|
+
language_code = "fr" if language == "French" else "en"
|
|
153
|
+
|
|
154
|
+
# Process new data
|
|
155
|
+
bertrend = train_new_data(
|
|
156
|
+
reference_timestamp=reference_timestamp,
|
|
157
|
+
new_data=new_data,
|
|
158
|
+
bertrend_models_path=bertrend_models_path,
|
|
159
|
+
embedding_service=embedding_service,
|
|
160
|
+
language=language,
|
|
161
|
+
granularity=granularity,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if len(bertrend.doc_groups) < 2:
|
|
165
|
+
# This is generally the case when we have only one model
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
# Compute popularities
|
|
169
|
+
bertrend.calculate_signal_popularity()
|
|
170
|
+
|
|
171
|
+
# classify last signals
|
|
172
|
+
cut_off_date = new_data["timestamp"].max() - timedelta(days=granularity)
|
|
173
|
+
noise_topics_df, weak_signal_topics_df, strong_signal_topics_df = (
|
|
174
|
+
bertrend.classify_signals(window_size, cut_off_date)
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# LLM-based interpretation
|
|
178
|
+
interpretation_path = get_model_interpretation_path(
|
|
179
|
+
user_name, model_id, reference_timestamp
|
|
180
|
+
)
|
|
181
|
+
interpretation_path.mkdir(parents=True, exist_ok=True)
|
|
182
|
+
for df, df_name in zip(
|
|
183
|
+
[noise_topics_df, weak_signal_topics_df, strong_signal_topics_df],
|
|
184
|
+
[NOISE, WEAK_SIGNALS, STRONG_SIGNALS],
|
|
185
|
+
):
|
|
186
|
+
if not df.empty:
|
|
187
|
+
# enrich signal description with LLM-based topic description
|
|
188
|
+
df[[LLM_TOPIC_TITLE_COLUMN, LLM_TOPIC_DESCRIPTION_COLUMN]] = df.apply(
|
|
189
|
+
lambda row: pd.Series(
|
|
190
|
+
generate_bertrend_topic_description(
|
|
191
|
+
topic_words=row["Representation"],
|
|
192
|
+
topic_number=row["Topic"],
|
|
193
|
+
texts=row["Documents"],
|
|
194
|
+
language_code=language_code,
|
|
195
|
+
)
|
|
196
|
+
),
|
|
197
|
+
axis=1,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Add documents URL
|
|
201
|
+
df = pd.merge(
|
|
202
|
+
df,
|
|
203
|
+
bertrend.merged_df[["Topic", URLS_COLUMN]],
|
|
204
|
+
on="Topic",
|
|
205
|
+
how="left",
|
|
206
|
+
)
|
|
207
|
+
df[URLS_COLUMN] = df[URLS_COLUMN].apply(
|
|
208
|
+
lambda x: list(set(x))
|
|
209
|
+
) # Removes duplicates within each list
|
|
210
|
+
|
|
211
|
+
# FIXME: for some unknown reasons, a few elements in the Documents column are not a str but a
|
|
212
|
+
# timestamp (the identifier of current model); this generates errors when trying to serialize the
|
|
213
|
+
# df to parquet. The code snippet below is a workaround to avoid this issue.
|
|
214
|
+
df["Documents"] = df["Documents"].apply(
|
|
215
|
+
lambda l: [x if isinstance(x, str) else "" for x in l]
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
output_path = interpretation_path / f"{df_name}.parquet"
|
|
219
|
+
df.to_parquet(output_path)
|
|
220
|
+
logger.success(f"{df_name} saved to: {output_path}")
|
|
221
|
+
|
|
222
|
+
# Obtain detailed LLM-based interpretion for signals
|
|
223
|
+
generate_llm_interpretation(
|
|
224
|
+
bertrend,
|
|
225
|
+
reference_timestamp=reference_timestamp,
|
|
226
|
+
df=df,
|
|
227
|
+
df_name=df_name,
|
|
228
|
+
output_path=interpretation_path,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def regenerate_models(model_id: str, user: str, with_analysis: bool = True):
|
|
233
|
+
"""Regenerate from scratch (method retrospective) the models associated with the specified model identifier
|
|
234
|
+
for the specified user."""
|
|
235
|
+
|
|
236
|
+
# Get relevant model info from config
|
|
237
|
+
granularity, window_size, language = get_relevant_model_config(
|
|
238
|
+
model_id=model_id, user=user
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Load model config
|
|
242
|
+
df = load_all_data(model_id=model_id, user=user, language=language)
|
|
243
|
+
logger.info(f"Size of dataset: {len(df)}")
|
|
244
|
+
|
|
245
|
+
# Split data by paragraphs
|
|
246
|
+
df = split_data(df)
|
|
247
|
+
|
|
248
|
+
# Process new data and save models
|
|
249
|
+
# - Group data based on granularity
|
|
250
|
+
grouped_data = group_by_days(df=df, day_granularity=granularity)
|
|
251
|
+
|
|
252
|
+
if not with_analysis:
|
|
253
|
+
# Path to saved models
|
|
254
|
+
bertrend_models_path = get_user_models_path(user, model_id)
|
|
255
|
+
|
|
256
|
+
# Initialization of embedding service
|
|
257
|
+
# TODO: customize service (lang, etc)
|
|
258
|
+
embedding_service = EmbeddingService(local=False)
|
|
259
|
+
|
|
260
|
+
# Train BERTrend
|
|
261
|
+
bertrend = BERTrend(
|
|
262
|
+
topic_model=BERTopicModel({"global": {"language": language}})
|
|
263
|
+
)
|
|
264
|
+
embeddings, _, _ = embedding_service.embed(
|
|
265
|
+
texts=df[TEXT_COLUMN],
|
|
266
|
+
)
|
|
267
|
+
bertrend.train_topic_models(
|
|
268
|
+
grouped_data=grouped_data,
|
|
269
|
+
embedding_model=embedding_service.embedding_model_name,
|
|
270
|
+
embeddings=embeddings,
|
|
271
|
+
bertrend_models_path=bertrend_models_path,
|
|
272
|
+
save_topic_models=True,
|
|
273
|
+
)
|
|
274
|
+
bertrend.save_model(models_path=bertrend_models_path)
|
|
275
|
+
|
|
276
|
+
logger.success(
|
|
277
|
+
f"Regenerated models for '{model_id}' from scratch. BERTrend model was built using {len(bertrend.doc_groups)} models/time periods."
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
else: # with analysis
|
|
281
|
+
for ts, df in sorted(grouped_data.items()):
|
|
282
|
+
train_new_model_for_period(
|
|
283
|
+
model_id=model_id,
|
|
284
|
+
user_name=user,
|
|
285
|
+
new_data=df.reset_index(drop=True),
|
|
286
|
+
reference_timestamp=ts,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
logger.success(
|
|
290
|
+
f"Regenerated models for '{model_id}' from scratch with LLM-based analysis. BERTrend model was built using {len(grouped_data)} models/time periods."
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
if __name__ == "__main__":
|
|
295
|
+
app = typer.Typer()
|
|
296
|
+
|
|
297
|
+
@app.command("train-new-model")
|
|
298
|
+
def train_new_model(
|
|
299
|
+
user_name: str = typer.Argument(help="Identifier of the user"),
|
|
300
|
+
model_id: str = typer.Argument(help="ID of the model/data to train"),
|
|
301
|
+
):
|
|
302
|
+
"""Incrementally enrich the BERTrend model with new data"""
|
|
303
|
+
logger.info(f'Processing new data for user "{user_name}" about "{model_id}"...')
|
|
304
|
+
|
|
305
|
+
# Get relevant model info from config
|
|
306
|
+
granularity, window_size, language = get_relevant_model_config(
|
|
307
|
+
model_id=model_id, user=user_name
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# Load data for last period
|
|
311
|
+
new_data = load_all_data(model_id=model_id, user=user_name, language=language)
|
|
312
|
+
# filter data according to granularity
|
|
313
|
+
# Calculate the date X days ago
|
|
314
|
+
reference_timestamp = pd.Timestamp(
|
|
315
|
+
new_data["timestamp"].max().date()
|
|
316
|
+
) # used to identify the last model
|
|
317
|
+
cut_off_date = new_data["timestamp"].max() - timedelta(days=granularity)
|
|
318
|
+
# Filter the DataFrame to keep only the rows within the last X days
|
|
319
|
+
filtered_df = new_data[new_data["timestamp"] >= cut_off_date]
|
|
320
|
+
|
|
321
|
+
# Split data by paragraphs
|
|
322
|
+
filtered_df = split_data(filtered_df)
|
|
323
|
+
|
|
324
|
+
train_new_model_for_period(
|
|
325
|
+
model_id=model_id,
|
|
326
|
+
user_name=user_name,
|
|
327
|
+
new_data=filtered_df,
|
|
328
|
+
reference_timestamp=reference_timestamp,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
@app.command("regenerate")
|
|
332
|
+
def regenerate(
|
|
333
|
+
user: str = typer.Argument(help="identifier of the user"),
|
|
334
|
+
model_id: str = typer.Argument(
|
|
335
|
+
help="ID of the model to be regenerated from scratch"
|
|
336
|
+
),
|
|
337
|
+
with_analysis: bool = typer.Option(
|
|
338
|
+
default=True, help="Regenerate LLM analysis (may take time)"
|
|
339
|
+
),
|
|
340
|
+
):
|
|
341
|
+
"""Regenerate past models from scratch"""
|
|
342
|
+
logger.info(
|
|
343
|
+
f"Regenerating models for user '{user}' about '{model_id}', with analysis: {with_analysis}..."
|
|
344
|
+
)
|
|
345
|
+
regenerate_models(model_id=model_id, user=user, with_analysis=with_analysis)
|
|
346
|
+
|
|
347
|
+
# Main app
|
|
348
|
+
app()
|
|
@@ -0,0 +1,214 @@
|
|
|
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 tempfile
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import streamlit as st
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from google.auth.exceptions import RefreshError
|
|
13
|
+
from loguru import logger
|
|
14
|
+
|
|
15
|
+
from bertrend.demos.demos_utils.icons import (
|
|
16
|
+
NEWSLETTER_ICON,
|
|
17
|
+
TOPIC_ICON,
|
|
18
|
+
ERROR_ICON,
|
|
19
|
+
DOWNLOAD_ICON,
|
|
20
|
+
EMAIL_ICON,
|
|
21
|
+
)
|
|
22
|
+
from bertrend_apps.common.mail_utils import get_credentials, send_email
|
|
23
|
+
from bertrend_apps.prospective_demo import (
|
|
24
|
+
WEAK_SIGNALS,
|
|
25
|
+
STRONG_SIGNALS,
|
|
26
|
+
LLM_TOPIC_DESCRIPTION_COLUMN,
|
|
27
|
+
LLM_TOPIC_TITLE_COLUMN,
|
|
28
|
+
URLS_COLUMN,
|
|
29
|
+
)
|
|
30
|
+
from bertrend_apps.prospective_demo.dashboard_common import choose_id_and_ts
|
|
31
|
+
from bertrend_apps.prospective_demo.report_utils import generate_html_report
|
|
32
|
+
|
|
33
|
+
WEAK_SIGNAL_NB = 3
|
|
34
|
+
STRONG_SIGNAL_NB = 5
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def reporting():
|
|
38
|
+
choose_id_and_ts()
|
|
39
|
+
|
|
40
|
+
tab1, tab2 = st.tabs(
|
|
41
|
+
[
|
|
42
|
+
TOPIC_ICON + " Etape 1: Sélection des sujets à retenir",
|
|
43
|
+
NEWSLETTER_ICON + " Etape 2: Export",
|
|
44
|
+
]
|
|
45
|
+
)
|
|
46
|
+
with tab1:
|
|
47
|
+
selected_weak_topics_df, selected_strong_topics_df = choose_topics()
|
|
48
|
+
with tab2:
|
|
49
|
+
configure_export(selected_weak_topics_df, selected_strong_topics_df)
|
|
50
|
+
|
|
51
|
+
# generate_newsletter()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def choose_topics():
|
|
55
|
+
st.subheader("Etape 1: Sélection des sujets à retenir")
|
|
56
|
+
model_id = st.session_state.model_id
|
|
57
|
+
dfs_interpretation = st.session_state.signal_interpretations
|
|
58
|
+
if model_id not in dfs_interpretation:
|
|
59
|
+
st.error(f"{ERROR_ICON} Pas de données")
|
|
60
|
+
st.stop()
|
|
61
|
+
cols = st.columns(2)
|
|
62
|
+
with cols[0]:
|
|
63
|
+
st.write("#### :orange[Sujets émergents]")
|
|
64
|
+
if WEAK_SIGNALS not in dfs_interpretation[model_id]:
|
|
65
|
+
st.error(f"{ERROR_ICON} Pas de données")
|
|
66
|
+
filtered_weak_signals = None
|
|
67
|
+
else:
|
|
68
|
+
df_w = dfs_interpretation[model_id][WEAK_SIGNALS]
|
|
69
|
+
weak_topics_list = choose_from_df(df_w)
|
|
70
|
+
filtered_weak_signals = df_w[df_w["Topic"].isin(weak_topics_list)]
|
|
71
|
+
|
|
72
|
+
with cols[1]:
|
|
73
|
+
st.write("#### :green[Sujets forts]")
|
|
74
|
+
if STRONG_SIGNALS not in dfs_interpretation[model_id]:
|
|
75
|
+
st.error(f"{ERROR_ICON} Pas de données")
|
|
76
|
+
filtered_strong_signals = None
|
|
77
|
+
else:
|
|
78
|
+
df_w = dfs_interpretation[model_id][STRONG_SIGNALS]
|
|
79
|
+
strong_topics_list = choose_from_df(df_w)
|
|
80
|
+
filtered_strong_signals = df_w[df_w["Topic"].isin(strong_topics_list)]
|
|
81
|
+
|
|
82
|
+
return filtered_weak_signals, filtered_strong_signals
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def choose_from_df(df: pd.DataFrame):
|
|
86
|
+
df["A retenir"] = True
|
|
87
|
+
df["Sujet"] = df[LLM_TOPIC_TITLE_COLUMN]
|
|
88
|
+
df["Description"] = df[LLM_TOPIC_DESCRIPTION_COLUMN]
|
|
89
|
+
columns = ["Topic", "A retenir", "Sujet", "Description"]
|
|
90
|
+
pd.DataFrame(
|
|
91
|
+
[
|
|
92
|
+
{"command": "st.selectbox", "rating": 4, "is_widget": True},
|
|
93
|
+
{"command": "st.balloons", "rating": 5, "is_widget": False},
|
|
94
|
+
{"command": "st.time_input", "rating": 3, "is_widget": True},
|
|
95
|
+
]
|
|
96
|
+
)
|
|
97
|
+
edited_df = st.data_editor(df[columns], num_rows="dynamic", column_order=columns)
|
|
98
|
+
selection = edited_df[edited_df["A retenir"] == True]["Topic"].tolist()
|
|
99
|
+
return selection
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def configure_export(weak_signals: pd.DataFrame, strong_signals: pd.DataFrame):
|
|
103
|
+
st.subheader("Etape 2: Configuration de l'export")
|
|
104
|
+
st.write(
|
|
105
|
+
"TODO: séléction des parties à inclure dans l'export par topic: résumé global, liens, évolution par période, analyse détaillée, etc."
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
st.button(
|
|
109
|
+
"Générer",
|
|
110
|
+
type="primary",
|
|
111
|
+
on_click=lambda: generate_report(weak_signals, strong_signals),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@st.dialog("Rapport (aperçu)", width="large")
|
|
116
|
+
def generate_report(weak_signals: pd.DataFrame, strong_signals: pd.DataFrame):
|
|
117
|
+
model_id = st.session_state.model_id
|
|
118
|
+
|
|
119
|
+
# TODO: output à configurer selon les infos choisies
|
|
120
|
+
# rendering parameters
|
|
121
|
+
number_links = 2
|
|
122
|
+
|
|
123
|
+
report_template = Path(__file__).parent / "report_template.html"
|
|
124
|
+
report_title = f"Actu <font color='royal_blue'>{model_id}</font>"
|
|
125
|
+
data_list = []
|
|
126
|
+
for df, color in zip([weak_signals, strong_signals], ["orange", "green"]):
|
|
127
|
+
# Iterate over the filtered DataFrame rows
|
|
128
|
+
for _, row in df.iterrows():
|
|
129
|
+
# For each row, iterate over each column
|
|
130
|
+
data_list.append(
|
|
131
|
+
{
|
|
132
|
+
"topic_title": row[LLM_TOPIC_TITLE_COLUMN],
|
|
133
|
+
"topic_description": row[LLM_TOPIC_DESCRIPTION_COLUMN],
|
|
134
|
+
"color": color,
|
|
135
|
+
"links": list(set(row[URLS_COLUMN]))[:number_links],
|
|
136
|
+
"analysis": row["analysis"],
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Generate the HTML
|
|
141
|
+
output_html = generate_html_report(report_template, report_title, data_list)
|
|
142
|
+
|
|
143
|
+
# Render HTML
|
|
144
|
+
with st.container(height=475):
|
|
145
|
+
st.html(output_html)
|
|
146
|
+
|
|
147
|
+
# Save report to temp file
|
|
148
|
+
temp_report_path = create_temp_report(output_html) # Create the file in temp dir
|
|
149
|
+
|
|
150
|
+
cols = st.columns([2, 3])
|
|
151
|
+
with cols[0]:
|
|
152
|
+
download(temp_report_path, model_id)
|
|
153
|
+
with cols[1]:
|
|
154
|
+
email(temp_report_path, mail_title=f"Rapport veille {model_id}") #
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def create_temp_report(html_content) -> Path:
|
|
158
|
+
with tempfile.NamedTemporaryFile(
|
|
159
|
+
mode="w", suffix=".html", delete=False
|
|
160
|
+
) as temp_file:
|
|
161
|
+
temp_file.write(html_content)
|
|
162
|
+
return Path(temp_file.name)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def download(temp_path: Path, model_id: str):
|
|
166
|
+
with open(temp_path, "r", encoding="utf-8") as file:
|
|
167
|
+
st.download_button(
|
|
168
|
+
label=f"{DOWNLOAD_ICON} Télécharger (html)",
|
|
169
|
+
type="primary",
|
|
170
|
+
data=file.read(),
|
|
171
|
+
file_name=f"rapport_{model_id}.html",
|
|
172
|
+
mime="text/html",
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def is_valid_email(email: str) -> bool:
|
|
177
|
+
"""Checks if an email address is valid using a regular expression."""
|
|
178
|
+
regex = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
|
|
179
|
+
return re.match(regex, email) is not None
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def email(temp_path: Path, mail_title: str) -> None:
|
|
183
|
+
col1, col2 = st.columns([0.6, 0.4])
|
|
184
|
+
with col1:
|
|
185
|
+
user_email = st.text_input("@", label_visibility="collapsed")
|
|
186
|
+
|
|
187
|
+
with col2:
|
|
188
|
+
if st.button(f"{EMAIL_ICON} Envoyer", type="primary"):
|
|
189
|
+
if not user_email:
|
|
190
|
+
return
|
|
191
|
+
if not is_valid_email(user_email):
|
|
192
|
+
st.error(f"{ERROR_ICON} Adresse mail incorrecte")
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
# Send newsletter by email
|
|
197
|
+
# string to list conversion for recipients
|
|
198
|
+
recipients = [user_email]
|
|
199
|
+
try:
|
|
200
|
+
if recipients:
|
|
201
|
+
credentials = get_credentials()
|
|
202
|
+
with open(temp_path, "r") as file:
|
|
203
|
+
# Read the entire contents of the file into a string
|
|
204
|
+
content = file.read()
|
|
205
|
+
send_email(credentials, mail_title, recipients, content, "html")
|
|
206
|
+
except RefreshError as re:
|
|
207
|
+
logger.error(
|
|
208
|
+
f"Problem with token for email, please regenerate it: {re}"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
st.success("Email sent successfully!")
|
|
212
|
+
|
|
213
|
+
except Exception as e:
|
|
214
|
+
st.error(f"Error sending email: {e}")
|