bertrend 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. bertrend/BERTopicModel.py +320 -0
  2. bertrend/BERTrend.py +1211 -0
  3. bertrend/__init__.py +59 -0
  4. bertrend/config/bertopic_default_config.toml +48 -0
  5. bertrend/config/bertrend_default_config.toml +16 -0
  6. bertrend/config/parameters.py +92 -0
  7. bertrend/config/services_default_config.toml +32 -0
  8. bertrend/demos/__init__.py +4 -0
  9. bertrend/demos/demos_utils/__init__.py +13 -0
  10. bertrend/demos/demos_utils/data_loading_component.py +267 -0
  11. bertrend/demos/demos_utils/embed_documents_component.py +52 -0
  12. bertrend/demos/demos_utils/icons.py +41 -0
  13. bertrend/demos/demos_utils/messages.py +11 -0
  14. bertrend/demos/demos_utils/parameters_component.py +162 -0
  15. bertrend/demos/demos_utils/state_utils.py +99 -0
  16. bertrend/demos/start_summarizer_app.sh +9 -0
  17. bertrend/demos/start_topic_analysis_app.sh +9 -0
  18. bertrend/demos/start_weak_signals_app.sh +9 -0
  19. bertrend/demos/summarization/__init__.py +4 -0
  20. bertrend/demos/summarization/summarizer_app.py +98 -0
  21. bertrend/demos/topic_analysis/__init__.py +4 -0
  22. bertrend/demos/topic_analysis/app.py +87 -0
  23. bertrend/demos/topic_analysis/app_utils.py +69 -0
  24. bertrend/demos/topic_analysis/demo_pages/__init__.py +4 -0
  25. bertrend/demos/topic_analysis/demo_pages/explore_topics.py +455 -0
  26. bertrend/demos/topic_analysis/demo_pages/newsletters_generation.py +178 -0
  27. bertrend/demos/topic_analysis/demo_pages/temporal_visualizations.py +573 -0
  28. bertrend/demos/topic_analysis/demo_pages/topic_visualizations.py +280 -0
  29. bertrend/demos/topic_analysis/demo_pages/training_page.py +219 -0
  30. bertrend/demos/topic_analysis/messages.py +13 -0
  31. bertrend/demos/weak_signals/__init__.py +4 -0
  32. bertrend/demos/weak_signals/app.py +510 -0
  33. bertrend/demos/weak_signals/messages.py +32 -0
  34. bertrend/demos/weak_signals/visualizations_utils.py +403 -0
  35. bertrend/llm_utils/__init__.py +4 -0
  36. bertrend/llm_utils/newsletter.css +131 -0
  37. bertrend/llm_utils/newsletter_features.py +272 -0
  38. bertrend/llm_utils/openai_client.py +235 -0
  39. bertrend/llm_utils/prompts.py +94 -0
  40. bertrend/metrics/__init__.py +4 -0
  41. bertrend/metrics/temporal_metrics_embedding.py +861 -0
  42. bertrend/metrics/topic_metrics.py +199 -0
  43. bertrend/resources/common_ngrams.json +23 -0
  44. bertrend/resources/stopwords-en.json +3 -0
  45. bertrend/resources/stopwords-fr.json +1 -0
  46. bertrend/resources/stopwords-rte.json +15 -0
  47. bertrend/services/__init__.py +4 -0
  48. bertrend/services/authentication.py +66 -0
  49. bertrend/services/embedding_client.py +154 -0
  50. bertrend/services/embedding_service.py +380 -0
  51. bertrend/services/summarizer.py +45 -0
  52. bertrend/services/summary/__init__.py +4 -0
  53. bertrend/services/summary/abstractive_summarizer.py +74 -0
  54. bertrend/services/summary/chatgpt_summarizer.py +67 -0
  55. bertrend/services/summary/extractive_summarizer.py +368 -0
  56. bertrend/services/summary/lexrank.py +126 -0
  57. bertrend/services/summary/prompts.py +43 -0
  58. bertrend/tests/__init__.py +4 -0
  59. bertrend/tests/test_abstractive_summarizer.py +140 -0
  60. bertrend/tests/test_bertrend.py +1091 -0
  61. bertrend/tests/test_cache_utils.py +111 -0
  62. bertrend/tests/test_chatgpt_summarizer.py +150 -0
  63. bertrend/tests/test_config_utils.py +54 -0
  64. bertrend/tests/test_data_loading.py +326 -0
  65. bertrend/tests/test_data_structure.py +53 -0
  66. bertrend/tests/test_embedding_client.py +546 -0
  67. bertrend/tests/test_extractive_summarizer.py +250 -0
  68. bertrend/tests/test_installation.py +183 -0
  69. bertrend/tests/test_newsletter_features.py +354 -0
  70. bertrend/tests/test_openai_client.py +210 -0
  71. bertrend/tests/test_representative_docs.py +125 -0
  72. bertrend/tests/test_summarizer.py +115 -0
  73. bertrend/tests/test_temporal_metrics_embedding.py +349 -0
  74. bertrend/tests/test_topic_description.py +159 -0
  75. bertrend/tests/test_topic_metrics.py +233 -0
  76. bertrend/tests/test_topic_model.py +285 -0
  77. bertrend/tests/test_trend_analysis_data_structure.py +174 -0
  78. bertrend/tests/test_trend_analysis_prompts.py +225 -0
  79. bertrend/tests/test_trend_analysis_visualizations.py +307 -0
  80. bertrend/tests/test_trend_analysis_weak_signals.py +598 -0
  81. bertrend/tests/test_utils_init.py +70 -0
  82. bertrend/tests/test_visualizations.py +306 -0
  83. bertrend/topic_analysis/__init__.py +4 -0
  84. bertrend/topic_analysis/data_structure.py +13 -0
  85. bertrend/topic_analysis/prompts.py +42 -0
  86. bertrend/topic_analysis/representative_docs.py +57 -0
  87. bertrend/topic_analysis/topic_description.py +66 -0
  88. bertrend/topic_analysis/visualizations.py +315 -0
  89. bertrend/trend_analysis/__init__.py +4 -0
  90. bertrend/trend_analysis/data_structure.py +78 -0
  91. bertrend/trend_analysis/prompts.py +229 -0
  92. bertrend/trend_analysis/signal_llm_template_en.html +181 -0
  93. bertrend/trend_analysis/signal_llm_template_fr.html +181 -0
  94. bertrend/trend_analysis/visualizations.py +605 -0
  95. bertrend/trend_analysis/weak_signals.py +425 -0
  96. bertrend/utils/__init__.py +31 -0
  97. bertrend/utils/cache_utils.py +31 -0
  98. bertrend/utils/config_utils.py +41 -0
  99. bertrend/utils/data_loading.py +329 -0
  100. bertrend-0.3.1.dist-info/AUTHORS.txt +15 -0
  101. bertrend-0.3.1.dist-info/LICENSE.md +373 -0
  102. bertrend-0.3.1.dist-info/METADATA +400 -0
  103. bertrend-0.3.1.dist-info/RECORD +181 -0
  104. bertrend-0.3.1.dist-info/WHEEL +4 -0
  105. bertrend_apps/__init__.py +4 -0
  106. bertrend_apps/common/__init__.py +4 -0
  107. bertrend_apps/common/crontab_utils.py +113 -0
  108. bertrend_apps/common/mail_utils.py +91 -0
  109. bertrend_apps/config/feeds/arxiv_feed.toml +15 -0
  110. bertrend_apps/config/feeds/decarbonation_feed.toml +15 -0
  111. bertrend_apps/config/feeds/llm_en_feed.toml +15 -0
  112. bertrend_apps/config/feeds/llm_feed.toml +16 -0
  113. bertrend_apps/config/feeds/nlp_feed.toml +15 -0
  114. bertrend_apps/config/feeds/rte_feed.toml +15 -0
  115. bertrend_apps/config/gmail_credentials.json +1 -0
  116. bertrend_apps/config/newsletters/arxiv_newsletter_from_last.toml +39 -0
  117. bertrend_apps/config/newsletters/arxiv_newsletter_from_scratch.toml +39 -0
  118. bertrend_apps/config/newsletters/decarbonation_newsletter_from_last.toml +38 -0
  119. bertrend_apps/config/newsletters/decarbonation_newsletter_from_scratch.toml +38 -0
  120. bertrend_apps/config/newsletters/llm_en_newsletter_from_last.toml +38 -0
  121. bertrend_apps/config/newsletters/llm_newsletter_from_last.toml +39 -0
  122. bertrend_apps/config/newsletters/nlp_newsletter_from_last.toml +38 -0
  123. bertrend_apps/config/newsletters/rte_newsletter_from_last.toml +38 -0
  124. bertrend_apps/config/newsletters/rte_newsletter_from_scratch.toml +38 -0
  125. bertrend_apps/data_provider/__init__.py +9 -0
  126. bertrend_apps/data_provider/__main__.py +237 -0
  127. bertrend_apps/data_provider/arxiv_provider.py +161 -0
  128. bertrend_apps/data_provider/bing_news_provider.py +82 -0
  129. bertrend_apps/data_provider/curebot_provider.py +105 -0
  130. bertrend_apps/data_provider/data_provider.py +194 -0
  131. bertrend_apps/data_provider/google_news_provider.py +94 -0
  132. bertrend_apps/data_provider/newscatcher_provider.py +74 -0
  133. bertrend_apps/data_provider/utils.py +165 -0
  134. bertrend_apps/exploration/__init__.py +4 -0
  135. bertrend_apps/exploration/curebot/__init__.py +4 -0
  136. bertrend_apps/exploration/curebot/app.py +45 -0
  137. bertrend_apps/exploration/curebot/app_utils.py +317 -0
  138. bertrend_apps/exploration/curebot/config.toml +33 -0
  139. bertrend_apps/exploration/curebot/import_extraction_article.ipynb +108 -0
  140. bertrend_apps/exploration/curebot/old_veille_analyse.py +298 -0
  141. bertrend_apps/exploration/curebot/prompts.py +24 -0
  142. bertrend_apps/exploration/curebot/start_newsletter_generator.sh +8 -0
  143. bertrend_apps/exploration/curebot/tabs/tab1.py +150 -0
  144. bertrend_apps/exploration/curebot/tabs/tab2.py +95 -0
  145. bertrend_apps/exploration/curebot/tabs/tab3.py +125 -0
  146. bertrend_apps/exploration/dataviz/Main_page.py +10 -0
  147. bertrend_apps/exploration/dataviz/__init__.py +4 -0
  148. bertrend_apps/exploration/dataviz/pages/1_Data_feeds.py +105 -0
  149. bertrend_apps/exploration/geolocalization/__init__.py +4 -0
  150. bertrend_apps/exploration/geolocalization/entity_utils.py +118 -0
  151. bertrend_apps/exploration/geolocalization/spacy/__init__.py +4 -0
  152. bertrend_apps/exploration/geolocalization/spacy/common_factory.py +142 -0
  153. bertrend_apps/exploration/geolocalization/spacy/dbpedia.py +206 -0
  154. bertrend_apps/exploration/geolocalization/spacy/nlp.config +37 -0
  155. bertrend_apps/exploration/geolocalization/spacy/utils.py +39 -0
  156. bertrend_apps/install_feeds_and_newsletters.py +29 -0
  157. bertrend_apps/newsletters/__init__.py +4 -0
  158. bertrend_apps/newsletters/__main__.py +277 -0
  159. bertrend_apps/prospective_demo/.streamlit/config.toml +3 -0
  160. bertrend_apps/prospective_demo/.streamlit/secrets.toml +9 -0
  161. bertrend_apps/prospective_demo/__init__.py +70 -0
  162. bertrend_apps/prospective_demo/app.py +111 -0
  163. bertrend_apps/prospective_demo/authentication.py +44 -0
  164. bertrend_apps/prospective_demo/dashboard_analysis.py +132 -0
  165. bertrend_apps/prospective_demo/dashboard_common.py +79 -0
  166. bertrend_apps/prospective_demo/dashboard_signals.py +226 -0
  167. bertrend_apps/prospective_demo/feeds_common.py +38 -0
  168. bertrend_apps/prospective_demo/feeds_config.py +261 -0
  169. bertrend_apps/prospective_demo/feeds_data.py +95 -0
  170. bertrend_apps/prospective_demo/i18n.py +136 -0
  171. bertrend_apps/prospective_demo/llm_utils.py +34 -0
  172. bertrend_apps/prospective_demo/models_info.py +361 -0
  173. bertrend_apps/prospective_demo/perf_utils.py +23 -0
  174. bertrend_apps/prospective_demo/process_new_data.py +348 -0
  175. bertrend_apps/prospective_demo/report_generation.py +214 -0
  176. bertrend_apps/prospective_demo/report_template.html +148 -0
  177. bertrend_apps/prospective_demo/report_utils.py +132 -0
  178. bertrend_apps/prospective_demo/start_demo.sh +9 -0
  179. bertrend_apps/prospective_demo/streamlit_utils.py +37 -0
  180. bertrend_apps/start_apps.sh +15 -0
  181. bertrend_apps/stop_apps.sh +11 -0
@@ -0,0 +1,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,4 @@
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.
@@ -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,4 @@
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.
@@ -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,4 @@
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.
@@ -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