pyprocessors-coreference 1.6.1__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.
- pyprocessors_coreference-1.6.1/.claude/settings.json +13 -0
- pyprocessors_coreference-1.6.1/.gitignore +31 -0
- pyprocessors_coreference-1.6.1/.gitmodules +4 -0
- pyprocessors_coreference-1.6.1/.python-version +1 -0
- pyprocessors_coreference-1.6.1/PKG-INFO +131 -0
- pyprocessors_coreference-1.6.1/README.md +102 -0
- pyprocessors_coreference-1.6.1/docs/adr/0001-linking-candidats-entityfishing-decision-jev.md +78 -0
- pyprocessors_coreference-1.6.1/pyproject.toml +74 -0
- pyprocessors_coreference-1.6.1/src/pyprocessors_coreference/__init__.py +3 -0
- pyprocessors_coreference-1.6.1/src/pyprocessors_coreference/coreference.py +174 -0
- pyprocessors_coreference-1.6.1/submodules/python-archetype/.claude-plugin/marketplace.json +18 -0
- pyprocessors_coreference-1.6.1/submodules/python-archetype/.claude-plugin/plugin.json +15 -0
- pyprocessors_coreference-1.6.1/submodules/python-archetype/HOWTO.md +204 -0
- pyprocessors_coreference-1.6.1/submodules/python-archetype/README.md +120 -0
- pyprocessors_coreference-1.6.1/submodules/python-archetype/resources/Stages.yml +114 -0
- pyprocessors_coreference-1.6.1/submodules/python-archetype/skills/new-plugin/SKILL.md +112 -0
- pyprocessors_coreference-1.6.1/submodules/python-archetype/skills/new-plugin/references/annotator.md +51 -0
- pyprocessors_coreference-1.6.1/submodules/python-archetype/skills/new-plugin/references/importer.md +50 -0
- pyprocessors_coreference-1.6.1/submodules/python-archetype/skills/new-plugin/references/processor.md +55 -0
- pyprocessors_coreference-1.6.1/submodules/python-archetype/skills/new-plugin/references/types.md +61 -0
- pyprocessors_coreference-1.6.1/submodules/python-archetype/tools/check-marketplace.py +237 -0
- pyprocessors_coreference-1.6.1/submodules/python-archetype/vars/pythonPipeline.groovy +638 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/.idea/
|
|
2
|
+
/.vscode/
|
|
3
|
+
/dist/
|
|
4
|
+
dist
|
|
5
|
+
.git
|
|
6
|
+
.cache
|
|
7
|
+
**/.pytest_cache
|
|
8
|
+
models
|
|
9
|
+
log
|
|
10
|
+
**/__pycache__/
|
|
11
|
+
*.pyc
|
|
12
|
+
**/test-reports
|
|
13
|
+
**/.passwd-pypi
|
|
14
|
+
**/.env
|
|
15
|
+
**/results.xml
|
|
16
|
+
|
|
17
|
+
# Specific
|
|
18
|
+
.idea/
|
|
19
|
+
.groovylintrc.json
|
|
20
|
+
.emailNotif
|
|
21
|
+
uv.lock
|
|
22
|
+
|
|
23
|
+
# SBOMs
|
|
24
|
+
**/sbom*.json
|
|
25
|
+
**/trivy*.html
|
|
26
|
+
**/audit*.json
|
|
27
|
+
# Le rapport lisible que py:vuln-report ecrit (AUDIT_REPORT). Ajoute apres les
|
|
28
|
+
# regles ci-dessus, il n'etait couvert par aucune : les huit depots ont donc un
|
|
29
|
+
# audit-report.md non suivi qui traine apres chaque 'task'. Meme correctif a y
|
|
30
|
+
# porter -- c'est du ressort de check-plugin-drift.
|
|
31
|
+
**/audit*.md
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pyprocessors-coreference
|
|
3
|
+
Version: 1.6.1
|
|
4
|
+
Summary: Processor grouping the mentions of the same entity
|
|
5
|
+
Project-URL: Homepage, https://bitbucket.org/kairntech/pyprocessors_coreference
|
|
6
|
+
Author-email: Olivier Terrier <olivier.terrier@kairntech.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Intended Audience :: Information Technology
|
|
11
|
+
Classifier: Intended Audience :: System Administrators
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Software Development
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Requires-Dist: pymultirole-plugins<1.7.0,>=1.6.0
|
|
21
|
+
Provides-Extra: sbom
|
|
22
|
+
Requires-Dist: cyclonedx-bom; extra == 'sbom'
|
|
23
|
+
Requires-Dist: pip-audit; extra == 'sbom'
|
|
24
|
+
Provides-Extra: test
|
|
25
|
+
Requires-Dist: pip; extra == 'test'
|
|
26
|
+
Requires-Dist: pytest; extra == 'test'
|
|
27
|
+
Requires-Dist: ruff; extra == 'test'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# pyprocessors-coreference
|
|
31
|
+
|
|
32
|
+
Processor grouping the mentions of the same entity.
|
|
33
|
+
|
|
34
|
+
Ce dépôt enregistre un processeur dans le groupe d'entry-points `pyprocessors.plugins` :
|
|
35
|
+
|
|
36
|
+
| Plugin | Classe |
|
|
37
|
+
|--------|--------|
|
|
38
|
+
| `coreference` | `pyprocessors_coreference.coreference:CoreferenceProcessor` |
|
|
39
|
+
|
|
40
|
+
Il regroupe les mentions d'une même entité **avant** le linking, pour qu'elles soient
|
|
41
|
+
liées une seule fois, à partir de leur forme la plus longue. C'est l'étape de
|
|
42
|
+
regroupement des mentions de l'ADR-0001 de `pyannotators_entityfishing`
|
|
43
|
+
([`docs/adr/`](docs/adr/0001-linking-candidats-entityfishing-decision-jev.md)), palier 1 :
|
|
44
|
+
les noms seulement.
|
|
45
|
+
|
|
46
|
+
## Ce qu'il fait
|
|
47
|
+
|
|
48
|
+
Pour chaque document, entre mentions de même label :
|
|
49
|
+
|
|
50
|
+
- **même surface normalisée → même groupe.** La normalisation ignore la casse, les
|
|
51
|
+
espaces, le style des apostrophes, et les civilités en tête d'un nom de personne
|
|
52
|
+
(« M. », « Mme », « Dr »…) ;
|
|
53
|
+
- **tous les labels de personne forment une seule famille** : le `person` du NER et
|
|
54
|
+
l'`afpperson` des lexiques AFP annotent le même span, et vont dans le même groupe ;
|
|
55
|
+
- **un nom partiel de personne rejoint le nom complet dont il est la fin**, que le
|
|
56
|
+
nom complet soit avant ou après lui dans le document : « Macron » et « M. Macron »
|
|
57
|
+
rejoignent « Emmanuel Macron » ;
|
|
58
|
+
- **un prénom seul ne rejoint jamais un nom complet** : « Emmanuel » reste à part ;
|
|
59
|
+
- **un nom partiel ambigu n'est pas fusionné** : avec « Emmanuel Macron » et
|
|
60
|
+
« Brigitte Macron » dans le texte, « Macron » garde son propre groupe, et liste les
|
|
61
|
+
groupes possibles pour que l'étape de linking tranche ;
|
|
62
|
+
- **la règle du nom partiel ne vaut que pour les personnes** : « Airbus » ne rejoint pas
|
|
63
|
+
« Airbus Defence and Space ».
|
|
64
|
+
|
|
65
|
+
Sur chaque annotation d'un label regroupé, il écrit dans `properties` :
|
|
66
|
+
|
|
67
|
+
| Propriété | Valeur |
|
|
68
|
+
|-----------|--------|
|
|
69
|
+
| `entity_group` | l'identifiant du groupe dans le document : `g1`, `g2`… dans l'ordre de première apparition |
|
|
70
|
+
| `canonical_form` | la forme la plus longue du groupe, sans civilité : c'est elle qu'on cherche dans la base de connaissances |
|
|
71
|
+
| `mention_type` | `name` (les périphrases et les pronoms, `nominal` et `pronoun`, viendront avec les paliers 2 et 3) |
|
|
72
|
+
| `entity_group_candidates` | seulement sur un nom partiel ambigu : les groupes auxquels il peut appartenir |
|
|
73
|
+
|
|
74
|
+
Les autres propriétés de l'annotation sont gardées, et traiter deux fois un document
|
|
75
|
+
donne le même résultat. La portée est le document entier, ce qui convient à une
|
|
76
|
+
dépêche ; les documents longs sont une question ouverte (SHERPA-3048).
|
|
77
|
+
|
|
78
|
+
## Requirements
|
|
79
|
+
|
|
80
|
+
- Python 3.12+
|
|
81
|
+
- `pymultirole_plugins` (>=1.6.0,<1.7.0)
|
|
82
|
+
|
|
83
|
+
## Installation
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
pip install pyprocessors-coreference
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Usage
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from pymultirole_plugins.v1.schema import Annotation, Document
|
|
93
|
+
|
|
94
|
+
from pyprocessors_coreference.coreference import CoreferenceParameters, CoreferenceProcessor
|
|
95
|
+
|
|
96
|
+
text = "Emmanuel Macron est arrivé. M. Macron a parlé."
|
|
97
|
+
document = Document(
|
|
98
|
+
text=text,
|
|
99
|
+
annotations=[
|
|
100
|
+
Annotation(start=0, end=15, labelName="person", text="Emmanuel Macron"),
|
|
101
|
+
Annotation(start=28, end=37, labelName="person", text="M. Macron"),
|
|
102
|
+
],
|
|
103
|
+
)
|
|
104
|
+
[document] = CoreferenceProcessor().process([document], CoreferenceParameters())
|
|
105
|
+
# Both annotations: entity_group "g1", canonical_form "Emmanuel Macron".
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Parameters
|
|
109
|
+
|
|
110
|
+
| Paramètre | Défaut | Description |
|
|
111
|
+
|-----------|--------|-------------|
|
|
112
|
+
| `labels` | vide | Labels des mentions à regrouper. Vide : tous les labels. |
|
|
113
|
+
| `person_labels` | vide | Labels de personne : ils forment une seule famille, et la règle du nom partiel s'y applique. Vide : tout label dont le nom contient « person » (`person`, `afpperson`…). |
|
|
114
|
+
| `honorifics` | `M.`, `Mme`, `Dr`, `Mr`… | Civilités ignorées en tête d'un nom de personne. Paramètre avancé. |
|
|
115
|
+
|
|
116
|
+
## Development
|
|
117
|
+
|
|
118
|
+
Le build est piloté par [Task](https://taskfile.dev) et [uv](https://docs.astral.sh/uv/),
|
|
119
|
+
les stages venant du submodule `python-archetype` :
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
git submodule update --init
|
|
123
|
+
task stages # les stages du pipeline, dans l'ordre
|
|
124
|
+
task # tout sauf le dernier stage — donc tout sauf la publication
|
|
125
|
+
task -- --skip-tests # idem, sans le stage de test
|
|
126
|
+
task up-to -- py:lint
|
|
127
|
+
task jenkins # tout, exactement ce que joue Jenkins
|
|
128
|
+
task --list
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`uv.lock` n'est pas versionné ici, donc `py:sync` résout les dépendances à neuf.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# pyprocessors-coreference
|
|
2
|
+
|
|
3
|
+
Processor grouping the mentions of the same entity.
|
|
4
|
+
|
|
5
|
+
Ce dépôt enregistre un processeur dans le groupe d'entry-points `pyprocessors.plugins` :
|
|
6
|
+
|
|
7
|
+
| Plugin | Classe |
|
|
8
|
+
|--------|--------|
|
|
9
|
+
| `coreference` | `pyprocessors_coreference.coreference:CoreferenceProcessor` |
|
|
10
|
+
|
|
11
|
+
Il regroupe les mentions d'une même entité **avant** le linking, pour qu'elles soient
|
|
12
|
+
liées une seule fois, à partir de leur forme la plus longue. C'est l'étape de
|
|
13
|
+
regroupement des mentions de l'ADR-0001 de `pyannotators_entityfishing`
|
|
14
|
+
([`docs/adr/`](docs/adr/0001-linking-candidats-entityfishing-decision-jev.md)), palier 1 :
|
|
15
|
+
les noms seulement.
|
|
16
|
+
|
|
17
|
+
## Ce qu'il fait
|
|
18
|
+
|
|
19
|
+
Pour chaque document, entre mentions de même label :
|
|
20
|
+
|
|
21
|
+
- **même surface normalisée → même groupe.** La normalisation ignore la casse, les
|
|
22
|
+
espaces, le style des apostrophes, et les civilités en tête d'un nom de personne
|
|
23
|
+
(« M. », « Mme », « Dr »…) ;
|
|
24
|
+
- **tous les labels de personne forment une seule famille** : le `person` du NER et
|
|
25
|
+
l'`afpperson` des lexiques AFP annotent le même span, et vont dans le même groupe ;
|
|
26
|
+
- **un nom partiel de personne rejoint le nom complet dont il est la fin**, que le
|
|
27
|
+
nom complet soit avant ou après lui dans le document : « Macron » et « M. Macron »
|
|
28
|
+
rejoignent « Emmanuel Macron » ;
|
|
29
|
+
- **un prénom seul ne rejoint jamais un nom complet** : « Emmanuel » reste à part ;
|
|
30
|
+
- **un nom partiel ambigu n'est pas fusionné** : avec « Emmanuel Macron » et
|
|
31
|
+
« Brigitte Macron » dans le texte, « Macron » garde son propre groupe, et liste les
|
|
32
|
+
groupes possibles pour que l'étape de linking tranche ;
|
|
33
|
+
- **la règle du nom partiel ne vaut que pour les personnes** : « Airbus » ne rejoint pas
|
|
34
|
+
« Airbus Defence and Space ».
|
|
35
|
+
|
|
36
|
+
Sur chaque annotation d'un label regroupé, il écrit dans `properties` :
|
|
37
|
+
|
|
38
|
+
| Propriété | Valeur |
|
|
39
|
+
|-----------|--------|
|
|
40
|
+
| `entity_group` | l'identifiant du groupe dans le document : `g1`, `g2`… dans l'ordre de première apparition |
|
|
41
|
+
| `canonical_form` | la forme la plus longue du groupe, sans civilité : c'est elle qu'on cherche dans la base de connaissances |
|
|
42
|
+
| `mention_type` | `name` (les périphrases et les pronoms, `nominal` et `pronoun`, viendront avec les paliers 2 et 3) |
|
|
43
|
+
| `entity_group_candidates` | seulement sur un nom partiel ambigu : les groupes auxquels il peut appartenir |
|
|
44
|
+
|
|
45
|
+
Les autres propriétés de l'annotation sont gardées, et traiter deux fois un document
|
|
46
|
+
donne le même résultat. La portée est le document entier, ce qui convient à une
|
|
47
|
+
dépêche ; les documents longs sont une question ouverte (SHERPA-3048).
|
|
48
|
+
|
|
49
|
+
## Requirements
|
|
50
|
+
|
|
51
|
+
- Python 3.12+
|
|
52
|
+
- `pymultirole_plugins` (>=1.6.0,<1.7.0)
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
pip install pyprocessors-coreference
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Usage
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from pymultirole_plugins.v1.schema import Annotation, Document
|
|
64
|
+
|
|
65
|
+
from pyprocessors_coreference.coreference import CoreferenceParameters, CoreferenceProcessor
|
|
66
|
+
|
|
67
|
+
text = "Emmanuel Macron est arrivé. M. Macron a parlé."
|
|
68
|
+
document = Document(
|
|
69
|
+
text=text,
|
|
70
|
+
annotations=[
|
|
71
|
+
Annotation(start=0, end=15, labelName="person", text="Emmanuel Macron"),
|
|
72
|
+
Annotation(start=28, end=37, labelName="person", text="M. Macron"),
|
|
73
|
+
],
|
|
74
|
+
)
|
|
75
|
+
[document] = CoreferenceProcessor().process([document], CoreferenceParameters())
|
|
76
|
+
# Both annotations: entity_group "g1", canonical_form "Emmanuel Macron".
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Parameters
|
|
80
|
+
|
|
81
|
+
| Paramètre | Défaut | Description |
|
|
82
|
+
|-----------|--------|-------------|
|
|
83
|
+
| `labels` | vide | Labels des mentions à regrouper. Vide : tous les labels. |
|
|
84
|
+
| `person_labels` | vide | Labels de personne : ils forment une seule famille, et la règle du nom partiel s'y applique. Vide : tout label dont le nom contient « person » (`person`, `afpperson`…). |
|
|
85
|
+
| `honorifics` | `M.`, `Mme`, `Dr`, `Mr`… | Civilités ignorées en tête d'un nom de personne. Paramètre avancé. |
|
|
86
|
+
|
|
87
|
+
## Development
|
|
88
|
+
|
|
89
|
+
Le build est piloté par [Task](https://taskfile.dev) et [uv](https://docs.astral.sh/uv/),
|
|
90
|
+
les stages venant du submodule `python-archetype` :
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
git submodule update --init
|
|
94
|
+
task stages # les stages du pipeline, dans l'ordre
|
|
95
|
+
task # tout sauf le dernier stage — donc tout sauf la publication
|
|
96
|
+
task -- --skip-tests # idem, sans le stage de test
|
|
97
|
+
task up-to -- py:lint
|
|
98
|
+
task jenkins # tout, exactement ce que joue Jenkins
|
|
99
|
+
task --list
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`uv.lock` n'est pas versionné ici, donc `py:sync` résout les dépendances à neuf.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
status: proposed
|
|
3
|
+
date: 2026-09-24
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ADR (relais) — Le linking d'entités sépare les candidats (entity-fishing) de la décision (Jev)
|
|
7
|
+
|
|
8
|
+
- **Décision** — C'est une ADR relais : la décision est prise, et argumentée, dans l'[ADR-0001 de `pyannotators_entityfishing`](https://bitbucket.org/kairntech/pyannotators_entityfishing/src/master/docs/adr/0001-linking-candidats-entityfishing-decision-jev.md). On ne la recopie pas ici, pour que les deux textes ne divergent pas.
|
|
9
|
+
- **Pourquoi ce dépôt existe** — Il porte l'étape de **regroupement des mentions** du pipeline de linking. Elle se place entre le NER et la génération des candidats : les mentions d'une même entité sont regroupées **avant** le linking, pour être liées une seule fois, à partir de leur forme la plus longue.
|
|
10
|
+
- **Statut** — `proposed`. C'est le statut de l'ADR source qui fait foi.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
- **Source** : [ADR-0001, `pyannotators_entityfishing`](https://bitbucket.org/kairntech/pyannotators_entityfishing/src/master/docs/adr/0001-linking-candidats-entityfishing-decision-jev.md), en particulier §3 (le pipeline et le regroupement), §4 (ce qui change par dépôt) et §5 (le contrat entre les étapes).
|
|
15
|
+
- **Suivi** (essai) : Epic [SHERPA-3035](https://kairntech.atlassian.net/browse/SHERPA-3035). Tickets de ce dépôt :
|
|
16
|
+
- [SHERPA-3038](https://kairntech.atlassian.net/browse/SHERPA-3038) : le processeur, palier 1 (les noms) ;
|
|
17
|
+
- [SHERPA-3046](https://kairntech.atlassian.net/browse/SHERPA-3046) : le nom et la graine du dépôt, tranchés par sa création même ;
|
|
18
|
+
- [SHERPA-3047](https://kairntech.atlassian.net/browse/SHERPA-3047) : les paliers 2 et 3 (périphrases, pronoms) ;
|
|
19
|
+
- [SHERPA-3048](https://kairntech.atlassian.net/browse/SHERPA-3048) : la portée du regroupement pour les documents longs.
|
|
20
|
+
|
|
21
|
+
## Le nom et l'enveloppe
|
|
22
|
+
|
|
23
|
+
- **Le nom : `pyprocessors_coreference`, plutôt que `pyprocessors_name_propagator`.** Un « propagateur de noms » décrit ce que fait l'AFP aujourd'hui : recopier des liens **après** le linking. Ici, on regroupe **avant**, et les paliers 2 et 3 relèvent de la coréférence.
|
|
24
|
+
- **L'enveloppe vient de `pyprocessors_seed`** (`task init`), la graine des processeurs créée pour l'occasion. La mesure de dérive se fait par `git fetch seed && git diff seed/master -- . ':!src' ':!tests'`.
|
|
25
|
+
|
|
26
|
+
## Ce que ce dépôt doit faire
|
|
27
|
+
|
|
28
|
+
**Palier 1 (SHERPA-3038).** Le processeur ajoute à chaque annotation du NER :
|
|
29
|
+
- `properties.entity_group` : l'identifiant du groupe dans le document ;
|
|
30
|
+
- `properties.canonical_form` : la forme la plus longue du groupe, qui sert au term lookup.
|
|
31
|
+
|
|
32
|
+
Les règles s'appliquent toujours entre mentions de même label NER :
|
|
33
|
+
- **même surface normalisée → même groupe** ;
|
|
34
|
+
- **PERSON seulement : un nom partiel rejoint un nom complet vu dans le document** (« Macron » rejoint « Emmanuel Macron ») ;
|
|
35
|
+
- **un nom partiel ambigu n'est pas fusionné de force** : avec Emmanuel et Brigitte Macron dans le même texte, « Macron » garde ses deux groupes possibles.
|
|
36
|
+
|
|
37
|
+
**Le code part de `person_varnames`**, aujourd'hui en double dans `pyprocessors_afp_entities` et `pyprocessors_escrim_reconciliation`, qui a trois défauts :
|
|
38
|
+
- la portée ne va que vers l'avant ;
|
|
39
|
+
- un prénom seul est enregistré comme variante ;
|
|
40
|
+
- les civilités (« M. », « Mme ») ne sont pas retirées.
|
|
41
|
+
|
|
42
|
+
Il était prévu de l'extraire. **Le palier 1 a finalement réécrit la règle**, parce que deux des trois défauts sont dans sa logique même : les variantes par préfixe, qui produisent le prénom seul, et le parcours vers l'avant seulement. Seul le rattachement d'une fin de nom au nom complet est conservé.
|
|
43
|
+
|
|
44
|
+
## Ce que le palier 1 fait, et ses limites connues
|
|
45
|
+
|
|
46
|
+
**Une décision prise en l'écrivant : les labels de personne forment une seule famille.** Dans le pipeline AFP, le NER (`person`) et le lexique AFP (`afpperson`) annotent le **même span**. Grouper strictement par label mettait chaque personne dans deux groupes. Le paramètre `person_labels` désigne ces labels ; s'il est vide, ce sont ceux dont le nom contient « person ». Les autres labels restent groupés par label exact : `location`/`afplocation` et `organization`/`afporganization` ont sans doute le même doublon, et la question reste ouverte.
|
|
47
|
+
|
|
48
|
+
**Ce qu'est une « fin de nom ».** Le nom partiel rejoint le nom complet si ses mots sont **exactement les derniers mots** du nom complet. La comparaison se fait mot à mot, après normalisation (casse, espaces, apostrophes, civilités), jamais sur une partie de mot :
|
|
49
|
+
|
|
50
|
+
| Nom partiel | Nom complet | Résultat |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| Lucas | Franck DeLucas | reste à part : `delucas` est un seul mot |
|
|
53
|
+
| Luc | Jean-Luc Mélenchon | reste à part : « Jean-Luc » est un seul mot |
|
|
54
|
+
| Smith | Emile Smith Rowe | reste à part : ce n'est pas la fin du nom |
|
|
55
|
+
| Mélenchon, Villepin | Jean-Luc Mélenchon, Dominique de Villepin | rejoint |
|
|
56
|
+
| de Gaulle, Le Pen, Smith Rowe | Charles de Gaulle, Marine Le Pen, Emile Smith Rowe | rejoint |
|
|
57
|
+
|
|
58
|
+
**Limite connue, acceptée : un nom de famille à particule ou composé peut être amputé.** Ces rattachements se font, alors qu'ils ne correspondent pas au nom de famille entier :
|
|
59
|
+
|
|
60
|
+
| Nom partiel | Nom complet |
|
|
61
|
+
|---|---|
|
|
62
|
+
| Gaulle | Charles de Gaulle |
|
|
63
|
+
| Pen | Marine Le Pen |
|
|
64
|
+
| Lucas | Franck De Lucas |
|
|
65
|
+
| Rowe | Emile Smith Rowe |
|
|
66
|
+
|
|
67
|
+
**On n'ajoute pas de règle sur les particules (décision du 2026-09-24).** Une telle règle aurait interdit à une fin de nom de commencer juste après une particule (de, le, van, von, da, del…). Elle n'est pas retenue, pour trois raisons :
|
|
68
|
+
- **un NER isole rarement** « Gaulle » ou « Pen » ;
|
|
69
|
+
- **les noms composés sans particule** (« Smith Rowe ») resteraient de toute façon hors de portée sans dictionnaire ;
|
|
70
|
+
- **l'erreur ne pèse qu'en cas d'homonyme.** Elle ne compte que si un autre porteur du nom amputé figure dans le même document, par exemple un autre Lucas à côté de « Franck De Lucas ». Or un nom partiel qui correspond à plusieurs noms complets n'est de toute façon pas fusionné.
|
|
71
|
+
|
|
72
|
+
C'est à revoir si l'évaluation sur le corpus annoté ([SHERPA-3037](https://kairntech.atlassian.net/browse/SHERPA-3037)) montre de tels faux rattachements.
|
|
73
|
+
|
|
74
|
+
## Ce qu'il ne faut pas faire en attendant
|
|
75
|
+
|
|
76
|
+
- **Ne pas publier le squelette.** Tant que le palier 1 n'est pas écrit, le code est celui de la graine : il ajoute une catégorie à chaque document. Le premier build de `master` publierait ce squelette sur l'index sous le nom `pyprocessors-coreference`, et l'index est immuable. On travaille donc sur une branche de feature, que `publishBranches: '^master$'` empêche de publier, et `master` ne reçoit que le palier 1 terminé.
|
|
77
|
+
- **Ne pas commencer les paliers 2 et 3** avant l'évaluation de SHERPA-3047.
|
|
78
|
+
- **Ne pas câbler le processeur dans un paquet de flavor de `pymultirole_server`** avant qu'il ait été validé dans le pipeline de bout en bout ([SHERPA-3041](https://kairntech.atlassian.net/browse/SHERPA-3041)).
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyprocessors-coreference"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Processor grouping the mentions of the same entity"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Olivier Terrier", email = "olivier.terrier@kairntech.com"},
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Intended Audience :: Information Technology",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Intended Audience :: System Administrators",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
21
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
22
|
+
"Topic :: Software Development :: Libraries",
|
|
23
|
+
"Topic :: Software Development",
|
|
24
|
+
"License :: OSI Approved :: MIT License",
|
|
25
|
+
"Development Status :: 4 - Beta",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"pymultirole-plugins>=1.6.0,<1.7.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://bitbucket.org/kairntech/pyprocessors_coreference"
|
|
34
|
+
|
|
35
|
+
[project.entry-points."pyprocessors.plugins"]
|
|
36
|
+
coreference = "pyprocessors_coreference.coreference:CoreferenceProcessor"
|
|
37
|
+
|
|
38
|
+
[project.optional-dependencies]
|
|
39
|
+
test = [
|
|
40
|
+
"pytest",
|
|
41
|
+
"ruff",
|
|
42
|
+
"pip",
|
|
43
|
+
]
|
|
44
|
+
sbom = ["cyclonedx-bom", "pip-audit"]
|
|
45
|
+
|
|
46
|
+
[tool.hatch.version]
|
|
47
|
+
path = "src/pyprocessors_coreference/__init__.py"
|
|
48
|
+
|
|
49
|
+
[tool.hatch.build.targets.wheel]
|
|
50
|
+
packages = ["src/pyprocessors_coreference"]
|
|
51
|
+
|
|
52
|
+
[tool.hatch.build]
|
|
53
|
+
exclude = [
|
|
54
|
+
"/tests",
|
|
55
|
+
"Jenkinsfile",
|
|
56
|
+
"Taskfile.yml",
|
|
57
|
+
"CLAUDE.md",
|
|
58
|
+
".gitignore"
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
[tool.pytest.ini_options]
|
|
62
|
+
addopts = "--durations=5"
|
|
63
|
+
norecursedirs = ["docs"]
|
|
64
|
+
|
|
65
|
+
[tool.ruff]
|
|
66
|
+
line-length = 256
|
|
67
|
+
target-version = "py312"
|
|
68
|
+
|
|
69
|
+
[tool.ruff.lint]
|
|
70
|
+
select = ["E", "W", "F", "I", "B", "C4", "UP", "ARG", "SIM"]
|
|
71
|
+
|
|
72
|
+
[tool.ruff.format]
|
|
73
|
+
quote-style = "double"
|
|
74
|
+
indent-style = "space"
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import unicodedata
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from typing import cast
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
from pymultirole_plugins.v1.processor import ProcessorBase, ProcessorParameters
|
|
8
|
+
from pymultirole_plugins.v1.schema import Annotation, Document
|
|
9
|
+
|
|
10
|
+
# Honorifics in front of a person name, stripped before comparing names: in AFP
|
|
11
|
+
# dispatches "M. Macron" follows "Emmanuel Macron". Compared case-insensitively.
|
|
12
|
+
DEFAULT_HONORIFICS = ["M.", "MM.", "Mme", "Mmes", "Mlle", "Mlles", "Me", "Dr", "Dr.", "Pr", "Pr.", "Mr", "Mr.", "Mrs", "Mrs.", "Ms", "Ms.", "Miss", "Sir", "Dame", "Lord", "Lady"]
|
|
13
|
+
|
|
14
|
+
# Properties written on every grouped annotation (ADR-0001, §5).
|
|
15
|
+
ENTITY_GROUP = "entity_group"
|
|
16
|
+
CANONICAL_FORM = "canonical_form"
|
|
17
|
+
MENTION_TYPE = "mention_type"
|
|
18
|
+
# Only on an ambiguous partial name: the groups it may belong to.
|
|
19
|
+
ENTITY_GROUP_CANDIDATES = "entity_group_candidates"
|
|
20
|
+
|
|
21
|
+
# The family of every person label: the NER's "person" and the AFP lexicon's
|
|
22
|
+
# "afpperson" annotate the same span, and must land in the same group.
|
|
23
|
+
PERSON_FAMILY = "\x00person"
|
|
24
|
+
|
|
25
|
+
APOSTROPHES = str.maketrans({"’": "'", "‘": "'", "ʼ": "'"})
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CoreferenceParameters(ProcessorParameters):
|
|
29
|
+
"""Parameters of the coreference processor"""
|
|
30
|
+
|
|
31
|
+
labels: list[str] | None = Field(
|
|
32
|
+
None,
|
|
33
|
+
description="""Labels of the mentions to group. If empty, the mentions of every label are grouped.<br/>
|
|
34
|
+
Mentions are only grouped with mentions of the same label, except person labels, which all form one family.""",
|
|
35
|
+
json_schema_extra={"extra": "label"},
|
|
36
|
+
)
|
|
37
|
+
person_labels: list[str] | None = Field(
|
|
38
|
+
None,
|
|
39
|
+
description="""Labels of person names. They form one family (person and afpperson mentions are grouped together),
|
|
40
|
+
and a partial name ("Macron") joins the full name it ends ("Emmanuel Macron").<br/>
|
|
41
|
+
If empty, every label whose name contains "person" (person, afpperson...).""",
|
|
42
|
+
json_schema_extra={"extra": "label"},
|
|
43
|
+
)
|
|
44
|
+
honorifics: list[str] = Field(
|
|
45
|
+
DEFAULT_HONORIFICS,
|
|
46
|
+
description="Honorifics ignored in front of a person name when comparing names (M., Mme, Dr...)",
|
|
47
|
+
json_schema_extra={"extra": "advanced"},
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class CoreferenceProcessor(ProcessorBase):
|
|
52
|
+
"""Group the mentions of the same entity, before entity linking.
|
|
53
|
+
|
|
54
|
+
Tier 1 of the mention grouping of ADR-0001 (pyannotators_entityfishing): names
|
|
55
|
+
only. Every annotation of a grouped label gets an `entity_group` (the same id for
|
|
56
|
+
every mention of the entity in the document) and a `canonical_form` (the longest
|
|
57
|
+
form of the group, which the candidate generation looks up):
|
|
58
|
+
|
|
59
|
+
- same normalized surface and same label → same group; all person labels count
|
|
60
|
+
as one (the NER's "person" and the AFP lexicon's "afpperson" annotate the same
|
|
61
|
+
span);
|
|
62
|
+
- person labels only: a partial name joins the full name it ends, wherever the
|
|
63
|
+
full name is in the document ("Macron" joins "Emmanuel Macron");
|
|
64
|
+
- a first name alone never joins ("Emmanuel" stays on its own);
|
|
65
|
+
- an ambiguous partial name ("Macron" with both Emmanuel and Brigitte Macron)
|
|
66
|
+
keeps a group of its own, and lists the possible groups in
|
|
67
|
+
`entity_group_candidates` for the linking step to decide.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def process(self, documents: list[Document], parameters: ProcessorParameters) -> list[Document]:
|
|
71
|
+
params = cast(CoreferenceParameters, parameters)
|
|
72
|
+
honorifics = {h.casefold() for h in params.honorifics}
|
|
73
|
+
for document in documents:
|
|
74
|
+
group_document(document, params, honorifics)
|
|
75
|
+
return documents
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def get_model(cls) -> type[BaseModel]:
|
|
79
|
+
return CoreferenceParameters
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def group_document(document: Document, params: CoreferenceParameters, honorifics: set[str]) -> None:
|
|
83
|
+
text = document.text or ""
|
|
84
|
+
mentions = [a for a in (document.annotations or []) if a.labelName and (not params.labels or a.labelName in params.labels)]
|
|
85
|
+
if not mentions:
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
# 1. Same label, same normalized surface: one surface group.
|
|
89
|
+
surfaces: dict[tuple[str, tuple[str, ...]], list[Annotation]] = defaultdict(list)
|
|
90
|
+
for a in sorted(mentions, key=lambda a: (a.start, a.end)):
|
|
91
|
+
person = is_person_label(a.labelName, params)
|
|
92
|
+
tokens = name_tokens(surface_of(a, text), honorifics if person else set())
|
|
93
|
+
if tokens:
|
|
94
|
+
surfaces[(PERSON_FAMILY if person else a.labelName, tokens)].append(a)
|
|
95
|
+
|
|
96
|
+
# 2. Person labels: a surface that is a proper suffix of longer surfaces joins
|
|
97
|
+
# them when they are all one entity. Longest first, so that "Emmanuel Macron" has
|
|
98
|
+
# already joined "Président Emmanuel Macron" when "Macron" is considered.
|
|
99
|
+
parent = {key: key for key in surfaces}
|
|
100
|
+
|
|
101
|
+
def root(key):
|
|
102
|
+
while parent[key] != key:
|
|
103
|
+
parent[key] = parent[parent[key]]
|
|
104
|
+
key = parent[key]
|
|
105
|
+
return key
|
|
106
|
+
|
|
107
|
+
ambiguous: dict[tuple, set] = {}
|
|
108
|
+
for key in sorted(surfaces, key=lambda k: -len(k[1])):
|
|
109
|
+
family, tokens = key
|
|
110
|
+
if family != PERSON_FAMILY:
|
|
111
|
+
continue
|
|
112
|
+
longer = {root(other) for other in surfaces if other[0] == PERSON_FAMILY and is_proper_suffix(tokens, other[1])}
|
|
113
|
+
longer.discard(root(key))
|
|
114
|
+
if len(longer) == 1:
|
|
115
|
+
parent[root(key)] = longer.pop()
|
|
116
|
+
elif longer:
|
|
117
|
+
ambiguous[key] = longer
|
|
118
|
+
|
|
119
|
+
# 3. Ids in order of first appearance, canonical form = longest mention of the group.
|
|
120
|
+
members: dict[tuple, list[Annotation]] = defaultdict(list)
|
|
121
|
+
for key, annotations in surfaces.items():
|
|
122
|
+
members[root(key)].extend(annotations)
|
|
123
|
+
ordered = sorted(members, key=lambda r: min(a.start for a in members[r]))
|
|
124
|
+
group_id = {r: f"g{i}" for i, r in enumerate(ordered, start=1)}
|
|
125
|
+
canonical = {r: canonical_form(members[r], text, honorifics if r[0] == PERSON_FAMILY else set()) for r in ordered}
|
|
126
|
+
|
|
127
|
+
for key, annotations in surfaces.items():
|
|
128
|
+
r = root(key)
|
|
129
|
+
for a in annotations:
|
|
130
|
+
properties = dict(a.properties or {})
|
|
131
|
+
properties[ENTITY_GROUP] = group_id[r]
|
|
132
|
+
properties[CANONICAL_FORM] = canonical[r]
|
|
133
|
+
properties[MENTION_TYPE] = "name"
|
|
134
|
+
if key in ambiguous:
|
|
135
|
+
properties[ENTITY_GROUP_CANDIDATES] = sorted((group_id[c] for c in ambiguous[key]), key=lambda g: int(g[1:]))
|
|
136
|
+
else:
|
|
137
|
+
properties.pop(ENTITY_GROUP_CANDIDATES, None)
|
|
138
|
+
a.properties = properties
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def is_person_label(label: str, params: CoreferenceParameters) -> bool:
|
|
142
|
+
if params.person_labels:
|
|
143
|
+
return label in params.person_labels
|
|
144
|
+
return "person" in label.casefold()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def surface_of(a: Annotation, text: str) -> str:
|
|
148
|
+
return a.text if a.text is not None else text[a.start : a.end]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def name_tokens(surface: str, honorifics: set[str]) -> tuple[str, ...]:
|
|
152
|
+
"""Normalized tokens of a surface: NFKC, one apostrophe, case-folded, and the
|
|
153
|
+
leading honorifics dropped (only for person names, where the set is not empty)."""
|
|
154
|
+
tokens = unicodedata.normalize("NFKC", surface).translate(APOSTROPHES).casefold().split()
|
|
155
|
+
while len(tokens) > 1 and tokens[0] in honorifics:
|
|
156
|
+
tokens = tokens[1:]
|
|
157
|
+
return tuple(tokens)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def strip_honorifics(surface: str, honorifics: set[str]) -> str:
|
|
161
|
+
words = re.sub(r"\s+", " ", surface).strip().split(" ")
|
|
162
|
+
while len(words) > 1 and words[0].casefold() in honorifics:
|
|
163
|
+
words = words[1:]
|
|
164
|
+
return " ".join(words)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def is_proper_suffix(tokens: tuple[str, ...], other: tuple[str, ...]) -> bool:
|
|
168
|
+
return len(tokens) < len(other) and other[len(other) - len(tokens) :] == tokens
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def canonical_form(annotations: list[Annotation], text: str, honorifics: set[str]) -> str:
|
|
172
|
+
"""The longest form of the group: most words, then most characters, then the first."""
|
|
173
|
+
forms = [strip_honorifics(surface_of(a, text), honorifics) for a in sorted(annotations, key=lambda a: a.start)]
|
|
174
|
+
return max(forms, key=lambda f: (len(f.split()), len(f)))
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
|
3
|
+
"name": "kairntech-python",
|
|
4
|
+
"description": "Les outils Claude Code de Kairntech, distribues avec l'archetype qu'ils decrivent.",
|
|
5
|
+
"owner": {
|
|
6
|
+
"name": "Kairntech",
|
|
7
|
+
"email": "olivier.terrier@kairntech.com"
|
|
8
|
+
},
|
|
9
|
+
"plugins": [
|
|
10
|
+
{
|
|
11
|
+
"name": "python-archetype",
|
|
12
|
+
"description": "Cree et entretient les depots Python qui consomment python-archetype : creation d'un plugin a partir de sa graine, et mesure de la derive.",
|
|
13
|
+
"author": { "name": "Kairntech" },
|
|
14
|
+
"category": "development",
|
|
15
|
+
"source": "."
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
|
+
"name": "python-archetype",
|
|
4
|
+
"displayName": "Kairntech python-archetype",
|
|
5
|
+
"version": "1.1.0",
|
|
6
|
+
"description": "Cree et entretient les depots Python de Kairntech qui consomment python-archetype : creation d'un plugin a partir de sa graine, et mesure de la derive d'un depot par rapport a elle.",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "Kairntech",
|
|
9
|
+
"email": "olivier.terrier@kairntech.com"
|
|
10
|
+
},
|
|
11
|
+
"repository": "https://bitbucket.org/kairntech/python-archetype",
|
|
12
|
+
"license": "Proprietary",
|
|
13
|
+
"keywords": ["kairntech", "taskfile", "uv", "pyimporters", "pymultirole"],
|
|
14
|
+
"skills": "./skills/"
|
|
15
|
+
}
|