pyannotators-entityfishing 1.6.154__tar.gz → 1.6.158__tar.gz

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 (51) hide show
  1. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/PKG-INFO +1 -1
  2. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/src/pyannotators_entityfishing/__init__.py +1 -1
  3. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/src/pyannotators_entityfishing/candidates.py +36 -7
  4. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/src/pyannotators_entityfishing/ef_client.py +29 -6
  5. pyannotators_entityfishing-1.6.158/tools/bench_plan.py +59 -0
  6. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/tools/sherpa_plan_project.py +2 -2
  7. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/.claude/skills/ef-mapping/SKILL.md +0 -0
  8. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/.gitignore +0 -0
  9. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/.gitmodules +0 -0
  10. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/.python-version +0 -0
  11. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/LICENSE +0 -0
  12. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/README.md +0 -0
  13. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/Taskfile.yml +0 -0
  14. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/pyproject.toml +0 -0
  15. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/src/pyannotators_entityfishing/entityfishing.py +0 -0
  16. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/submodules/python-archetype/.claude-plugin/marketplace.json +0 -0
  17. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/submodules/python-archetype/.claude-plugin/plugin.json +0 -0
  18. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/submodules/python-archetype/HOWTO.md +0 -0
  19. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/submodules/python-archetype/README.md +0 -0
  20. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/submodules/python-archetype/resources/Stages.yml +0 -0
  21. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/submodules/python-archetype/resources/Taskfile.yml +0 -0
  22. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/submodules/python-archetype/resources/help/help_examples.py +0 -0
  23. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/submodules/python-archetype/resources/help/plugin-help.py +0 -0
  24. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/submodules/python-archetype/resources/help/template.en.md +0 -0
  25. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/submodules/python-archetype/resources/help/template.fr.md +0 -0
  26. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/submodules/python-archetype/skills/new-plugin/SKILL.md +0 -0
  27. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/submodules/python-archetype/skills/new-plugin/references/annotator.md +0 -0
  28. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/submodules/python-archetype/skills/new-plugin/references/converter.md +0 -0
  29. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/submodules/python-archetype/skills/new-plugin/references/formatter.md +0 -0
  30. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/submodules/python-archetype/skills/new-plugin/references/importer.md +0 -0
  31. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/submodules/python-archetype/skills/new-plugin/references/processor.md +0 -0
  32. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/submodules/python-archetype/skills/new-plugin/references/segmenter.md +0 -0
  33. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/submodules/python-archetype/skills/new-plugin/references/types.md +0 -0
  34. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/submodules/python-archetype/skills/plugin-help/SKILL.md +0 -0
  35. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/submodules/python-archetype/tools/check-marketplace.py +0 -0
  36. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/submodules/python-archetype/vars/pythonPipeline.groovy +0 -0
  37. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/tools/build_poi.py +0 -0
  38. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/tools/build_poi_lexicons.py +0 -0
  39. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/tools/build_rerank_data.py +0 -0
  40. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/tools/check_rerank_endpoint.py +0 -0
  41. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/tools/el_jev.py +0 -0
  42. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/tools/eval_linking.py +0 -0
  43. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/tools/eval_mappings.py +0 -0
  44. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/tools/eval_rerank.py +0 -0
  45. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/tools/finetune_rerank.py +0 -0
  46. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/tools/merge_rerank.py +0 -0
  47. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/tools/poi_common_words.json +0 -0
  48. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/tools/recalibrate_linking.py +0 -0
  49. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/tools/review_linking.html +0 -0
  50. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/tools/review_linking.py +0 -0
  51. {pyannotators_entityfishing-1.6.154 → pyannotators_entityfishing-1.6.158}/tools/wdfilter.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pyannotators-entityfishing
3
- Version: 1.6.154
3
+ Version: 1.6.158
4
4
  Summary: Annotator based on entity-fishing
5
5
  Project-URL: Homepage, https://github.com/oterrier/pyannotators_entityfishing/
6
6
  Author-email: Olivier Terrier <olivier.terrier@kairntech.com>
@@ -1,3 +1,3 @@
1
1
  """Annotator based on entity-fishing"""
2
2
 
3
- __version__ = "1.6.154"
3
+ __version__ = "1.6.158"
@@ -23,6 +23,8 @@ import threading
23
23
  import time
24
24
  import unicodedata
25
25
  from collections import OrderedDict, defaultdict
26
+ from concurrent.futures import ThreadPoolExecutor
27
+ from functools import lru_cache
26
28
  from typing import cast
27
29
 
28
30
  import mongoquery
@@ -92,6 +94,14 @@ FILTERED = LRUCache(int(os.getenv("APP_EF_CANDIDATE_CACHE_SIZE", "50000")), CACH
92
94
  def clear_caches() -> None:
93
95
  CONCEPTS.clear()
94
96
  FILTERED.clear()
97
+ get_client.cache_clear()
98
+
99
+
100
+ @lru_cache(maxsize=8)
101
+ def get_client(ef_uri: str, pool_size: int) -> EntityFishingClient:
102
+ """One client per service, kept from one batch to the next: its connections stay open, and its
103
+ pool_size threads cap the calls to entity-fishing of every document of the process together."""
104
+ return EntityFishingClient(base_url=ef_uri, pool_size=pool_size)
95
105
 
96
106
 
97
107
  class EntityFishingCandidatesParameters(ProcessorParameters):
@@ -162,9 +172,15 @@ class EntityFishingCandidatesParameters(ProcessorParameters):
162
172
  )
163
173
  ef_pool_size: int = Field(
164
174
  int(os.getenv("APP_EF_CLIENT_POOLSIZE", "8")),
165
- description="Number of parallel HTTP connections to entity-fishing",
175
+ description="Number of calls to entity-fishing at the same time, all the documents processed together",
166
176
  json_schema_extra={"extra": "internal"},
167
177
  )
178
+ concurrency: int = Field(
179
+ 1,
180
+ description="""How many documents are processed at the same time. Their calls to entity-fishing
181
+ still go at most ef_pool_size at a time.""",
182
+ json_schema_extra={"extra": "advanced"},
183
+ )
168
184
 
169
185
 
170
186
  class EntityFishingCandidatesProcessor(ProcessorBase):
@@ -185,7 +201,7 @@ class EntityFishingCandidatesProcessor(ProcessorBase):
185
201
  raise AttributeError(f"Metadata language '{lang}' is required and must be in {SUPPORTED_LANGUAGES}")
186
202
  langs.append(lang)
187
203
  mapping = {k: json.loads(v) for k, v in params.mapped_labels.items()} if params.mapped_labels else {}
188
- client = EntityFishingClient(base_url=params.ef_uri, pool_size=params.ef_pool_size)
204
+ client = get_client(params.ef_uri, params.ef_pool_size)
189
205
  add_candidates(
190
206
  documents,
191
207
  langs,
@@ -200,6 +216,7 @@ class EntityFishingCandidatesProcessor(ProcessorBase):
200
216
  comma_separated_to_list(params.wikidata_facts),
201
217
  params.facts_values,
202
218
  params.facts_max_values,
219
+ params.concurrency,
203
220
  )
204
221
  return documents
205
222
 
@@ -222,18 +239,21 @@ def add_candidates(
222
239
  wiki_facts: list[str] | None = None,
223
240
  facts_values: PropertyValues = PropertyValues.label,
224
241
  facts_max_values: int = 3,
242
+ concurrency: int = 1,
225
243
  ) -> None:
226
244
  """Write the Wikidata candidates of every mention in `annotation.terms`, in place."""
227
245
  wiki_facts = wiki_facts or []
228
246
  queries = {label: mongoquery.Query(condition) for label, condition in mapping.items()}
229
247
  hashes = {label: hashlib.sha1(json.dumps(condition, sort_keys=True).encode()).hexdigest() for label, condition in mapping.items()}
230
248
  ef = client.base_url
231
- failures: list[tuple[str, str, str]] = getattr(client, "failures", [])
232
- for document, lang in zip(documents, langs, strict=True):
233
- failed_before = len(failures)
249
+
250
+ def add_document(document: Document, lang: str) -> None:
251
+ # The calls this thread lost, so those of this document only.
252
+ failures: list[tuple[str, str, str]] = getattr(client, "failures", [])
253
+ failures.clear()
234
254
  groups = group_mentions(document, labels, mapping)
235
255
  if not groups:
236
- continue
256
+ return
237
257
  # One term lookup per group, on its longest form; the surface when that finds nothing.
238
258
  forms = {key: lookup_forms(members) for key, members in groups.items()}
239
259
 
@@ -312,7 +332,15 @@ def add_candidates(
312
332
  # Replace the Wikidata candidates, keep the terms of any other lexicon (AFP...).
313
333
  kept = [t for t in (a.terms or []) if t.lexicon != WIKIDATA]
314
334
  a.terms = kept + [t.model_copy(deep=True) for t in mine]
315
- report_failures(document, failures[failed_before:])
335
+ report_failures(document, failures)
336
+
337
+ workers = min(max(concurrency, 1), len(documents))
338
+ if workers > 1:
339
+ with ThreadPoolExecutor(max_workers=workers) as pool:
340
+ list(pool.map(add_document, documents, langs))
341
+ else:
342
+ for document, lang in zip(documents, langs, strict=True):
343
+ add_document(document, lang)
316
344
 
317
345
 
318
346
  def report_failures(document: Document, lost: list[tuple[str, str, str]]) -> None:
@@ -320,6 +348,7 @@ def report_failures(document: Document, lost: list[tuple[str, str, str]]) -> Non
320
348
  they concern may have lost candidates, and the document is worth processing again. A document
321
349
  processed again without failure drops the report of the earlier run."""
322
350
  metadata = {k: v for k, v in (document.metadata or {}).items() if k != ERROR_METADATA}
351
+ lost = list(dict.fromkeys(lost)) # a page fetched again by a later step fails again
323
352
  if lost:
324
353
  calls = ", ".join(f"{kind} {key}" for kind, _, key in lost)
325
354
  metadata[ERROR_METADATA] = f"{len(lost)} entity-fishing calls failed, candidates may be missing: {calls}"[:MAX_ERROR_CHARS]
@@ -1,6 +1,9 @@
1
1
  import hashlib
2
+ import json
2
3
  import logging
3
4
  import os
5
+ import re
6
+ import threading
4
7
  import time
5
8
  from collections.abc import Iterable
6
9
  from concurrent.futures import as_completed
@@ -25,6 +28,18 @@ VERIFY_SSL = os.getenv("APP_EF_VERIFY_SSL", "true").lower() not in ("0", "false"
25
28
  RETRY = Retry(total=4, backoff_factor=1, status_forcelist=(429, 500, 502, 503, 504), allowed_methods=None, raise_on_status=False)
26
29
  # (connect, read) seconds for a term lookup or a concept: without one, a stuck entity-fishing holds the document forever
27
30
  KB_TIMEOUT = (10, 60)
31
+ # EF does not escape the quotes of a property name ("Apple Maps "auid" identifier", P10046), which breaks the JSON of
32
+ # every concept holding that property: the White House, Georgia... Workaround until kermitt2/entity-fishing#177 is fixed
33
+ PROPERTY_NAME = re.compile(r'("propertyName" : ")(.*?)(", "valueType")')
34
+
35
+
36
+ def concept_json(resp: requests.Response) -> dict:
37
+ """The concept of a kb response, its unescaped property names escaped if it does not parse as is."""
38
+ try:
39
+ return resp.json()
40
+ except ValueError:
41
+ # ponytail: repairs propertyName only; drop concept_json once kermitt2/entity-fishing#177 is fixed
42
+ return json.loads(PROPERTY_NAME.sub(lambda m: m[1] + re.sub(r'(?<!\\)"', r"\"", m[2]) + m[3], resp.text))
28
43
 
29
44
 
30
45
  class EntityFishingClient:
@@ -79,14 +94,22 @@ class EntityFishingClient:
79
94
  self.ksession.verify = VERIFY_SSL
80
95
  self.fksession = FuturesSession(session=self.ksession, max_workers=pool_size)
81
96
 
82
- # the calls that failed even after the retries, ("term" or "concept", lang, term or page id): the
83
- # candidates step reports them on the document, and does not cache what they left out
84
- self.failures: list[tuple[str, str, str]] = []
97
+ self._local = threading.local()
85
98
 
86
99
  self.disamb_url = "/service/disambiguate/"
87
100
  self.kb_url = "/service/kb/concept/"
88
101
  self.term_url = "/service/kb/term/"
89
102
 
103
+ @property
104
+ def failures(self) -> list[tuple[str, str, str]]:
105
+ """The calls that failed even after the retries, ("term" or "concept", lang, term or page id): the
106
+ candidates step reports them on the document, and does not cache what they left out. One list per
107
+ thread, as the failures are recorded in the calling thread: documents processed at the same time
108
+ through one client each see only their own."""
109
+ if not hasattr(self._local, "failures"):
110
+ self._local.failures = []
111
+ return self._local.failures
112
+
90
113
  def _build_disamb_query(self, text: str, sentences: list[dict], lang: str, minSelectorScore: float, maxTermFrequency: float | None, short_text: bool = False) -> dict:
91
114
  text = text.replace("\r\n", " \n")
92
115
  disamb_query = {
@@ -228,7 +251,7 @@ class EntityFishingClient:
228
251
  try:
229
252
  resp = self.ksession.get(self.base_url + self.kb_url + qid)
230
253
  if resp.ok:
231
- return resp.json()
254
+ return concept_json(resp)
232
255
  else:
233
256
  resp.raise_for_status()
234
257
  except Exception:
@@ -244,7 +267,7 @@ class EntityFishingClient:
244
267
  try:
245
268
  resp = future.result()
246
269
  if resp.ok:
247
- concept = resp.json()
270
+ concept = concept_json(resp)
248
271
  if "wikidataId" in concept:
249
272
  concepts[concept["wikidataId"]] = concept
250
273
  else:
@@ -288,7 +311,7 @@ class EntityFishingClient:
288
311
  try:
289
312
  resp = future.result()
290
313
  if resp.ok:
291
- concepts[futures[future]] = resp.json()
314
+ concepts[futures[future]] = concept_json(resp)
292
315
  elif resp.status_code != 404: # 404: a page the KB does not hold, not a failure
293
316
  resp.raise_for_status()
294
317
  except Exception:
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env python3
2
+ """Throughput of one or more Sherpa plans on the same documents of a project.
3
+
4
+ Each plan runs twice on the same documents: the first run fills the caches (entity-fishing
5
+ concepts and candidates, vLLM prefixes), the second is the one measured. The documents are
6
+ sent without their annotations: the plan runs the NER itself.
7
+
8
+ SHERPA_TOKEN=... python3 tools/bench_plan.py --sherpa https://viper.kairntech.com --project afp_ner_xx \\
9
+ --plans pipeline_ner_old pipeline_ner_new --limit 200 --batch 10 --jobs 1
10
+
11
+ --batch is what each processor receives at once: a processor's `concurrency` only helps above 1.
12
+ """
13
+
14
+ import argparse
15
+ import os
16
+ import sys
17
+ import time
18
+ from concurrent.futures import ThreadPoolExecutor
19
+
20
+ from sherpa_plan_project import annotate, documents, login
21
+
22
+
23
+ def main() -> int:
24
+ parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
25
+ parser.add_argument("--sherpa", default="http://localhost:7070")
26
+ parser.add_argument("--project", required=True)
27
+ parser.add_argument("--plans", nargs="+", required=True)
28
+ parser.add_argument("--limit", type=int, default=200, help="first N documents")
29
+ parser.add_argument("--batch", type=int, default=10, help="documents per annotate call")
30
+ parser.add_argument("--jobs", type=int, default=1, help="annotate calls at the same time")
31
+ parser.add_argument("--parallelize", action="store_true", help="Sherpa sends each document of a batch to each step in its own call, all at once")
32
+ args = parser.parse_args()
33
+ token, email, password = os.getenv("SHERPA_TOKEN"), os.getenv("SHERPA_EMAIL"), os.getenv("SHERPA_PASSWORD")
34
+ if not token and not (email and password):
35
+ print("set SHERPA_TOKEN, or SHERPA_EMAIL and SHERPA_PASSWORD", file=sys.stderr)
36
+ return 1
37
+ auth = {"X-Kairntech-key": token} if token else {"Authorization": f"Bearer {login(args.sherpa, email, password)}"}
38
+ headers = {**auth, "Accept": "application/json"}
39
+
40
+ docs = documents(args.sherpa, args.project, headers)
41
+ print(f"{len(docs)} documents in {args.project}", flush=True)
42
+ docs = [{k: d[k] for k in ("identifier", "title", "text", "metadata") if d.get(k) is not None} for d in docs if d.get("text")][: args.limit]
43
+ if not docs:
44
+ print("no document with a text", file=sys.stderr)
45
+ return 1
46
+ batches = [docs[i : i + args.batch] for i in range(0, len(docs), args.batch)]
47
+
48
+ for plan in args.plans:
49
+ for run in ("warm-up", "measured"):
50
+ start = time.time()
51
+ with ThreadPoolExecutor(args.jobs) as pool:
52
+ list(pool.map(lambda batch, plan=plan: annotate(args.sherpa, args.project, plan, headers, batch, args.parallelize), batches))
53
+ elapsed = time.time() - start
54
+ print(f"{plan} [{run}] {len(docs)} documents in {elapsed:.1f} s -> {len(docs) / elapsed:.1f} documents/s", flush=True)
55
+ return 0
56
+
57
+
58
+ if __name__ == "__main__":
59
+ sys.exit(main())
@@ -47,10 +47,10 @@ def to_input(doc: dict) -> dict:
47
47
  return out
48
48
 
49
49
 
50
- def annotate(sherpa: str, project: str, plan: str, headers: dict, batch: list[dict]) -> list[dict]:
50
+ def annotate(sherpa: str, project: str, plan: str, headers: dict, batch: list[dict], parallelize: bool = False) -> list[dict]:
51
51
  r = requests.post(
52
52
  f"{sherpa}/api/annotate/projects/{project}/annotators/{plan}/_annotate_documents",
53
- params={"inlineLabels": True, "inlineText": True},
53
+ params={"inlineLabels": True, "inlineText": True, "parallelize": str(parallelize).lower()},
54
54
  json=batch,
55
55
  headers=headers,
56
56
  timeout=1800,