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,10 @@
|
|
|
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 Wattelse, a NLP application suite.
|
|
5
|
+
|
|
6
|
+
import streamlit as st
|
|
7
|
+
|
|
8
|
+
st.set_page_config(layout="wide")
|
|
9
|
+
|
|
10
|
+
st.write("Select a page")
|
|
@@ -0,0 +1,105 @@
|
|
|
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 Wattelse, a NLP application suite.
|
|
5
|
+
import math
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import streamlit as st
|
|
9
|
+
import pydeck as pdk
|
|
10
|
+
|
|
11
|
+
from bertrend.utils.data_loading import TIMESTAMP_COLUMN
|
|
12
|
+
from bertrend_apps.exploration.geolocalization.entity_utils import (
|
|
13
|
+
geojson_to_dataframe,
|
|
14
|
+
geolocalize_data,
|
|
15
|
+
)
|
|
16
|
+
from bertrend_apps.exploration.geolocalization.spacy.utils import load_nlp
|
|
17
|
+
|
|
18
|
+
GEODATA_COLUM = "geodata"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_coords(geojson_data):
|
|
22
|
+
geo_df = geojson_to_dataframe(geojson_data)
|
|
23
|
+
# selection of overall coordinates for the whole doc
|
|
24
|
+
# TODO: heuristics to be defined, basic example below (doc location represented by the one of its first entity)
|
|
25
|
+
if len(geo_df) > 0:
|
|
26
|
+
return (
|
|
27
|
+
geo_df.iloc[0]["latitude"],
|
|
28
|
+
geo_df.iloc[0]["longitude"],
|
|
29
|
+
geo_df.iloc[0]["label"],
|
|
30
|
+
)
|
|
31
|
+
else:
|
|
32
|
+
return math.nan, math.nan, math.nan
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def plot_geojson_on_map(df: pd.DataFrame):
|
|
36
|
+
# Use st.map to plot GeoJSON data
|
|
37
|
+
# Convert the GeoDataFrame to a DataFrame
|
|
38
|
+
df[["latitude", "longitude", "label"]] = (
|
|
39
|
+
df[GEODATA_COLUM].apply(get_coords).apply(pd.Series)
|
|
40
|
+
)
|
|
41
|
+
print(df)
|
|
42
|
+
st.map(df.dropna())
|
|
43
|
+
# st.pydeck_chart(create_pydeck_chart(df.dropna()))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def create_pydeck_chart(geo_dataframe: pd.DataFrame):
|
|
47
|
+
# Create a PyDeck layer with lat,lon data
|
|
48
|
+
layer = pdk.Layer(
|
|
49
|
+
"ScatterplotLayer",
|
|
50
|
+
data=geo_dataframe,
|
|
51
|
+
get_position="[longitute, latitude]",
|
|
52
|
+
get_color="[255, 0, 0]",
|
|
53
|
+
get_radius=1000,
|
|
54
|
+
pickable=True,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Create a PyDeck view
|
|
58
|
+
view_state = pdk.ViewState(
|
|
59
|
+
longitude=geo_dataframe["longitude"].mean(),
|
|
60
|
+
latitude=geo_dataframe["latitude"].mean(),
|
|
61
|
+
zoom=5,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Create a PyDeck deck
|
|
65
|
+
deck = pdk.Deck(
|
|
66
|
+
map_style="mapbox://styles/mapbox/light-v9",
|
|
67
|
+
layers=[layer],
|
|
68
|
+
initial_view_state=view_state,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return deck
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@st.cache_resource
|
|
75
|
+
def load_nlp_wrapper():
|
|
76
|
+
return load_nlp()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
nlp = load_nlp_wrapper()
|
|
80
|
+
|
|
81
|
+
st.title("Browse data")
|
|
82
|
+
|
|
83
|
+
# Load selected DataFrame
|
|
84
|
+
choose_data(DATA_DIR, ["*.csv", "*.jsonl*"])
|
|
85
|
+
|
|
86
|
+
df = (
|
|
87
|
+
load_data_wrapper(
|
|
88
|
+
f"{st.session_state['data_folder']}/{st.session_state['data_name']}"
|
|
89
|
+
)
|
|
90
|
+
.sort_values(by=TIMESTAMP_COLUMN, ascending=False)
|
|
91
|
+
.reset_index(drop=True)
|
|
92
|
+
).reset_index()
|
|
93
|
+
|
|
94
|
+
with st.expander("Data content"):
|
|
95
|
+
st.dataframe(df, hide_index=True)
|
|
96
|
+
|
|
97
|
+
# data overview
|
|
98
|
+
data_overview(df)
|
|
99
|
+
|
|
100
|
+
# Géolocalization of data
|
|
101
|
+
if st.button("Geolocalize data"):
|
|
102
|
+
with st.expander("Data geolocalization"):
|
|
103
|
+
with st.spinner("Extraction of locations from text"):
|
|
104
|
+
df[GEODATA_COLUM] = geolocalize_data(nlp, df)
|
|
105
|
+
plot_geojson_on_map(df)
|
|
@@ -0,0 +1,118 @@
|
|
|
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 Wattelse, a NLP application suite.
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
from SPARQLWrapper import SPARQLWrapper, JSON
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
from loguru import logger
|
|
12
|
+
from tqdm import tqdm
|
|
13
|
+
|
|
14
|
+
TEXT_COLUMN = "text"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def geolocalize_data(nlp, df: pd.DataFrame) -> pd.DataFrame:
|
|
18
|
+
"""Creates geojson data associated to text entries of the dataframe"""
|
|
19
|
+
text_col = "processed_text" if "processed_text" in df.columns else TEXT_COLUMN
|
|
20
|
+
logger.info("Geolocalizing data...")
|
|
21
|
+
geojson_data_list = []
|
|
22
|
+
|
|
23
|
+
for doc in tqdm(nlp.pipe(df[text_col], n_process=os.cpu_count() - 1)):
|
|
24
|
+
# DBPedia resources
|
|
25
|
+
resources = doc._.dbpedia_raw_result.get("Resources")
|
|
26
|
+
features = []
|
|
27
|
+
if resources:
|
|
28
|
+
for r in resources:
|
|
29
|
+
if "DBpedia:Location" in r["@types"].split(","):
|
|
30
|
+
features += get_geojson_features_from_dbpedia_resource(r["@URI"])
|
|
31
|
+
geojson_data = {"type": "FeatureCollection", "features": features}
|
|
32
|
+
geojson_data_list.append(geojson_data)
|
|
33
|
+
logger.debug(geojson_data)
|
|
34
|
+
return geojson_data_list
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def geojson_to_dataframe(geojson_data) -> pd.DataFrame:
|
|
38
|
+
features = geojson_data["features"]
|
|
39
|
+
|
|
40
|
+
# Initialize lists to store latitude, longitude, and label data
|
|
41
|
+
latitudes = []
|
|
42
|
+
longitudes = []
|
|
43
|
+
labels = []
|
|
44
|
+
|
|
45
|
+
# Extract data from GeoJSON features
|
|
46
|
+
for feature in features:
|
|
47
|
+
geometry = feature["geometry"]
|
|
48
|
+
properties = feature["properties"]
|
|
49
|
+
|
|
50
|
+
# Extract latitude and longitude
|
|
51
|
+
latitude, longitude = geometry["coordinates"]
|
|
52
|
+
latitudes.append(latitude)
|
|
53
|
+
longitudes.append(longitude)
|
|
54
|
+
|
|
55
|
+
# Extract label (if available)
|
|
56
|
+
label = properties.get("label", None)
|
|
57
|
+
labels.append(label)
|
|
58
|
+
|
|
59
|
+
# Create a DataFrame
|
|
60
|
+
df = pd.DataFrame({"latitude": latitudes, "longitude": longitudes, "label": labels})
|
|
61
|
+
|
|
62
|
+
return df
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_geojson_features_from_dbpedia_resource(resource_uri):
|
|
66
|
+
# Define the DBpedia SPARQL endpoint
|
|
67
|
+
sparql_endpoint = "http://fr.dbpedia.org/sparql"
|
|
68
|
+
|
|
69
|
+
# Set up SPARQLWrapper
|
|
70
|
+
sparql = SPARQLWrapper(sparql_endpoint)
|
|
71
|
+
|
|
72
|
+
# Define the SPARQL query to retrieve GeoJSON data
|
|
73
|
+
sparql_query = f"""
|
|
74
|
+
PREFIX geo: <http://www.w3.org/2003/01/geo/wgs84_pos#>
|
|
75
|
+
SELECT DISTINCT ?subject ?lat ?long ?label
|
|
76
|
+
WHERE {{
|
|
77
|
+
<{resource_uri}> geo:lat ?lat ;
|
|
78
|
+
geo:long ?long ;
|
|
79
|
+
rdfs:label ?label .
|
|
80
|
+
}}
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
# Set the SPARQL query and request JSON format
|
|
84
|
+
sparql.setQuery(sparql_query)
|
|
85
|
+
sparql.setReturnFormat(JSON)
|
|
86
|
+
|
|
87
|
+
# Execute the SPARQL query
|
|
88
|
+
results = sparql.query().convert()
|
|
89
|
+
|
|
90
|
+
# Process the results and create GeoJSON
|
|
91
|
+
features = []
|
|
92
|
+
for result in results["results"]["bindings"]:
|
|
93
|
+
feature = {
|
|
94
|
+
"type": "Feature",
|
|
95
|
+
"geometry": {
|
|
96
|
+
"type": "Point",
|
|
97
|
+
"coordinates": [
|
|
98
|
+
float(result["long"]["value"]),
|
|
99
|
+
float(result["lat"]["value"]),
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
"properties": {"label": result["label"]["value"]},
|
|
103
|
+
}
|
|
104
|
+
features.append(feature)
|
|
105
|
+
break # to avoid similar entries (ex. "Berlin" in different languages)
|
|
106
|
+
|
|
107
|
+
return features
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
# Specify the DBpedia resource URI
|
|
112
|
+
dbpedia_resource_uri = "http://fr.dbpedia.org/resource/Berlin"
|
|
113
|
+
|
|
114
|
+
# Get GeoJSON data for the specified resource
|
|
115
|
+
geojson_data = get_geojson_features_from_dbpedia_resource(dbpedia_resource_uri)
|
|
116
|
+
|
|
117
|
+
# Print the GeoJSON data
|
|
118
|
+
print(json.dumps(geojson_data, indent=2))
|
|
@@ -0,0 +1,142 @@
|
|
|
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 Wattelse, a NLP application suite.
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
import unicodedata
|
|
8
|
+
from typing import List, Union
|
|
9
|
+
|
|
10
|
+
from loguru import logger
|
|
11
|
+
from spacy import Language
|
|
12
|
+
from spacy.tokens import Token
|
|
13
|
+
|
|
14
|
+
from bertrend_apps.exploration.geolocalization.spacy.dbpedia import DBPediaEntityLinker
|
|
15
|
+
|
|
16
|
+
RE_NORM_APOSTROPHE_DASH = re.compile(r"(-|l'|d')")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@Language.factory("normalizer_component")
|
|
20
|
+
def create_normalizer_component(nlp, name):
|
|
21
|
+
return NormalizerComponent()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@Language.factory("tokenizer_component")
|
|
25
|
+
def create_tokenizer_component(nlp, name):
|
|
26
|
+
return TokenizerComponent()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@Language.factory(
|
|
30
|
+
"DBpedia_spotlight",
|
|
31
|
+
default_config={
|
|
32
|
+
"language_code": "fr",
|
|
33
|
+
"dbpedia_rest_endpoint": None,
|
|
34
|
+
"process": "annotate",
|
|
35
|
+
"confidence": 0.5,
|
|
36
|
+
"support": "",
|
|
37
|
+
"types": None,
|
|
38
|
+
"sparql": "",
|
|
39
|
+
"policy": "",
|
|
40
|
+
"span_group": "dbpedia_spotlight",
|
|
41
|
+
"overwrite_ents": True,
|
|
42
|
+
},
|
|
43
|
+
)
|
|
44
|
+
def dbpedia_spotlight_factory(
|
|
45
|
+
nlp: Language,
|
|
46
|
+
name: str,
|
|
47
|
+
language_code: str,
|
|
48
|
+
dbpedia_rest_endpoint: str,
|
|
49
|
+
process: str,
|
|
50
|
+
confidence: float,
|
|
51
|
+
support: str,
|
|
52
|
+
types: List[str],
|
|
53
|
+
sparql: str,
|
|
54
|
+
policy: str,
|
|
55
|
+
span_group: str,
|
|
56
|
+
overwrite_ents: bool,
|
|
57
|
+
):
|
|
58
|
+
"""Factory of the pipeline stage `dbpedia_spotlight`.
|
|
59
|
+
Parameters:
|
|
60
|
+
- `language_code`: which language to use for entity linking. Possible values are listed in EntityLinker.supported_languages. If the parameter is left as None, the language code is matched with the nlp object currently used.
|
|
61
|
+
- `dbpedia_rest_endpoint`: this needs to be configured if you want to use a different REST endpoint from the default `EntityLinker.base_url`. Example: `http://localhost:2222/rest` for a localhost server
|
|
62
|
+
- `process`: (REST API path) which of the processes to use from DBpedia Spotlight (see https://www.dbpedia-spotlight.org/api). The value can be 'annotate', 'spot' or 'candidates'
|
|
63
|
+
- `confidence`: (REST API parameter) confidence score for disambiguation / linking
|
|
64
|
+
- `support`: (REST API parameter) how prominent is this entity in Lucene Model, i.e. number of inlinks in Wikipedia
|
|
65
|
+
- `types`: (REST API parameter) types filter (Eg.DBpedia:Place)
|
|
66
|
+
- `sparql`: (REST API parameter) SPARQL filtering
|
|
67
|
+
- `policy`: (REST API parameter) (whitelist) select all entities that have the same type; (blacklist) - select all entities that have not the same type.
|
|
68
|
+
- `span_group`: which span group to write the entities to. By default the value is `dbpedia_spotlight` which writes to `doc.spans['dbpedia_spotlight']`
|
|
69
|
+
- `overwrite_ents`: if set to False, it won't overwrite `doc.ents` in cases of overlapping spans with current entities, and only produce the results in `doc.spans[span_group]. If it is True, it will move the entities from doc.ents into `doc.spans['ents_original']`
|
|
70
|
+
"""
|
|
71
|
+
logger.debug(
|
|
72
|
+
f"dbpedia_spotlight_factory: {nlp}, language_code {language_code}, "
|
|
73
|
+
f"dbpedia_rest_endpoint {dbpedia_rest_endpoint}, process {process}, "
|
|
74
|
+
f"confidence {confidence}, support {support}, types {types}, "
|
|
75
|
+
f"sparql {sparql}, policy {policy}, overwrite_ents {overwrite_ents}"
|
|
76
|
+
)
|
|
77
|
+
# take the language code from the nlp object
|
|
78
|
+
nlp_lang_code = nlp.meta["lang"]
|
|
79
|
+
# language_code can override the language code from the nlp object
|
|
80
|
+
if not language_code:
|
|
81
|
+
language_code = nlp_lang_code
|
|
82
|
+
return DBPediaEntityLinker(
|
|
83
|
+
language_code,
|
|
84
|
+
dbpedia_rest_endpoint,
|
|
85
|
+
process,
|
|
86
|
+
confidence,
|
|
87
|
+
support,
|
|
88
|
+
types,
|
|
89
|
+
sparql,
|
|
90
|
+
policy,
|
|
91
|
+
span_group,
|
|
92
|
+
overwrite_ents,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class NormalizerComponent:
|
|
97
|
+
"""
|
|
98
|
+
Do some custom normalization steps
|
|
99
|
+
WARNING: This DOES NOT modify utils length (applied to .norm_ token attribute)
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __call__(self, doc):
|
|
103
|
+
for token in doc:
|
|
104
|
+
norm = normalize(token)
|
|
105
|
+
token.norm_ = norm
|
|
106
|
+
return doc
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class TokenizerComponent:
|
|
110
|
+
"""Split tokens by apostrophes and dashes"""
|
|
111
|
+
|
|
112
|
+
def __call__(self, doc):
|
|
113
|
+
with doc.retokenize() as retokenizer:
|
|
114
|
+
for token in doc:
|
|
115
|
+
text = token.text
|
|
116
|
+
if len(text) > 1 and RE_NORM_APOSTROPHE_DASH.search(text):
|
|
117
|
+
elts = RE_NORM_APOSTROPHE_DASH.split(text)
|
|
118
|
+
elts = list(filter(bool, elts))
|
|
119
|
+
heads = [(token, i) for i in range(len(elts))]
|
|
120
|
+
retokenizer.split(token, elts, heads=heads)
|
|
121
|
+
return doc
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def strip_accents(s: str) -> str:
|
|
125
|
+
# From https://stackoverflow.com/questions/517923/what-is-the-best-way-to-remove-accents-normalize-in-a-python-unicode-string
|
|
126
|
+
return "".join(
|
|
127
|
+
c for c in unicodedata.normalize("NFD", s) if unicodedata.category(c) != "Mn"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def normalize(s: Union[str, Token]) -> str:
|
|
132
|
+
if isinstance(s, Token):
|
|
133
|
+
norm = s.lower_
|
|
134
|
+
elif isinstance(s, str):
|
|
135
|
+
norm = s.lower()
|
|
136
|
+
norm = strip_accents(norm)
|
|
137
|
+
norm = norm.strip()
|
|
138
|
+
if norm == "saint":
|
|
139
|
+
norm = "st"
|
|
140
|
+
if norm == "sainte":
|
|
141
|
+
norm = "ste"
|
|
142
|
+
return norm
|
|
@@ -0,0 +1,206 @@
|
|
|
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 Wattelse, a NLP application suite.
|
|
5
|
+
|
|
6
|
+
# NB. Largely inspired from https://github.com/MartinoMensio/spacy-dbpedia-spotlight (MIT license)
|
|
7
|
+
|
|
8
|
+
import spacy
|
|
9
|
+
import requests
|
|
10
|
+
from loguru import logger
|
|
11
|
+
from requests import HTTPError
|
|
12
|
+
|
|
13
|
+
from spacy.tokens import Span, Doc
|
|
14
|
+
|
|
15
|
+
DBPEDIA_SPOTLIGHT_DEFAULT_ENDPOINT = "https://api.dbpedia-spotlight.org"
|
|
16
|
+
DBPEDIA_ENT = "DBPedia"
|
|
17
|
+
|
|
18
|
+
# span extension attribute for raw json
|
|
19
|
+
Span.set_extension("dbpedia_raw_result", default=None, force=True)
|
|
20
|
+
Doc.set_extension("dbpedia_raw_result", default=None, force=True)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DBPediaEntityLinker(object):
|
|
24
|
+
"""This class manages the querying of DBpedia and attaches the found entities to the document"""
|
|
25
|
+
|
|
26
|
+
# default location of the service
|
|
27
|
+
base_url = DBPEDIA_SPOTLIGHT_DEFAULT_ENDPOINT
|
|
28
|
+
# list of supported languages
|
|
29
|
+
supported_languages = ["en", "de", "es", "fr", "it", "nl", "pt", "ru"]
|
|
30
|
+
# list of supported processes
|
|
31
|
+
supported_processes = ["annotate", "spot", "candidates"]
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
language_code="en",
|
|
36
|
+
dbpedia_rest_endpoint=None,
|
|
37
|
+
process="annotate",
|
|
38
|
+
confidence=None,
|
|
39
|
+
support=None,
|
|
40
|
+
types=None,
|
|
41
|
+
sparql=None,
|
|
42
|
+
policy=None,
|
|
43
|
+
span_group=DBPEDIA_ENT,
|
|
44
|
+
overwrite_ents=True,
|
|
45
|
+
):
|
|
46
|
+
# constructor of the pipeline stage
|
|
47
|
+
if language_code not in self.supported_languages:
|
|
48
|
+
raise ValueError(
|
|
49
|
+
f"Linker not available in {language_code}. Choose one of {self.supported_languages}"
|
|
50
|
+
)
|
|
51
|
+
self.language_code = language_code
|
|
52
|
+
if process not in self.supported_processes:
|
|
53
|
+
raise ValueError(
|
|
54
|
+
f"The process {process} is not supported. Choose one of {self.supported_processes}"
|
|
55
|
+
)
|
|
56
|
+
self.process = process
|
|
57
|
+
self.confidence = confidence
|
|
58
|
+
self.support = support
|
|
59
|
+
self.types = types
|
|
60
|
+
self.sparql = sparql
|
|
61
|
+
self.policy = policy
|
|
62
|
+
self.span_group = span_group
|
|
63
|
+
self.overwrite_ents = overwrite_ents
|
|
64
|
+
self.dbpedia_rest_endpoint = dbpedia_rest_endpoint
|
|
65
|
+
|
|
66
|
+
def get_uri(self, el):
|
|
67
|
+
# fields have different names depending on the process
|
|
68
|
+
if self.process == "annotate":
|
|
69
|
+
return el["@URI"]
|
|
70
|
+
elif self.process == "candidates":
|
|
71
|
+
return f"http://dbpedia.org/resource/{el['resource']['@uri']}"
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
def get_ents_list(self, json):
|
|
75
|
+
# fields have different names depending on the process
|
|
76
|
+
if self.process == "annotate":
|
|
77
|
+
return json.get("Resources", [])
|
|
78
|
+
return json.get("annotation", {}).get("surfaceForm", [])
|
|
79
|
+
|
|
80
|
+
def __call__(self, doc):
|
|
81
|
+
# called in the pipeline
|
|
82
|
+
if self.dbpedia_rest_endpoint:
|
|
83
|
+
# override the default endpoint, e.g., 'http://localhost:2222/rest'
|
|
84
|
+
endpoint = self.dbpedia_rest_endpoint
|
|
85
|
+
else:
|
|
86
|
+
# use the default endpoint for the language selected
|
|
87
|
+
endpoint = f"{self.base_url}/{self.language_code}"
|
|
88
|
+
|
|
89
|
+
params = {"text": doc.text}
|
|
90
|
+
if self.confidence:
|
|
91
|
+
params["confidence"] = self.confidence
|
|
92
|
+
if self.support:
|
|
93
|
+
params["support"] = self.support
|
|
94
|
+
if self.types:
|
|
95
|
+
params["types"] = ",".join(
|
|
96
|
+
self.types
|
|
97
|
+
) # JP: for proper formatting in the API
|
|
98
|
+
if self.sparql:
|
|
99
|
+
params["sparql"] = self.sparql
|
|
100
|
+
if self.policy:
|
|
101
|
+
params["policy"] = self.policy
|
|
102
|
+
|
|
103
|
+
# TODO: application/ld+json would be more detailed? https://github.com/digitalbazaar/pyld
|
|
104
|
+
try:
|
|
105
|
+
response = requests.post(
|
|
106
|
+
f"{endpoint}/{self.process}",
|
|
107
|
+
headers={"accept": "application/json"},
|
|
108
|
+
data=params,
|
|
109
|
+
)
|
|
110
|
+
except HTTPError as e:
|
|
111
|
+
# due to too many requests to the endpoint - this happens sometimes with the default public endpoint
|
|
112
|
+
logger.warn(
|
|
113
|
+
f"Bad response from server {endpoint}, probably too many requests. Consider using your own endpoint. Document not updated."
|
|
114
|
+
)
|
|
115
|
+
logger.trace(str(e))
|
|
116
|
+
return doc
|
|
117
|
+
except Exception as e: # other erros
|
|
118
|
+
logger.error(
|
|
119
|
+
f"Endpoint {endpoint} unreachable, please check your connection. Document not updated."
|
|
120
|
+
)
|
|
121
|
+
logger.trace(str(e))
|
|
122
|
+
return doc
|
|
123
|
+
|
|
124
|
+
response.raise_for_status()
|
|
125
|
+
data = response.json()
|
|
126
|
+
logger.trace(f"Received data: {data}")
|
|
127
|
+
|
|
128
|
+
doc._.dbpedia_raw_result = data
|
|
129
|
+
|
|
130
|
+
ents_data = []
|
|
131
|
+
|
|
132
|
+
# fields have different names depending on the process
|
|
133
|
+
text_key = "@name"
|
|
134
|
+
# get_offset
|
|
135
|
+
if self.process == "annotate":
|
|
136
|
+
text_key = "@surfaceForm"
|
|
137
|
+
|
|
138
|
+
for ent in self.get_ents_list(data):
|
|
139
|
+
start_ch = int(ent["@offset"])
|
|
140
|
+
end_ch = int(start_ch + len(ent[text_key]))
|
|
141
|
+
ent_kb_id = self.get_uri(ent)
|
|
142
|
+
# TODO look at '@types' and choose most relevant?
|
|
143
|
+
# JP: adaptation to indicate the type instead of a generic span type when possible.
|
|
144
|
+
# In case of a span matching several types, we retain the first one (the assumption is that types
|
|
145
|
+
# are ordered by decreasing preference)
|
|
146
|
+
if not self.types:
|
|
147
|
+
# set a default span type
|
|
148
|
+
span_type = DBPEDIA_ENT
|
|
149
|
+
else:
|
|
150
|
+
# takes the first one that matches
|
|
151
|
+
for t in self.types:
|
|
152
|
+
if t in ent["@types"]:
|
|
153
|
+
span_type = t
|
|
154
|
+
break
|
|
155
|
+
|
|
156
|
+
# if ent_kb_id:
|
|
157
|
+
# span = doc.char_span(start_ch, end_ch, span_type, ent_kb_id)
|
|
158
|
+
span = doc.char_span(start_ch, end_ch, span_type)
|
|
159
|
+
|
|
160
|
+
if not span:
|
|
161
|
+
# something strange like "something@bbc.co.uk" where the match is only part of a SpaCy token
|
|
162
|
+
# 1. find the token to split
|
|
163
|
+
# tokens also wider than start_ch, end_ch
|
|
164
|
+
tokens_to_split = [
|
|
165
|
+
t for t in doc if t.idx >= start_ch or t.idx + len(t) <= end_ch
|
|
166
|
+
]
|
|
167
|
+
span = doc.char_span(
|
|
168
|
+
min(t.idx for t in tokens_to_split),
|
|
169
|
+
max(t.idx + len(t) for t in tokens_to_split),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
span._.dbpedia_raw_result = ent
|
|
173
|
+
|
|
174
|
+
ents_data.append(span)
|
|
175
|
+
|
|
176
|
+
# try to add results to doc.ents
|
|
177
|
+
try:
|
|
178
|
+
# NB. Order is important here for precedence of infra entities over other entities
|
|
179
|
+
doc.ents = list(doc.ents) + ents_data
|
|
180
|
+
except Exception as e:
|
|
181
|
+
logger.trace(str(e))
|
|
182
|
+
if self.overwrite_ents:
|
|
183
|
+
# overwrite ok
|
|
184
|
+
doc.spans["ents_original"] = doc.ents
|
|
185
|
+
try:
|
|
186
|
+
doc.ents = ents_data
|
|
187
|
+
except (
|
|
188
|
+
ValueError
|
|
189
|
+
): # if there are overlapping spans in the dbpedia_spotlight entities
|
|
190
|
+
doc.ents = spacy.util.filter_spans(ents_data)
|
|
191
|
+
# else don't overwrite
|
|
192
|
+
|
|
193
|
+
doc.spans[self.span_group] = ents_data
|
|
194
|
+
|
|
195
|
+
return doc
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def create(language_code, nlp=None):
|
|
199
|
+
"""Creates an instance of a Language with the DBpedia EntityLinker pipeline stage.
|
|
200
|
+
If the parameter `nlp` is None, it will return a blank language with the EntityLinker.
|
|
201
|
+
If the parameter `nlp` is an existing Language, it simply adds the EntityLinker pipeline stage (equivalent to `nlp.add`)
|
|
202
|
+
"""
|
|
203
|
+
if not nlp:
|
|
204
|
+
nlp = spacy.blank(language_code)
|
|
205
|
+
nlp.add_pipe("dbpedia_spotlight")
|
|
206
|
+
return nlp
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[paths]
|
|
2
|
+
train = null
|
|
3
|
+
dev = null
|
|
4
|
+
vectors = null
|
|
5
|
+
init_tok2vec = null
|
|
6
|
+
|
|
7
|
+
[system]
|
|
8
|
+
gpu_allocator = "pytorch"
|
|
9
|
+
seed = 0
|
|
10
|
+
|
|
11
|
+
[nlp]
|
|
12
|
+
lang = "fr"
|
|
13
|
+
batch_size = 128
|
|
14
|
+
disabled = []
|
|
15
|
+
before_creation = null
|
|
16
|
+
after_creation = null
|
|
17
|
+
after_pipeline_creation = null
|
|
18
|
+
tokenizer = {"@tokenizers":"spacy.Tokenizer.v1"}
|
|
19
|
+
|
|
20
|
+
pipeline = ["tokenizer_component", "normalizer_component",
|
|
21
|
+
"dbpedia"]
|
|
22
|
+
|
|
23
|
+
[components]
|
|
24
|
+
|
|
25
|
+
[components.tokenizer_component]
|
|
26
|
+
factory = "tokenizer_component"
|
|
27
|
+
|
|
28
|
+
[components.normalizer_component]
|
|
29
|
+
factory = "normalizer_component"
|
|
30
|
+
|
|
31
|
+
[components.dbpedia]
|
|
32
|
+
factory = "DBpedia_spotlight"
|
|
33
|
+
overwrite_ents = false
|
|
34
|
+
language_code= "fr"
|
|
35
|
+
confidence = 0.3
|
|
36
|
+
types = ["DBpedia:Place", "DBpedia:Organisation"]
|
|
37
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
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 Wattelse, a NLP application suite.
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Union
|
|
10
|
+
|
|
11
|
+
from loguru import logger
|
|
12
|
+
from spacy.lang.fr import French
|
|
13
|
+
from thinc.config import Config
|
|
14
|
+
|
|
15
|
+
from bertrend_apps.exploration.geolocalization.spacy import common_factory
|
|
16
|
+
|
|
17
|
+
# Those imports are not explicitly used, but mandatory to have a working nlp pipeline... - DO NOT REMOVE!
|
|
18
|
+
|
|
19
|
+
ASSETS_PATH = Path(__file__).parent.absolute()
|
|
20
|
+
RTE_NLP_CONFIG_PATH = ASSETS_PATH / "nlp.config"
|
|
21
|
+
DBPEDIA_REST_API_URL = os.getenv("DBPEDIA_REST_API_URL", "http://localhost:2222/rest")
|
|
22
|
+
|
|
23
|
+
# These logs are just there to force the imports of the factories - DO NOT REMOVE!
|
|
24
|
+
logger.debug(f"{common_factory.__name__} loaded.")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def load_nlp(config_path: Union[str, Path] = RTE_NLP_CONFIG_PATH, **kwargs):
|
|
28
|
+
t1 = time.time()
|
|
29
|
+
logger.info("Building nlp...")
|
|
30
|
+
config = Config().from_disk(
|
|
31
|
+
config_path,
|
|
32
|
+
overrides={"components.dbpedia.dbpedia_rest_endpoint": DBPEDIA_REST_API_URL},
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
nlp = French.from_config(config, **kwargs)
|
|
36
|
+
t2 = time.time()
|
|
37
|
+
|
|
38
|
+
logger.success(f"NLP pipeline built in {t2 - t1:3f} seconds!")
|
|
39
|
+
return nlp
|