# Python API Reference This document provides a comprehensive reference for LELA Python API, built on spaCy's component architecture. ## Table of Contents - [Core Classes](#core-classes) - [spaCy Components](#spacy-components) - [Data Types](#data-types) - [Configuration](#configuration) - [Context Extraction](#context-extraction) - [Advanced Features](#advanced-features) - [Usage Examples](#usage-examples) ## Core Classes ### Lela The main entry point for LELA entity linking. Accepts a configuration as a plain dict or a path to a JSON file. **Location:** `lela/pipeline.py` ```python from lela import Lela ``` #### Constructor ```python Lela(config, *, progress_callback=None, cancel_event=None) ``` **Parameters:** - `config`: A dict with pipeline configuration, or a path (`str`/`Path`) to a JSON config file - `progress_callback`: Optional callback function `(progress: float, description: str) -> None` for tracking initialization progress (0.0 to 1.0) - `cancel_event`: Optional `threading.Event` to signal cancellation **Initialization:** - If `config` is a path, loads it as JSON - Instantiates knowledge base and document loader from registries - Builds a spaCy `Language` pipeline with configured components - Sets up caching directory **Properties:** - `nlp`: The spaCy `Language` instance with pipeline components - `kb`: Knowledge base instance - `loader`: Document loader instance #### Methods ##### `process_document(doc: Document) -> Dict` Process a single document through the spaCy pipeline. **Parameters:** - `doc`: A `Document` object to process **Returns:** ```python { "id": str, # Document ID "text": str, # Full document text "entities": List[{ # List of resolved entities "text": str, # Mention text "start": int, # Start character position "end": int, # End character position "label": str, # Entity type label "context": str, # Surrounding context "entity_id": Optional[str], # Resolved entity ID "entity_title": Optional[str], # Entity title "entity_description": Optional[str], # Entity description "candidates": List[{ # Candidate list "entity_id": str, "score": float, "description": str }] }], "meta": Dict # Document metadata } ``` ##### `process_document_with_progress(doc: Document, progress_callback: Optional[Callable], base_progress: float = 0.0, progress_range: float = 1.0) -> Dict` Process a single document with detailed progress callbacks for each pipeline stage. **Parameters:** - `doc`: A `Document` object to process - `progress_callback`: Callback function `(progress: float, description: str) -> None` - `base_progress`: Starting progress value (0.0-1.0) - `progress_range`: How much progress this processing represents (0.0-1.0) **Returns:** - Same format as `process_document` **Example:** ```python def my_progress_callback(progress: float, description: str): print(f"{progress*100:.1f}%: {description}") result = pipeline.process_document_with_progress( doc, progress_callback=my_progress_callback ) # Output: # 0.0%: Tokenizing document... # 15.0%: NER (GLiNER)... # 45.0%: Candidate generation (BM25)... # 75.0%: Disambiguation (LLM)... # 95.0%: Serializing results... # 100.0%: Document processing complete ``` ##### `run(*paths, output_path=None) -> List[Dict]` Process one or more files through the pipeline. **Parameters:** - `*paths`: One or more file paths to process - `output_path`: Optional path to write JSONL output **Returns:** - List of result dictionaries (same format as `process_document`) **Example:** ```python lela = Lela("config.json") results = lela.run("doc1.txt", "doc2.pdf", "doc3.html", output_path="results.jsonl") ``` ### Configuration `Lela` accepts a plain dict or a JSON file path. The dict has these top-level keys: | Key | Type | Description | |-----|------|-------------| | `loader` | Dict | Loader configuration | | `ner` | Dict | NER component configuration | | `candidate_generator` | Dict | Candidate generator configuration | | `reranker` | Dict | Reranker configuration | | `disambiguator` | Dict | Disambiguator configuration | | `knowledge_base` | Dict | Knowledge base configuration | | `cache_dir` | str | Directory for document caching | | `batch_size` | int | Batch size for processing | **Example:** ```python from lela import Lela # From a JSON file path lela = Lela("config.json") # From a dict lela = Lela({ "loader": {"name": "text"}, "ner": {"name": "regex", "params": {"min_len": 3}}, "candidate_generator": {"name": "fuzzy", "params": {"top_k": 10}}, "reranker": {"name": "none"}, "disambiguator": {"name": "first"}, "knowledge_base": {"name": "jsonl", "params": {"path": "kb.jsonl"}}, "cache_dir": ".ner_cache", "batch_size": 1 }) ``` ## spaCy Components All pipeline components are implemented as spaCy factories and can be used directly with spaCy's `nlp.add_pipe()` method. ### Component Registration Import the `spacy_components` module to register all factories: ```python from lela import spacy_components # Registers all factories import spacy nlp = spacy.blank("en") nlp.add_pipe("simple_ner") # Now available ``` ### spaCy Extensions The pipeline uses custom extensions on `Span` objects: ```python from spacy.tokens import Span # Automatically registered when components are loaded Span.set_extension("context", default=None) Span.set_extension("candidates", default=[]) Span.set_extension("resolved_entity", default=None) ``` ### NER Components #### `chunked_gliner_ner` Zero-shot GLiNER NER with LELA defaults. **Config Options:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `model_name` | str | "numind/NuNER_Zero-span" | GLiNER model | | `labels` | List[str] | LELA defaults | Entity types | | `threshold` | float | 0.5 | Detection threshold | | `context_mode` | str | "sentence" | Context extraction mode | **Example:** ```python nlp.add_pipe("chunked_gliner_ner", config={ "labels": ["person", "organization", "location"], "threshold": 0.4 }) ``` #### `simple_ner` Lightweight regex-based NER. **Config Options:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `min_len` | int | 3 | Minimum mention length | | `context_mode` | str | "sentence" | Context extraction mode | **Example:** ```python nlp.add_pipe("simple_ner", config={"min_len": 2}) ``` #### `gliner_ner` Standard GLiNER wrapper. **Config Options:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `model_name` | str | "urchade/gliner_base" | GLiNER model | | `labels` | List[str] | ["person", "organization", "location"] | Entity types | | `threshold` | float | 0.5 | Detection threshold | | `context_mode` | str | "sentence" | Context extraction mode | #### `ner_filter` Post-filter for spaCy's built-in NER (adds context extension). **Usage:** ```python # Load pretrained spaCy model spacy_nlp = spacy.load("en_core_web_sm") # Copy NER and add filter nlp.add_pipe("ner", source=spacy_nlp) nlp.add_pipe("ner_filter") ``` ### Candidate Generation Components #### `dense_candidates` Dense retrieval using SentenceTransformers and FAISS. **Config Options:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `model_name` | str | LELA default | Embedding model | | `top_k` | int | 64 | Maximum candidates | | `device` | str | None | Device override (e.g., "cuda", "cpu") | | `use_context` | bool | False | Include context in query | #### `fuzzy_candidates` RapidFuzz string matching. **Config Options:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `top_k` | int | 20 | Maximum candidates | #### `bm25_candidates` Standard BM25 using rank-bm25 library. **Config Options:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `top_k` | int | 20 | Maximum candidates | ### Reranker Components #### `embedder_transformers_reranker` Bi-encoder reranker using SentenceTransformers. Uses cosine similarity between query and candidate embeddings. **Config Options:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `model_name` | str | LELA default | Embedding model | | `top_k` | int | 10 | Candidates to keep | | `device` | str | None | Device override (e.g., "cuda", "cpu") | #### `embedder_vllm_reranker` Bi-encoder reranker using vLLM with task="embed". Manual L2 normalization of embeddings. **Config Options:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `model_name` | str | LELA default | Embedding model | | `top_k` | int | 10 | Candidates to keep | | `max_model_len` | int | None | vLLM context length cap | #### `cross_encoder_vllm_reranker` Cross-encoder reranker using vLLM `.score()` API with the Qwen3-Reranker-seq-cls model variant. **Config Options:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `model_name` | str | LELA default | Cross-encoder model | | `top_k` | int | 10 | Candidates to keep | | `max_model_len` | int | None | vLLM context length cap | #### `cross_encoder_reranker` Cross-encoder reranking using sentence-transformers. **Config Options:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `model_name` | str | "cross-encoder/ms-marco-MiniLM-L-6-v2" | Model | | `top_k` | int | 10 | Candidates to keep | #### `noop_reranker` Pass-through (no reranking). **Config Options:** None ### Disambiguator Components #### `vllm_disambiguator` vLLM-based LLM disambiguation - sends all candidates at once. **Config Options:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `model_name` | str | "Qwen/Qwen3-4B" | LLM model | | `tensor_parallel_size` | int | 1 | GPU parallelism | | `max_model_len` | int | None | Max context length | | `add_none_candidate` | bool | True | Add "None" option for NIL linking | | `add_descriptions` | bool | True | Include descriptions | | `enable_thinking` | bool \| None | None | Pass through to chat template; `None` = template default; auto-True for `gemma-4` models | | `system_prompt` | str | LELA default | Custom prompt | | `generation_config` | dict | {} | vLLM generation settings | | `self_consistency_k` | int | 1 | Voting samples (>1 enables majority voting; invalid answers are dropped before voting when `add_none_candidate=False`) | | `context_window` | int | 0 | Token window around mention; 0 = full doc | **Requires initialization:** ```python component = nlp.add_pipe("vllm_disambiguator") component.initialize(kb) ``` **See Also:** [Self-Consistency Voting](#self-consistency-voting), [NIL Linking](#nil-linking), [Qwen3 Thinking Mode](#qwen3-thinking-mode) #### `transformers_disambiguator` Transformers-based LLM disambiguation (alternative to vLLM). **Config Options:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `model_name` | str | "Qwen/Qwen3-4B" | LLM model | | `add_none_candidate` | bool | True | Add "None" option for NIL linking | | `add_descriptions` | bool | True | Include descriptions | | `enable_thinking` | bool \| None | None | Pass through to chat template; auto-True for `gemma-4` | | `system_prompt` | str | LELA default | Custom prompt | | `generation_config` | dict | {} | HuggingFace generation settings | | `context_window` | int | 0 | Token window around mention; 0 = full doc | **Requires initialization:** ```python component = nlp.add_pipe("transformers_disambiguator") component.initialize(kb) ``` **When to use:** Use this instead of `vllm` when: - vLLM installation fails or has compatibility problems - You need direct HuggingFace transformers integration **Example:** ```json { "disambiguator": { "name": "transformers", "params": { "model_name": "Qwen/Qwen3-4B", "enable_thinking": false } } } ``` #### `first_disambiguator` Select first candidate. **Requires initialization:** Yes (needs KB reference) ## Data Types All core data types are re-exported from the top-level package — import them with `from lela import Candidate, Document, Entity, Mention, ResolvedMention, ProgressCallback`. (Defined in `lela/_types.py`.) ### Document Represents an input document. ```python from lela import Document doc = Document( id="doc-001", text="Albert Einstein was born in Germany.", meta={"source": "wikipedia", "date": "2024-01-01"} ) ``` **Attributes:** | Attribute | Type | Description | |-----------|------|-------------| | `id` | Optional[str] | Unique document identifier | | `text` | str | Document text content | | `meta` | Dict | Optional metadata dictionary | ### Entity Represents an entity in the knowledge base. ```python from lela import Entity entity = Entity( id="Q937", title="Albert Einstein", description="German-born theoretical physicist", metadata={"birth_year": 1879} ) ``` **Attributes:** | Attribute | Type | Description | |-----------|------|-------------| | `id` | str | Unique entity identifier | | `title` | str | Entity name/title | | `description` | Optional[str] | Entity description | | `metadata` | Dict | Optional metadata dictionary | ### Candidate Represents a potential KB match for a mention. ```python from lela import Candidate candidate = Candidate( entity_id="Q937", score=0.95, description="German-born theoretical physicist" ) ``` **Attributes:** | Attribute | Type | Description | |-----------|------|-------------| | `entity_id` | str | Entity identifier in KB | | `score` | Optional[float] | Relevance score | | `description` | Optional[str] | Entity description | ## Configuration ### Component Options Summary #### Config Name → spaCy Factory Mapping | Config Name | spaCy Factory | |-------------|---------------| | **NER** | | | `regex` | `simple_ner` | | `gliner` | `gliner_ner` | | `spacy` | Built-in NER + `ner_filter` | | **Candidate Generators** | | | `dense` | `dense_candidates` | | `fuzzy` | `fuzzy_candidates` | | `bm25` | `bm25_candidates` | | **Rerankers** | | | `embedder_transformers` | `embedder_transformers_reranker` | | `embedder_vllm` | `embedder_vllm_reranker` | | `cross_encoder_vllm` | `cross_encoder_vllm_reranker` | | `cross_encoder` | `cross_encoder_reranker` | | `vllm_api_client` | `vllm_api_client_reranker` | | `llama_server` | `llama_server_reranker` | | `none` | `noop_reranker` | | **Disambiguators** | | | `vllm` | `vllm_disambiguator` | | `transformers` | `transformers_disambiguator` | | `openai_api` | `openai_api_disambiguator` | | `first` | `first_disambiguator` | **Note:** The `chunked_gliner_ner` factory is registered and can be used directly with `nlp.add_pipe()`, but is not yet available as a config name through `Lela`. #### Loaders (Registry-based) | Name | Description | |------|-------------| | `text` | Plain text files | | `pdf` | PDF documents | | `docx` | Word documents | | `html` | HTML pages | | `json` | JSON files | | `jsonl` | JSON Lines files | **JSON/JSONL Loader Parameters:** The `json` and `jsonl` loaders support a `text_field` parameter to customize which field contains the document text: ```json { "loader": { "name": "jsonl", "params": { "text_field": "content" // Default is "text" } } } ``` **Example JSONL with custom field:** ```jsonl {"id": "doc-1", "content": "Document text here", "meta": {}} {"id": "doc-2", "content": "Another document", "meta": {}} ``` #### Knowledge Bases (Registry-based) | Name | Parameters | Description | |------|------------|-------------| | `jsonl` | `path`, `cache_dir` | JSONL KB (supports persistent caching) | ## Context Extraction Utilities for extracting context around mentions. **Location:** `lela/context.py` ### Functions #### `extract_sentence_context(text, start, end, max_sentences=1)` Extract surrounding sentences containing the mention. ```python from lela.context import extract_sentence_context text = "First sentence. Albert Einstein was born in Germany. Third sentence." context = extract_sentence_context(text, start=16, end=31, max_sentences=1) # Returns: "Albert Einstein was born in Germany." ``` #### `extract_window_context(text, start, end, window_chars=150)` Extract a fixed character window around the mention. ```python from lela.context import extract_window_context context = extract_window_context(text, start=16, end=31, window_chars=100) ``` #### `extract_context(text, start, end, mode="sentence", **kwargs)` General dispatcher for context extraction. ```python from lela.context import extract_context # Sentence mode context = extract_context(text, 16, 31, mode="sentence", max_sentences=2) # Window mode context = extract_context(text, 16, 31, mode="window", window_chars=150) ``` ## Advanced Features ### Progress Callbacks The pipeline supports progress callbacks at multiple levels for tracking processing status. #### Pipeline Initialization ```python from lela import Lela def init_callback(progress: float, description: str): print(f"Init {progress*100:.0f}%: {description}") lela = Lela(config_dict, progress_callback=init_callback) # Output: # Init 0%: Loading knowledge base... # Init 15%: Initializing document loader... # Init 20%: Building spaCy pipeline... # Init 25%: Loading NER model (gliner)... # Init 45%: Loading candidate generator (dense)... # Init 75%: Loading disambiguator (vllm)... # Init 100%: Pipeline initialization complete ``` #### Document Processing ```python def process_callback(progress: float, description: str): print(f"Processing {progress*100:.0f}%: {description}") result = lela.process_document_with_progress(doc, progress_callback=process_callback) ``` --- ### Self-Consistency Voting The `vllm` disambiguator supports self-consistency voting for improved accuracy. When `self_consistency_k > 1`, the model generates multiple responses and selects the answer by majority vote. **Configuration:** ```json { "disambiguator": { "name": "vllm", "params": { "self_consistency_k": 5 // Generate 5 responses, take majority vote } } } ``` **How it works:** 1. The LLM generates `k` candidate answers for each entity 2. Each answer is parsed to extract the selected candidate index 3. The most frequently selected index wins (majority voting) **Trade-offs:** - Higher `k` = better accuracy but slower (k times more LLM calls) - Recommended: `k=3` or `k=5` for important decisions - Default: `k=1` (no voting, fastest) --- ### NIL Linking NIL linking allows the model to reject all candidates when none match the mention. This is enabled via the `add_none_candidate` parameter. **Configuration:** ```json { "disambiguator": { "name": "vllm", "params": { "add_none_candidate": true // Enable NIL linking } } } ``` **How it works:** When `add_none_candidate=true`: - Candidate index 0 is reserved for "None of the listed candidates" - The LLM prompt includes this option explicitly - If the model selects index 0, `ent._.resolved_entity` remains `None` **Prompt format with NIL option:** ``` Candidates: 0. None of the listed candidates 1. Paris (city): Capital city of France 2. Paris (novel): 1897 novel by Emile Zola 3. Paris (Texas): City in Texas, USA ``` **Output behavior:** ```python for ent in doc.ents: if ent._.resolved_entity is None: print(f"{ent.text}: Not linked (NIL)") else: print(f"{ent.text}: {ent._.resolved_entity.title}") ``` --- ### Thinking Mode Models that ship a "thinking mode" in their chat template (Qwen3, Gemma-4, etc.) emit chain-of-thought reasoning before the final answer. LELA forwards `enable_thinking` to `tokenizer.apply_chat_template(..., chat_template_kwargs={"enable_thinking": ...})` — the same mechanism vLLM / HuggingFace expose. Works the same for any model whose template understands the flag. **Configuration:** ```json { "disambiguator": { "name": "vllm", "params": { "enable_thinking": false // false = skip thinking; true = force on; omit = template default } } } ``` **Tri-state semantics (`enable_thinking`):** | Value | Effect | |-------|--------| | `true` | Force thinking on (more tokens, often better accuracy) | | `false` | Force thinking off (faster, fewer tokens) | | `null` / omit | Use the chat template's default. Auto-resolves to `true` for `gemma-4*` models (their template requires it) | **Parser:** LELA looks for `answer ...: N` first, then falls back to the last number on the last non-empty line. Models are expected to emit the final `answer: N` once at the end of the response — this holds for Qwen3 and Gemma-4 with thinking on or off. --- ## Usage Examples ### Basic Pipeline Usage ```python from lela import Lela from lela import Document # Load configuration from JSON file lela = Lela("config.json") # Process single document doc = Document( id="test-doc", text="Albert Einstein was born in Germany and later moved to the United States.", meta={} ) result = lela.process_document(doc) # Print results for entity in result["entities"]: print(f"Mention: {entity['text']}") print(f" Label: {entity['label']}") print(f" Resolved to: {entity.get('entity_title', 'N/A')}") print(f" Candidates: {len(entity['candidates'])}") ``` ### Direct spaCy Usage ```python import spacy from lela import spacy_components # Register factories from lela.knowledge_bases.jsonl import JSONLKnowledgeBase # Build custom pipeline nlp = spacy.blank("en") nlp.add_pipe("simple_ner", config={"min_len": 3}) cand_component = nlp.add_pipe("fuzzy_candidates", config={"top_k": 10}) disamb_component = nlp.add_pipe("first_disambiguator") # Initialize with knowledge base kb = JSONLKnowledgeBase(path="kb.jsonl") cand_component.initialize(kb) disamb_component.initialize(kb) # Process text doc = nlp("Albert Einstein was born in Germany.") # Access entities and their attributes for ent in doc.ents: print(f"Entity: {ent.text} ({ent.label_})") print(f" Context: {ent._.context}") print(f" Candidates: {len(ent._.candidates)}") if ent._.resolved_entity: print(f" Resolved: {ent._.resolved_entity.title}") ``` ### Processing Multiple Files ```python # Process multiple files with output results = lela.run("doc1.txt", "doc2.pdf", "doc3.html", output_path="output/results.jsonl") # Results are also returned for result in results: print(f"Document {result['id']}: {len(result['entities'])} entities") ``` ### Working with Knowledge Bases ```python from lela.knowledge_bases.jsonl import JSONLKnowledgeBase # Load knowledge base kb = JSONLKnowledgeBase(path="data/yago/yago-entities.jsonl") # or any JSONL KB # Get entity by ID entity = kb.get_entity("Q937") # Fuzzy search entities by title results = kb.search("Einstein", top_k=10) # Iterate all entities for entity in kb.all_entities(): print(f"{entity.id}: {entity.title}") ``` ### LELA-Specific Configuration ```python config_dict = { "loader": {"name": "text"}, "ner": { "name": "gliner", "params": { "model_name": "numind/NuNER_Zero-span", "labels": ["person", "organization", "location"], "threshold": 0.5 } }, "candidate_generator": { "name": "dense", "params": {"top_k": 64, "use_context": True} }, "reranker": { "name": "embedder_transformers", "params": { "model_name": "Qwen/Qwen3-Embedding-4B", "top_k": 10 } }, "disambiguator": { "name": "vllm", "params": { "model_name": "Qwen/Qwen3-8B", "tensor_parallel_size": 1, "add_none_candidate": True } }, "knowledge_base": { "name": "jsonl", "params": {"path": "data/yago/yago-entities.jsonl"} } } ``` ## Output Format The pipeline outputs JSONL (JSON Lines) format: ```json { "id": "doc-001", "text": "Albert Einstein was born in Germany.", "entities": [ { "text": "Albert Einstein", "start": 0, "end": 15, "label": "PERSON", "context": "Albert Einstein was born in Germany.", "entity_id": "Q937", "entity_title": "Albert Einstein", "entity_description": "German-born theoretical physicist", "candidates": [ { "entity_id": "Q937", "score": 0.95, "description": "German-born theoretical physicist" }, { "entity_id": "Q1234", "score": 0.45, "description": "Another Einstein" } ] }, { "text": "Germany", "start": 28, "end": 35, "label": "GPE", "context": "Albert Einstein was born in Germany.", "entity_id": "Q183", "entity_title": "Germany", "entity_description": "Country in Central Europe", "candidates": [...] } ], "meta": { "source": "wikipedia" } } ```