| """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"}, ...}], ...}}' |
| )}, |
| ], |
| } |
|
|
| |
| |
| 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, |
| "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"} |
| |
| 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} |
|
|