File size: 9,000 Bytes
53395c6
d538019
493b437
d538019
 
 
 
 
493b437
 
 
 
 
 
 
 
55f70b7
493b437
 
99fea15
 
493b437
99412aa
493b437
25e6476
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f9b1c76
25e6476
 
 
f9b1c76
25e6476
 
f9b1c76
 
25e6476
493b437
 
 
d538019
 
 
 
 
 
 
 
 
 
 
 
 
 
7bb3ae6
d538019
f9b1c76
d538019
 
f9b1c76
d538019
 
 
 
493b437
 
d538019
b3f34c3
 
 
25e6476
b3f34c3
 
 
25e6476
b3f34c3
 
 
 
 
d538019
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b3f34c3
 
 
 
 
 
 
 
d538019
b3f34c3
 
 
 
 
 
 
d538019
 
 
b3f34c3
d538019
 
b3f34c3
 
 
 
 
 
 
 
 
 
493b437
 
 
 
 
 
 
 
 
 
 
 
d538019
493b437
 
 
 
 
 
 
 
 
55f70b7
 
 
493b437
 
 
55f70b7
 
d538019
 
 
 
 
 
 
 
 
 
55f70b7
7bb3ae6
 
55f70b7
 
 
 
 
d538019
 
55f70b7
 
 
 
 
 
 
 
 
d538019
55f70b7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
"""Span attributes demo: entities carry per-span attribute groups (sentiment,
assertion, role), single- or multi-label, scoped with applies_to.

Two examples: a clinical note (conditions with assertion status, medications,
procedures) and a mixed product review (products with sentiment, people with
role). Free-typed text falls back to the review schema.

Response contract:
  {"spans": [{"start", "end", "text", "label", "score",
              "attributes": {"<group>": [{"label": str, "score": float}]}}]}
"""

import json
import os

MOCK = os.environ.get("GLINER_MOCK", "1") == "1"
MODEL_ID = os.environ.get("MODEL_ID", "fastino/gliner2.5-multi-v1")

DEMO = {
    "title": "Span attribute extraction",
    "subtitle": "Extract descriptive labels alongside spans for richer context.",
    "model_name": "GLiNER 2.5",
    "model_variant": "0.3B parameters, f32, CPU",
    "examples": [
        {"chip": "Clinical note", "fixture": "clinical.json",
         "code": (
             'from gliner2 import AutoExtractor, AttributeGroup\n'
             '\n'
             'extractor = AutoExtractor.from_pretrained("fastino/gliner2.5-multi-v1", map_location="cpu")\n'
             'extractor.float()\n'
             '\n'
             'note = open("chart_note.txt").read()\n'
             '\n'
             'schema = (extractor.create_schema()\n'
             '    .entities({"medication": "A medication or drug mentioned in the note",\n'
             '               "condition": "A medical condition, diagnosis, or symptom",\n'
             '               "procedure": "A medical procedure or test"})\n'
             '    .entity_attributes({\n'
             '        "assertion": AttributeGroup(\n'
             '            labels=["present", "absent", "possible", "historical"],\n'
             '            applies_to=["condition"])}))\n'
             '\n'
             'result = extractor.extract(note, schema, include_spans=True, include_confidence=True)\n'
             '# -> {"entities": {"condition": [{"text": "chest pain", "assertion": {"label": "present"}, ...}], ...}}'
         )},
        {"chip": "Mixed review", "fixture": "review.json",
         "code": (
             'from gliner2 import AutoExtractor, AttributeGroup\n'
             '\n'
             'extractor = AutoExtractor.from_pretrained("fastino/gliner2.5-multi-v1", map_location="cpu")\n'
             'extractor.float()\n'
             '\n'
             'review = open("product_review.txt").read()\n'
             '\n'
             'schema = (extractor.create_schema()\n'
             '    .entities({"feature_mention": "A feature, component, or aspect of a product mentioned in the review"})\n'
             '    .entity_attributes({\n'
             '        "sentiment": AttributeGroup(\n'
             '            labels=["positive", "neutral", "negative"],\n'
             '            applies_to=["feature_mention"])}))\n'
             '\n'
             'result = extractor.extract(review, schema, include_spans=True, include_confidence=True)\n'
             '# -> {"entities": {"feature_mention": [{"text": "keyboard", "sentiment": {"label": "positive"}, ...},\n'
             '#                            {"text": "docking station", "sentiment": {"label": "negative"}, ...}], ...}}'
         )},
    ],
}

# Per-example schemas: entity types + attribute groups differ by domain.
# Keys match the fixture .txt basenames.
EXAMPLE_SCHEMAS = {
    "clinical": {
        "entities": {
            "medication": "A medication or drug mentioned in the note",
            "condition": "A medical condition, diagnosis, or symptom",
            "procedure": "A medical procedure or test",
        },
        "attributes": {
            "assertion": (["present", "absent", "possible", "historical"], ["condition"]),
        },
    },
    "review": {
        "threshold": 0.15,  # features in complaint clauses score below the 0.5 default
        "entities": {
            "feature_mention": "A feature, component, or aspect of a product mentioned in the review",
        },
        "attributes": {
            "sentiment": (["positive", "neutral", "negative"], ["feature_mention"]),
        },
    },
}

_HERE = os.path.dirname(os.path.abspath(__file__))


def config():
    """DEMO for /api/config, with result previews baked from fixtures."""
    out = {k: v for k, v in DEMO.items() if k != "code"}
    # per-example code lives on each example dict
    out["mock"] = MOCK
    out["examples"] = []
    for ex in DEMO["examples"]:
        ex2 = {**ex, "text": _text(ex)}
        ex2["result"] = _result_preview(ex["fixture"])
        out["examples"].append(ex2)
    return out


def warmup():
    if MOCK:
        for ex in DEMO["examples"]:
            _fixture(ex["fixture"], _text(ex))
    else:
        _load_real()


def infer(data):
    text = (data.get("text") or "").replace("\r", "")
    if not text.strip():
        return {"spans": []}
    if MOCK:
        return _infer_mock(text)
    return _infer_real(text)


def _text(ex):
    txt = ex.get("text")
    if txt is None:
        with open(os.path.join(_HERE, "fixtures", ex["fixture"].replace(".json", ".txt"))) as f:
            txt = f.read().rstrip("\n")
    return txt


def _schema_key(text):
    for ex in DEMO["examples"]:
        if _text(ex) == text:
            return ex["fixture"].replace(".json", "")
    return None


def _result_preview(fixture_name, per_type=2):
    """Notebook-style Out[1] preview: the API response shape, truncated per label."""
    with open(os.path.join(_HERE, "fixtures", fixture_name)) as f:
        out = json.load(f)
    entities = {}
    for sp in out["spans"]:
        entities.setdefault(sp["label"], [])
        if len(entities[sp["label"]]) < per_type:
            entities[sp["label"]].append(sp)
    counts = {}
    for sp in out["spans"]:
        counts[sp["label"]] = counts.get(sp["label"], 0) + 1
    body = []
    for label, entries in entities.items():
        entry_strs = []
        for e in entries:
            attrs = {g: [{"label": a["label"], "score": a["score"]} for a in es]
                     for g, es in e.get("attributes", {}).items() if es}
            base = (f'{{"text": {json.dumps(e["text"])}, "confidence": {e["score"]}, '
                    f'"start": {e["start"]}, "end": {e["end"]}')
            if attrs:
                base += f', "attributes": {json.dumps(attrs)}'
            base += "}"
            entry_strs.append(base)
        more = counts[label] - len(entries)
        parts = ", ".join(entry_strs)
        if more > 0:
            parts += f",\n        # ... {more} more {label} span{'' if more == 1 else 's'}"
        body.append(f'        {json.dumps(label)}: [\n            {parts}\n        ]')
    return f'{{\n    "entities": {{\n' + ",\n".join(body) + "\n    }\n}"


def _fixture(name, expect_text=None):
    with open(os.path.join(_HERE, "fixtures", name)) as f:
        out = json.load(f)
    if expect_text is not None:
        for sp in out.get("spans", []):
            assert expect_text[sp["start"]:sp["end"]] == sp["text"], \
                f"{name}: bad offsets for {sp['text']!r}"
    return out


def _infer_mock(text):
    for ex in DEMO["examples"]:
        if _text(ex) == text:
            return _fixture(ex["fixture"])
    return {"spans": []}


_model = None


def _load_real():
    global _model
    from gliner2 import AutoExtractor
    _model = AutoExtractor.from_pretrained(MODEL_ID, map_location="cpu")
    _model.float()


def _infer_real(text):
    from gliner2 import AttributeGroup

    key = _schema_key(text)
    cfg = EXAMPLE_SCHEMAS.get(key, EXAMPLE_SCHEMAS["review"])

    schema = _model.create_schema().entities(cfg["entities"])
    attr_groups = {}
    for group, (labels, applies_to) in cfg["attributes"].items():
        kwargs = {"applies_to": applies_to} if applies_to else {}
        attr_groups[group] = AttributeGroup(labels=labels, **kwargs)
    if attr_groups:
        schema = schema.entity_attributes(attr_groups)

    result = _model.extract(text, schema, threshold=cfg.get("threshold", 0.5),
                            include_spans=True, include_confidence=True)

    spans = []
    for label, entries in result.get("entities", {}).items():
        for e in entries:
            span = {
                "start": e["start"], "end": e["end"], "text": e["text"],
                "label": label, "score": e.get("confidence", 1.0),
                "attributes": {},
            }
            for k, v in e.items():
                if k not in ("text", "start", "end", "confidence"):
                    if isinstance(v, dict) and "label" in v:
                        span["attributes"][k] = [{"label": v["label"], "score": v.get("confidence", 1.0)}]
                    elif isinstance(v, list):
                        span["attributes"][k] = [{"label": x["label"], "score": x.get("confidence", 1.0)} for x in v]
            spans.append(span)
    spans.sort(key=lambda s: s["start"])
    return {"spans": spans}