pyannotators-entityfishing 1.6.64__tar.gz → 1.6.71__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 (24) hide show
  1. {pyannotators_entityfishing-1.6.64 → pyannotators_entityfishing-1.6.71}/PKG-INFO +17 -7
  2. {pyannotators_entityfishing-1.6.64 → pyannotators_entityfishing-1.6.71}/README.md +16 -6
  3. {pyannotators_entityfishing-1.6.64 → pyannotators_entityfishing-1.6.71}/src/pyannotators_entityfishing/__init__.py +1 -1
  4. {pyannotators_entityfishing-1.6.64 → pyannotators_entityfishing-1.6.71}/submodules/python-archetype/vars/pythonPipeline.groovy +35 -25
  5. pyannotators_entityfishing-1.6.71/tools/el_jev.py +118 -0
  6. {pyannotators_entityfishing-1.6.64 → pyannotators_entityfishing-1.6.71}/.claude/skills/ef-mapping/SKILL.md +0 -0
  7. {pyannotators_entityfishing-1.6.64 → pyannotators_entityfishing-1.6.71}/.gitignore +0 -0
  8. {pyannotators_entityfishing-1.6.64 → pyannotators_entityfishing-1.6.71}/.gitmodules +0 -0
  9. {pyannotators_entityfishing-1.6.64 → pyannotators_entityfishing-1.6.71}/.python-version +0 -0
  10. {pyannotators_entityfishing-1.6.64 → pyannotators_entityfishing-1.6.71}/Taskfile.yml +0 -0
  11. {pyannotators_entityfishing-1.6.64 → pyannotators_entityfishing-1.6.71}/pyproject.toml +0 -0
  12. {pyannotators_entityfishing-1.6.64 → pyannotators_entityfishing-1.6.71}/src/pyannotators_entityfishing/ef_client.py +0 -0
  13. {pyannotators_entityfishing-1.6.64 → pyannotators_entityfishing-1.6.71}/src/pyannotators_entityfishing/entityfishing.py +0 -0
  14. {pyannotators_entityfishing-1.6.64 → pyannotators_entityfishing-1.6.71}/submodules/python-archetype/.claude-plugin/marketplace.json +0 -0
  15. {pyannotators_entityfishing-1.6.64 → pyannotators_entityfishing-1.6.71}/submodules/python-archetype/.claude-plugin/plugin.json +0 -0
  16. {pyannotators_entityfishing-1.6.64 → pyannotators_entityfishing-1.6.71}/submodules/python-archetype/HOWTO.md +0 -0
  17. {pyannotators_entityfishing-1.6.64 → pyannotators_entityfishing-1.6.71}/submodules/python-archetype/README.md +0 -0
  18. {pyannotators_entityfishing-1.6.64 → pyannotators_entityfishing-1.6.71}/submodules/python-archetype/resources/Stages.yml +0 -0
  19. {pyannotators_entityfishing-1.6.64 → pyannotators_entityfishing-1.6.71}/submodules/python-archetype/resources/Taskfile.yml +0 -0
  20. {pyannotators_entityfishing-1.6.64 → pyannotators_entityfishing-1.6.71}/submodules/python-archetype/skills/new-plugin/SKILL.md +0 -0
  21. {pyannotators_entityfishing-1.6.64 → pyannotators_entityfishing-1.6.71}/submodules/python-archetype/skills/new-plugin/references/types.md +0 -0
  22. {pyannotators_entityfishing-1.6.64 → pyannotators_entityfishing-1.6.71}/submodules/python-archetype/tools/check-marketplace.py +0 -0
  23. {pyannotators_entityfishing-1.6.64 → pyannotators_entityfishing-1.6.71}/tools/eval_mappings.py +0 -0
  24. {pyannotators_entityfishing-1.6.64 → pyannotators_entityfishing-1.6.71}/tools/wdfilter.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pyannotators-entityfishing
3
- Version: 1.6.64
3
+ Version: 1.6.71
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>
@@ -89,24 +89,34 @@ for ann in docs[0].annotations:
89
89
  The build is driven by [Task](https://taskfile.dev) and [uv](https://docs.astral.sh/uv/),
90
90
  with the shared stages coming from the `python-archetype` submodule.
91
91
 
92
- ### Prerequisites
92
+ ### Getting started
93
93
 
94
- **Task is the only manual prerequisite.** An archetype cannot bootstrap itself: uv and the
95
- Python interpreter install themselves on demand (every task that runs uv depends on an
96
- internal `install-python` task), but the thing that runs them does not.
94
+ The stages live in a Git submodule, so **clone with `--recurse-submodules`**:
97
95
 
98
96
  ```bash
97
+ git clone --recurse-submodules git@bitbucket.org:kairntech/pyannotators_entityfishing.git
98
+ cd pyannotators_entityfishing
99
99
  sh -c "$(curl -sSL https://taskfile.dev/install.sh)" -- -d -b ~/.local/bin
100
+ task
100
101
  ```
101
102
 
102
- Make sure `~/.local/bin` is on your `PATH` — that is where `task` and `uv` both land.
103
+ Already cloned without it? The submodule directory is empty, and `task` fails on:
104
+
105
+ ```
106
+ task: No Taskfile found at ".../submodules/python-archetype/resources/Taskfile.yml"
107
+ ```
103
108
 
104
- Then fetch the shared stages:
109
+ which means exactly that, and nothing worse:
105
110
 
106
111
  ```bash
107
112
  git submodule update --init
108
113
  ```
109
114
 
115
+ **Task is the only manual prerequisite.** An archetype cannot bootstrap itself: uv and the
116
+ Python interpreter install themselves on demand (every task that runs uv depends on an
117
+ internal `install-python` task), but the thing that runs them does not. Make sure
118
+ `~/.local/bin` is on your `PATH` — that is where `task` and `uv` both land.
119
+
110
120
  ### Running the pipeline
111
121
 
112
122
  ```bash
@@ -41,24 +41,34 @@ for ann in docs[0].annotations:
41
41
  The build is driven by [Task](https://taskfile.dev) and [uv](https://docs.astral.sh/uv/),
42
42
  with the shared stages coming from the `python-archetype` submodule.
43
43
 
44
- ### Prerequisites
44
+ ### Getting started
45
45
 
46
- **Task is the only manual prerequisite.** An archetype cannot bootstrap itself: uv and the
47
- Python interpreter install themselves on demand (every task that runs uv depends on an
48
- internal `install-python` task), but the thing that runs them does not.
46
+ The stages live in a Git submodule, so **clone with `--recurse-submodules`**:
49
47
 
50
48
  ```bash
49
+ git clone --recurse-submodules git@bitbucket.org:kairntech/pyannotators_entityfishing.git
50
+ cd pyannotators_entityfishing
51
51
  sh -c "$(curl -sSL https://taskfile.dev/install.sh)" -- -d -b ~/.local/bin
52
+ task
52
53
  ```
53
54
 
54
- Make sure `~/.local/bin` is on your `PATH` — that is where `task` and `uv` both land.
55
+ Already cloned without it? The submodule directory is empty, and `task` fails on:
56
+
57
+ ```
58
+ task: No Taskfile found at ".../submodules/python-archetype/resources/Taskfile.yml"
59
+ ```
55
60
 
56
- Then fetch the shared stages:
61
+ which means exactly that, and nothing worse:
57
62
 
58
63
  ```bash
59
64
  git submodule update --init
60
65
  ```
61
66
 
67
+ **Task is the only manual prerequisite.** An archetype cannot bootstrap itself: uv and the
68
+ Python interpreter install themselves on demand (every task that runs uv depends on an
69
+ internal `install-python` task), but the thing that runs them does not. Make sure
70
+ `~/.local/bin` is on your `PATH` — that is where `task` and `uv` both land.
71
+
62
72
  ### Running the pipeline
63
73
 
64
74
  ```bash
@@ -1,3 +1,3 @@
1
1
  """Annotator based on entity-fishing"""
2
2
 
3
- __version__ = "1.6.64"
3
+ __version__ = "1.6.71"
@@ -211,6 +211,28 @@ def call(Map config = [:]) {
211
211
  }
212
212
  }
213
213
 
214
+ stage('py:sbom') {
215
+ steps {
216
+ sh 'task py:sbom'
217
+ }
218
+ }
219
+
220
+ stage('py:check-vulnerabilities') {
221
+ steps {
222
+ script {
223
+ // pip-audit ne voit que les paquets Python, jamais une couche OS : ce
224
+ // stage ne remplace pas un scan d'image, il couvre les dépendances.
225
+ if (env._BLOCK_ON_VULNS == 'true') {
226
+ sh 'task py:check-vulnerabilities'
227
+ } else {
228
+ catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
229
+ sh 'task py:check-vulnerabilities'
230
+ }
231
+ }
232
+ }
233
+ }
234
+ }
235
+
214
236
  stage('images:base') {
215
237
  when { expression { return stageDeclared('images:base') } }
216
238
  steps {
@@ -254,28 +276,6 @@ def call(Map config = [:]) {
254
276
  }
255
277
  }
256
278
 
257
- stage('py:sbom') {
258
- steps {
259
- sh 'task py:sbom'
260
- }
261
- }
262
-
263
- stage('py:check-vulnerabilities') {
264
- steps {
265
- script {
266
- // pip-audit ne voit que les paquets Python, jamais une couche OS : ce
267
- // stage ne remplace pas un scan d'image, il couvre les dépendances.
268
- if (env._BLOCK_ON_VULNS == 'true') {
269
- sh 'task py:check-vulnerabilities'
270
- } else {
271
- catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
272
- sh 'task py:check-vulnerabilities'
273
- }
274
- }
275
- }
276
- }
277
- }
278
-
279
279
  stage('version') {
280
280
  when {
281
281
  allOf {
@@ -545,7 +545,17 @@ Either add it here, or remove it from STAGES — not two lists drifting in silen
545
545
  """
546
546
  }
547
547
 
548
- /** The stages this pipeline runs, in order. Kept next to checkStageDrift so both change together. */
548
+ /**
549
+ * The stages this pipeline runs, in order. Kept next to checkStageDrift so both change together.
550
+ *
551
+ * L'ordre suit une seule regle : tout ce qui peut dire non passe avant ce qu'on ne peut pas
552
+ * defaire. 'version' pousse un commit et 'py:publish' envoie sur PyPI, qui est immuable -- ces
553
+ * deux-la sont les seuls a avoir un effet hors de l'agent, donc ils restent en queue, apres
554
+ * les portes. Les portes Python (py:sbom, py:check-vulnerabilities) et les portes d'image
555
+ * (images:scan) n'ont aucune dependance entre elles : aucun depot ne declare a la fois une roue
556
+ * et des images. Les premieres sont donc groupees avec py:test, pour que les stages py:* d'un
557
+ * depot a roue soient contigus et que les colonnes images:*, vides chez lui, ne les coupent pas.
558
+ */
549
559
  @groovy.transform.Field
550
560
  List<String> JENKINS_STAGES = [
551
561
  'init',
@@ -553,12 +563,12 @@ List<String> JENKINS_STAGES = [
553
563
  'py:lock-check',
554
564
  'py:lint',
555
565
  'py:test',
566
+ 'py:sbom',
567
+ 'py:check-vulnerabilities',
556
568
  'images:base',
557
569
  'images:flavors',
558
570
  'backport-proof',
559
571
  'images:scan',
560
- 'py:sbom',
561
- 'py:check-vulnerabilities',
562
572
  'version',
563
573
  'py:build',
564
574
  'py:publish',
@@ -0,0 +1,118 @@
1
+ """Prototype : NER amont -> candidats entity-fishing -> désambiguïsation Jev.
2
+
3
+ Clé lue dans JEV_API_KEY (comme tests/.env) ou TYPESAFE_API_KEY ; sans clé, affiche la
4
+ requête Jev au lieu de l'envoyer. Voir docs/entity-fishing-disambiguation-candidates_fr.md.
5
+
6
+ .venv/bin/python tools/el_jev.py "Macron à Washington" "Macron:PERSON,Washington:LOCATION" fr
7
+ """
8
+
9
+ import json
10
+ import os
11
+ import re
12
+ import urllib.parse
13
+ import urllib.request
14
+ from pathlib import Path
15
+
16
+ import mongoquery
17
+
18
+ EF = "https://entityfishing.kairntech.com/service"
19
+ API_KEY = os.getenv("JEV_API_KEY") or os.getenv("TYPESAFE_API_KEY")
20
+ JEV = "https://api.typesafe.ai/v1/systemone"
21
+ TOP_K = 8
22
+ NONE = "NONE"
23
+
24
+ # Filtres de type : les mappings mongoquery de l'annotateur (tests/data_v2/<Label>.json),
25
+ # appliqués au JSON de /kb/concept. Un type NER sans mapping (EVENT) n'est pas filtré.
26
+ MAPPINGS = Path(__file__).resolve().parent.parent / "tests" / "data_v2"
27
+ NER_TO_LABEL = {"PERSON": "Person", "LOCATION": "Location", "ORGANIZATION": "Organization"}
28
+ FILTERS = {ner: mongoquery.Query(json.loads((MAPPINGS / f"{label}.json").read_text())) for ner, label in NER_TO_LABEL.items()}
29
+
30
+
31
+ def get(url):
32
+ with urllib.request.urlopen(url, timeout=60) as r:
33
+ return json.load(r)
34
+
35
+
36
+ def senses(term, lang="en"):
37
+ return get(f"{EF}/kb/term/{urllib.parse.quote(term)}?lang={lang}").get("senses", [])
38
+
39
+
40
+ def concept(pageid, lang="en"):
41
+ return get(f"{EF}/kb/concept/{pageid}?lang={lang}")
42
+
43
+
44
+ def first_sentence(wikitext):
45
+ t = re.sub(r"\[\[(?:[^|\]]*\|)?([^\]]*)\]\]", r"\1", wikitext)
46
+ t = re.sub(r"'''?|\{\{[^}]*\}\}|\([^)]*\)", "", t)
47
+ return re.split(r"(?<=\.)\s", re.sub(r"\s+", " ", t).strip())[0]
48
+
49
+
50
+ def candidates(mention, ner_type, lang="en"):
51
+ out = []
52
+ for s in senses(mention, lang)[:TOP_K]:
53
+ c = concept(s["pageid"], lang)
54
+ st = c.get("statements", [])
55
+ if ner_type in FILTERS and not FILTERS[ner_type].match(c):
56
+ continue
57
+ facts = {}
58
+ for x in st:
59
+ if x["propertyId"] in ("P31", "P106", "P27", "P641", "P585") and x.get("valueName"):
60
+ facts.setdefault(x["propertyName"], []).append(x["valueName"])
61
+ if x["propertyId"] == "P569":
62
+ facts["date of birth"] = [x["value"]["time"][1:11]]
63
+ defs = c.get("definitions") or [{}]
64
+ out.append(
65
+ {
66
+ "qid": c["wikidataId"],
67
+ "label": s["preferred"],
68
+ "prior": s["prob_c"],
69
+ "description": first_sentence(defs[0].get("definition", "")),
70
+ "facts": {k: v[:3] for k, v in facts.items()},
71
+ }
72
+ )
73
+ return out
74
+
75
+
76
+ def jev_request(text, mentions):
77
+ """Une Choice par mention, toutes dans le même appel (évaluées en parallèle)."""
78
+ questions = {}
79
+ for i, m in enumerate(mentions):
80
+ criteria = {c["qid"]: {"name": c["label"], "description": c["description"], "facts": c["facts"]} for c in m["candidates"]}
81
+ criteria[NONE] = "None of the listed entities is the one meant by the mention"
82
+ questions[f"m{i}"] = {
83
+ "type": "choice",
84
+ "instructions": {
85
+ "mention": {"surface": m["surface"], "ner_type": m["type"]},
86
+ "question": "In `text`, which knowledge-base entity does `mention.surface` refer to?",
87
+ },
88
+ "criteria": criteria,
89
+ }
90
+ return {"model": "jev-latest", "state": {"text": text}, "questions": questions}
91
+
92
+
93
+ def call_jev(payload):
94
+ req = urllib.request.Request(JEV, data=json.dumps(payload).encode(), headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"})
95
+ with urllib.request.urlopen(req, timeout=60) as r:
96
+ return json.load(r)
97
+
98
+
99
+ if __name__ == "__main__":
100
+ import sys
101
+
102
+ text = sys.argv[1] if len(sys.argv) > 1 else "Ronaldo at the World Cup"
103
+ spec = sys.argv[2] if len(sys.argv) > 2 else "Ronaldo:PERSON,World Cup:EVENT"
104
+ lang = sys.argv[3] if len(sys.argv) > 3 else "en"
105
+ ner = [m.rsplit(":", 1) for m in spec.split(",")]
106
+ mentions = [{"surface": s, "type": t, "candidates": candidates(s, t, lang)} for s, t in ner]
107
+ payload = jev_request(text, mentions)
108
+ if not API_KEY:
109
+ print(json.dumps(payload, indent=1, ensure_ascii=False))
110
+ sys.exit()
111
+ answers = call_jev(payload)["answers"]
112
+ for i, m in enumerate(mentions):
113
+ a = answers[f"m{i}"]
114
+ prior = {c["qid"]: c["prior"] for c in m["candidates"]}
115
+ print(f"{m['surface']} -> {a['choice']} (conf {a['confidence']:.2f})")
116
+ for qid, p in sorted(a["probabilities"].items(), key=lambda kv: -kv[1]):
117
+ label = {c["qid"]: c["label"] for c in m["candidates"]}.get(qid, "")
118
+ print(f" {qid:10} jev={p:.3f} prior={prior.get(qid, 0):.3f} {label}")