chore: import upstream snapshot with attribution
Auto Update PR / update-prs (push) Has been cancelled
CI / format-check (push) Has been cancelled
CI / test (3.10) (push) Has been cancelled
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / live-api-tests (push) Has been cancelled
CI / plugin-integration-test (push) Has been cancelled
CI / ollama-integration-test (push) Has been cancelled
CI / test-fork-pr (push) Has been cancelled
Auto Update PR / update-prs (push) Has been cancelled
CI / format-check (push) Has been cancelled
CI / test (3.10) (push) Has been cancelled
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / live-api-tests (push) Has been cancelled
CI / plugin-integration-test (push) Has been cancelled
CI / ollama-integration-test (push) Has been cancelled
CI / test-fork-pr (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
# Vertex AI Batch Processing Guide
|
||||
|
||||
The Vertex AI Batch API offers significant cost savings (~50%) for large, non-time-critical workloads. `langextract` seamlessly integrates this with automatic routing, caching, and fault tolerance.
|
||||
|
||||
**[Vertex AI Batch Prediction Documentation →](https://docs.cloud.google.com/vertex-ai/generative-ai/docs/multimodal/batch-prediction-gemini)**
|
||||
**[Quotas & Limits →](https://docs.cloud.google.com/vertex-ai/generative-ai/docs/quotas#batch-prediction-quotas)**
|
||||
|
||||
## Real-World Example: Processing Shakespeare
|
||||
|
||||
This example demonstrates how to process a large text (the first ~20 pages of *Romeo and Juliet*) using the Batch API. We use a small chunk size (`max_char_buffer=500`) to generate enough chunks to trigger batch processing.
|
||||
|
||||
```python
|
||||
import requests
|
||||
import textwrap
|
||||
import langextract as lx
|
||||
import logging
|
||||
|
||||
# Configure logging to see progress (both in console and file)
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler("batch_process.log"),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
|
||||
# 1. Download Text (Shakespeare's Romeo and Juliet)
|
||||
url = "https://www.gutenberg.org/files/1513/1513-0.txt"
|
||||
print(f"Downloading {url}...")
|
||||
text = requests.get(url).text
|
||||
|
||||
# Process first ~20 pages (approx. 60k characters).
|
||||
text_subset = text[:60000]
|
||||
print(f"Processing first {len(text_subset)} characters...")
|
||||
|
||||
# 2. Define Prompt & Examples
|
||||
prompt = textwrap.dedent("""\
|
||||
Extract characters and emotions from the text.
|
||||
Use exact text from the input for extraction_text.""")
|
||||
|
||||
examples = [
|
||||
lx.data.ExampleData(
|
||||
text="ROMEO. But soft! What light through yonder window breaks?",
|
||||
extractions=[
|
||||
lx.data.Extraction(extraction_class="character", extraction_text="ROMEO"),
|
||||
lx.data.Extraction(extraction_class="emotion", extraction_text="But soft!"),
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
# 3. Configure Batch Settings
|
||||
batch_config = {
|
||||
"enabled": True,
|
||||
"threshold": 10,
|
||||
"poll_interval": 30,
|
||||
"timeout": 3600,
|
||||
# Set to True to cache results in GCS. Add timestamp to prompt to force re-run.
|
||||
"enable_caching": True,
|
||||
# Retention policy for GCS bucket (days). None for permanent.
|
||||
"retention_days": 30,
|
||||
}
|
||||
|
||||
# 4. Run Extraction
|
||||
# langextract will automatically chunk the text and submit a batch job.
|
||||
results = lx.extract(
|
||||
text_or_documents=text_subset,
|
||||
prompt_description=prompt,
|
||||
examples=examples,
|
||||
model_id="gemini-3.5-flash",
|
||||
max_char_buffer=500,
|
||||
batch_length=1000,
|
||||
language_model_params={
|
||||
"vertexai": True,
|
||||
"project": "your-gcp-project", # TODO: Replace with your Project ID.
|
||||
"location": "us-central1",
|
||||
"batch": batch_config
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## GCS File Structure
|
||||
|
||||
The library automatically creates and manages a GCS bucket for you, named:
|
||||
`langextract-{project}-{location}-batch`
|
||||
|
||||
Inside this bucket, data is organized as follows:
|
||||
|
||||
- **Input**: `batch-input/{job_name}.jsonl`
|
||||
- **Output**: `batch-input/{job_name}/dest/prediction-model-{timestamp}/predictions.jsonl`
|
||||
- **Cache**: `cache/{hash}.json` (Individual cached results)
|
||||
|
||||
## Cost Optimization & Caching
|
||||
|
||||
LangExtract's batch processing is designed to minimize costs:
|
||||
|
||||
1. **Cost Efficiency**: Vertex AI Batch predictions are typically ~50% cheaper than online predictions.
|
||||
2. **Smart Caching**:
|
||||
- Results are cached in your GCS bucket (`cache/` directory).
|
||||
- **Instant Retrieval**: Re-running identical prompts fetches results directly from storage, bypassing model inference.
|
||||
- **Reduced Inference**: You avoid paying for redundant model calls on previously processed data.
|
||||
- **Lifecycle Management**: Use `retention_days` (e.g., 30) to automatically clean up old data and manage storage usage.
|
||||
|
||||
## Analyze Results
|
||||
|
||||
```python
|
||||
print(f"Extracted {len(results.extractions)} entities.")
|
||||
print("First 5 extractions:")
|
||||
for extraction in results.extractions[:5]:
|
||||
print(f"- {extraction.extraction_class}: {extraction.extraction_text}")
|
||||
```
|
||||
|
||||
## Sample Output
|
||||
|
||||
```text
|
||||
Extracted 767 entities.
|
||||
First 5 extractions:
|
||||
- character: ESCALUS
|
||||
- character: MERCUTIO
|
||||
- character: PARIS
|
||||
- character: Page to Paris
|
||||
- character: MONTAGUE
|
||||
```
|
||||
|
||||
> **Note on `batch_length`**: The `batch_length` parameter controls how many chunks are submitted in a single batch job. For optimal performance with the Batch API, set this to a high value (e.g., `1000`) to process all chunks in a single job rather than multiple sequential jobs.
|
||||
|
||||
## Key Features
|
||||
|
||||
### 1. Automatic Routing
|
||||
`langextract` automatically switches between real-time and batch APIs based on your `threshold`.
|
||||
- **< Threshold**: Uses real-time API for immediate results.
|
||||
- **>= Threshold**: Uses Batch API for cost savings.
|
||||
|
||||
### 2. Fault Tolerance & Caching
|
||||
Built-in GCS caching (`enable_caching=True`) allows you to resume interrupted jobs without re-processing completed items, saving time and cost.
|
||||
|
||||
### 3. Automated Storage
|
||||
`langextract` handles all GCS operations automatically using a dedicated bucket (`gs://langextract-{project}-{location}-batch`). Note that input/output files are retained for debugging.
|
||||
|
||||
## Tracking Job Status
|
||||
|
||||
To monitor progress, you can watch the log file from a separate terminal:
|
||||
|
||||
```bash
|
||||
tail -f batch_process.log
|
||||
```
|
||||
|
||||
When running a batch job, `langextract` provides clear log feedback with a direct link to the Google Cloud Console:
|
||||
|
||||
```text
|
||||
INFO - Batch job created successfully: projects/123456789/locations/us-central1/batchPredictionJobs/987654321
|
||||
INFO - Job State: JobState.JOB_STATE_PENDING
|
||||
INFO - Job Console URL: https://console.cloud.google.com/vertex-ai/jobs/batch-predictions/987654321?project=123456789
|
||||
INFO - Batch job is running... (State: JOB_STATE_PENDING)
|
||||
INFO - Batch job is running... (State: JOB_STATE_RUNNING)
|
||||
```
|
||||
|
||||
- **Completion**: Once the job succeeds, `langextract` automatically downloads, parses, and aligns the results.
|
||||
@@ -0,0 +1,64 @@
|
||||
# Japanese Information Extraction
|
||||
|
||||
This example demonstrates how to use LangExtract to extract structured information from Japanese text.
|
||||
|
||||
> **Note:** For non-spaced languages like Japanese, use `UnicodeTokenizer` to ensure correct character-based segmentation and alignment.
|
||||
|
||||
## Full Pipeline Example
|
||||
|
||||
```python
|
||||
import langextract as lx
|
||||
from langextract.core import tokenizer
|
||||
|
||||
# Japanese text with entities (Person, Location, Organization)
|
||||
# "Mr. Tanaka from Tokyo works at Google."
|
||||
input_text = "東京出身の田中さんはGoogleで働いています。"
|
||||
|
||||
# Define extraction prompt
|
||||
prompt_description = "Extract named entities including Person, Location, and Organization."
|
||||
|
||||
# Define example data (few-shot examples help the model understand the task)
|
||||
examples = [
|
||||
lx.data.ExampleData(
|
||||
text="大阪の山田さんはソニーに入社しました。", # Mr. Yamada from Osaka joined Sony.
|
||||
extractions=[
|
||||
lx.data.Extraction(extraction_class="Location", extraction_text="大阪"),
|
||||
lx.data.Extraction(extraction_class="Person", extraction_text="山田"),
|
||||
lx.data.Extraction(extraction_class="Organization", extraction_text="ソニー"),
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
# 1. Initialize the UnicodeTokenizer
|
||||
# Essential for Japanese to ensure correct grapheme segmentation.
|
||||
unicode_tokenizer = tokenizer.UnicodeTokenizer()
|
||||
|
||||
# 2. Run Extraction with the Custom Tokenizer
|
||||
result = lx.extract(
|
||||
text_or_documents=input_text,
|
||||
prompt_description=prompt_description,
|
||||
examples=examples,
|
||||
model_id="gemini-3.5-flash",
|
||||
tokenizer=unicode_tokenizer, # <--- Pass the tokenizer here
|
||||
api_key="your-api-key-here" # Optional if env var is set
|
||||
)
|
||||
|
||||
# 3. Display Results
|
||||
print(f"Input: {input_text}\n")
|
||||
print("Extracted Entities:")
|
||||
for entity in result.extractions:
|
||||
position_info = ""
|
||||
if entity.char_interval:
|
||||
start, end = entity.char_interval.start_pos, entity.char_interval.end_pos
|
||||
position_info = f" (pos: {start}-{end})"
|
||||
|
||||
print(f"• {entity.extraction_class}: {entity.extraction_text}{position_info}")
|
||||
|
||||
# Expected Output:
|
||||
# Input: 東京出身の田中さんはGoogleで働いています。
|
||||
#
|
||||
# Extracted Entities:
|
||||
# • Location: 東京 (pos: 0-2)
|
||||
# • Person: 田中 (pos: 5-7)
|
||||
# • Organization: Google (pos: 10-16)
|
||||
```
|
||||
@@ -0,0 +1,177 @@
|
||||
# *Romeo and Juliet* Full Text Extraction
|
||||
|
||||
LangExtract can process entire documents directly from URLs, handling large texts with high accuracy through parallel processing and enhanced sensitivity features. This example demonstrates extraction from the complete text of *Romeo and Juliet* from Project Gutenberg.
|
||||
|
||||
## Example code
|
||||
|
||||
The following code uses a comprehensive prompt and examples optimized for large, complex literary texts. For large complex inputs, using more detailed examples is suggested to increase extraction robustness.
|
||||
|
||||
> **Warning:** Running this example processes a large document (~44 000 tokens) and will incur costs. For large-scale use, a Tier 2 Gemini quota is suggested to avoid rate-limit issues ([details](https://ai.google.dev/gemini-api/docs/rate-limits#tier-2)). Please review the [Gemini API pricing](https://ai.google.dev/gemini-api/docs/pricing) before proceeding.
|
||||
|
||||
```python
|
||||
import langextract as lx
|
||||
import textwrap
|
||||
from collections import Counter, defaultdict
|
||||
|
||||
# Define comprehensive prompt and examples for complex literary text
|
||||
prompt = textwrap.dedent("""\
|
||||
Extract characters, emotions, and relationships from the given text.
|
||||
|
||||
Provide meaningful attributes for every entity to add context and depth.
|
||||
|
||||
Important: Use exact text from the input for extraction_text. Do not paraphrase.
|
||||
Extract entities in order of appearance with no overlapping text spans.
|
||||
|
||||
Note: In play scripts, speaker names appear in ALL-CAPS followed by a period.""")
|
||||
|
||||
examples = [
|
||||
lx.data.ExampleData(
|
||||
text=textwrap.dedent("""\
|
||||
ROMEO. But soft! What light through yonder window breaks?
|
||||
It is the east, and Juliet is the sun.
|
||||
JULIET. O Romeo, Romeo! Wherefore art thou Romeo?"""),
|
||||
extractions=[
|
||||
lx.data.Extraction(
|
||||
extraction_class="character",
|
||||
extraction_text="ROMEO",
|
||||
attributes={"emotional_state": "wonder"}
|
||||
),
|
||||
lx.data.Extraction(
|
||||
extraction_class="emotion",
|
||||
extraction_text="But soft!",
|
||||
attributes={"feeling": "gentle awe", "character": "Romeo"}
|
||||
),
|
||||
lx.data.Extraction(
|
||||
extraction_class="relationship",
|
||||
extraction_text="Juliet is the sun",
|
||||
attributes={"type": "metaphor", "character_1": "Romeo", "character_2": "Juliet"}
|
||||
),
|
||||
lx.data.Extraction(
|
||||
extraction_class="character",
|
||||
extraction_text="JULIET",
|
||||
attributes={"emotional_state": "yearning"}
|
||||
),
|
||||
lx.data.Extraction(
|
||||
extraction_class="emotion",
|
||||
extraction_text="Wherefore art thou Romeo?",
|
||||
attributes={"feeling": "longing question", "character": "Juliet"}
|
||||
),
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
# Process Romeo & Juliet directly from Project Gutenberg
|
||||
print("Downloading and processing Romeo and Juliet from Project Gutenberg...")
|
||||
|
||||
result = lx.extract(
|
||||
text_or_documents="https://www.gutenberg.org/files/1513/1513-0.txt",
|
||||
prompt_description=prompt,
|
||||
examples=examples,
|
||||
model_id="gemini-3.5-flash",
|
||||
extraction_passes=3, # Multiple passes for improved recall
|
||||
max_workers=20, # Parallel processing for speed
|
||||
max_char_buffer=1000 # Smaller contexts for better accuracy
|
||||
)
|
||||
|
||||
print(f"Extracted {len(result.extractions)} entities from {len(result.text):,} characters")
|
||||
|
||||
# Save and visualize the results
|
||||
lx.io.save_annotated_documents([result], output_name="romeo_juliet_extractions.jsonl", output_dir=".")
|
||||
|
||||
# Generate the interactive visualization
|
||||
html_content = lx.visualize("romeo_juliet_extractions.jsonl")
|
||||
with open("romeo_juliet_visualization.html", "w") as f:
|
||||
if hasattr(html_content, 'data'):
|
||||
f.write(html_content.data) # For Jupyter/Colab
|
||||
else:
|
||||
f.write(html_content)
|
||||
|
||||
print("Interactive visualization saved to romeo_juliet_visualization.html")
|
||||
```
|
||||
|
||||
This creates an interactive HTML visualization for exploring the extracted entities:
|
||||
|
||||

|
||||
|
||||
```python
|
||||
|
||||
# Analyze character mentions
|
||||
characters = {}
|
||||
for e in result.extractions:
|
||||
if e.extraction_class == "character":
|
||||
char_name = e.extraction_text
|
||||
if char_name not in characters:
|
||||
characters[char_name] = {"count": 0, "attributes": set()}
|
||||
characters[char_name]["count"] += 1
|
||||
if e.attributes:
|
||||
for attr_key, attr_val in e.attributes.items():
|
||||
characters[char_name]["attributes"].add(f"{attr_key}: {attr_val}")
|
||||
|
||||
# Print character summary
|
||||
print(f"\nCHARACTER SUMMARY ({len(characters)} unique characters)")
|
||||
print("=" * 60)
|
||||
|
||||
sorted_chars = sorted(characters.items(), key=lambda x: x[1]["count"], reverse=True)
|
||||
for char_name, char_data in sorted_chars[:10]: # Top 10 characters
|
||||
attrs_preview = list(char_data["attributes"])[:3]
|
||||
attrs_str = f" ({', '.join(attrs_preview)})" if attrs_preview else ""
|
||||
print(f"{char_name}: {char_data['count']} mentions{attrs_str}")
|
||||
|
||||
# Entity type breakdown
|
||||
entity_counts = Counter(e.extraction_class for e in result.extractions)
|
||||
print(f"\nENTITY TYPE BREAKDOWN")
|
||||
print("=" * 60)
|
||||
for entity_type, count in entity_counts.most_common():
|
||||
percentage = (count / len(result.extractions)) * 100
|
||||
print(f"{entity_type}: {count} ({percentage:.1f}%)")
|
||||
```
|
||||
|
||||
## Sample output
|
||||
|
||||
```
|
||||
Downloading and processing Romeo and Juliet from Project Gutenberg...
|
||||
Downloaded 147,843 characters (25,976 words) from 1513-0.txt
|
||||
Extracted 4,088 entities from 147,843 characters
|
||||
Interactive visualization saved to romeo_juliet_visualization.html
|
||||
|
||||
CHARACTER SUMMARY (153 unique characters)
|
||||
============================================================
|
||||
ROMEO: 287 mentions (emotional_state: excitement, emotional_state: eager to please)
|
||||
JULIET: 204 mentions (emotional_state: fond, emotional_state: resilient)
|
||||
NURSE: 168 mentions (emotional_state: reporting, emotional_state: teasing and evasive)
|
||||
MERCUTIO: 107 mentions (emotional_state: approving, emotional_state: responsive)
|
||||
BENVOLIO: 82 mentions (emotional_state: cautious, emotional_state: teasing)
|
||||
|
||||
ENTITY TYPE BREAKDOWN
|
||||
============================================================
|
||||
character: 1,685 (41.2%)
|
||||
emotion: 1,524 (37.3%)
|
||||
relationship: 879 (21.5%)
|
||||
```
|
||||
|
||||
## Key benefits for long documents
|
||||
|
||||
### Sequential extraction passes
|
||||
|
||||
Multiple extraction passes improve recall by performing independent extractions and merging non-overlapping results. Each pass uses identical parameters and processing—they are independent runs of the same extraction task. The number of passes is controlled by the `extraction_passes` parameter (e.g., `extraction_passes=3`).
|
||||
|
||||
**How it works**: Each pass processes the full text independently using the same prompt and examples. Results are then merged using a "first-pass wins" strategy for overlapping entities, while adding unique non-overlapping entities from later passes. This approach captures entities that might be missed in any single run due to the stochastic nature of language model generation.
|
||||
|
||||
### Portable and Interoperable Data with JSONL
|
||||
LangExtract uses JSONL, a human-readable format ideal for language model data. Each line is a self-contained JSON object, making outputs easy to parse, share, and integrate with other tools. You can save results with `lx.io.save_annotated_documents` and reload them for later analysis, ensuring your data is both portable and persistent.
|
||||
|
||||
### Optimal long context management
|
||||
While single-inference approaches can be powerful, their accuracy may be affected by distant context. LangExtract uses smart chunking strategies that respect text delimiters (such as paragraph breaks) to keep context intact and well-formed for the model. Users can configure context sizes (`max_char_buffer`) combined with parallel processing (`max_workers`) to maintain extraction quality across large documents. Multiple sequential extraction passes further enhance sensitivity by capturing entities that might be missed in any single run due to the stochastic nature of language model generation.
|
||||
|
||||
### Enhanced accuracy through chunking
|
||||
The chunked processing approach can improve extraction quality over a single inference pass on a large document because each chunk uses a smaller, more manageable context size. This helps the model focus on the most relevant information and prevents interference from distant context. While the overall latency and time required remain similar due to parallelization, the extraction quality can be substantially higher with better entity coverage and more accurate attribute assignment across the entire document.¹
|
||||
|
||||
### Interactive visualization at scale
|
||||
Seamlessly explore hundreds or thousands of entities through interactive HTML visualizations generated directly from JSONL output files. The generated visualizations handle large result sets efficiently, providing navigation and detailed entity inspection capabilities for comprehensive analysis of complex documents.
|
||||
|
||||
### Schema-guided knowledge extraction
|
||||
LangExtract combines precise text positioning with world knowledge enrichment, enabling extraction of information not explicitly stated in the text (like character identities and traits). Under the hood, the library implements [Controlled Generation](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/control-generated-output) with supported models to ensure extracted data adheres to your specified schema while maintaining robust extractions across large inputs.
|
||||
|
||||
---
|
||||
|
||||
¹ Models like Gemini 1.5 Pro show strong performance on many benchmarks, but [needle-in-a-haystack tests](https://cloud.google.com/blog/products/ai-machine-learning/the-needle-in-the-haystack-test-and-how-gemini-pro-solves-it) across million-token contexts indicate that performance can vary in multi-fact retrieval scenarios. This demonstrates how LangExtract's smaller context windows approach ensures consistently high quality across entire documents by avoiding the complexity and potential degradation of massive single-context processing.
|
||||
@@ -0,0 +1,252 @@
|
||||
# Medication Extraction Examples
|
||||
|
||||
LangExtract excels at extracting structured medical information from clinical text, making it particularly useful for healthcare applications. The methodology originated from research in medical information extraction, where early versions of the techniques were demonstrated to accelerate annotation tasks significantly.
|
||||
|
||||
> **Disclaimer:** This demonstration is only for illustrative purposes of LangExtract's baseline capability. It does not represent a finished or approved product, is not intended to diagnose or suggest treatment of any disease or condition, and should not be used for medical advice.
|
||||
|
||||
---
|
||||
|
||||
**Medical Information Extraction Research:**
|
||||
The concepts and methods underlying LangExtract were first demonstrated in:
|
||||
|
||||
Goel, A., Lehman, E., Gulati, A., Chen, R., Nori, H., Hager, G. D., & Durr, N. J. (2023).
|
||||
"LLMs Accelerate Annotation for Medical Information Extraction."
|
||||
*Machine Learning for Health (ML4H), PMLR, 2023*.
|
||||
[arXiv:2312.02296](https://arxiv.org/abs/2312.02296)
|
||||
|
||||
---
|
||||
|
||||
## Basic Named Entity Recognition (NER)
|
||||
|
||||
In this basic medical example, LangExtract extracts structured medication information:
|
||||
|
||||
```python
|
||||
import langextract as lx
|
||||
|
||||
# Text with a medication mention
|
||||
input_text = "Patient took 400 mg PO Ibuprofen q4h for two days."
|
||||
|
||||
# Define extraction prompt
|
||||
prompt_description = "Extract medication information including medication name, dosage, route, frequency, and duration in the order they appear in the text."
|
||||
|
||||
# Define example data with entities in order of appearance
|
||||
examples = [
|
||||
lx.data.ExampleData(
|
||||
text="Patient was given 250 mg IV Cefazolin TID for one week.",
|
||||
extractions=[
|
||||
lx.data.Extraction(extraction_class="dosage", extraction_text="250 mg"),
|
||||
lx.data.Extraction(extraction_class="route", extraction_text="IV"),
|
||||
lx.data.Extraction(extraction_class="medication", extraction_text="Cefazolin"),
|
||||
lx.data.Extraction(extraction_class="frequency", extraction_text="TID"), # TID = three times a day
|
||||
lx.data.Extraction(extraction_class="duration", extraction_text="for one week")
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
result = lx.extract(
|
||||
text_or_documents=input_text,
|
||||
prompt_description=prompt_description,
|
||||
examples=examples,
|
||||
model_id="gemini-2.5-pro",
|
||||
api_key="your-api-key-here" # Optional if LANGEXTRACT_API_KEY environment variable is set
|
||||
)
|
||||
|
||||
# Display entities with positions
|
||||
print(f"Input: {input_text}\n")
|
||||
print("Extracted entities:")
|
||||
for entity in result.extractions:
|
||||
position_info = ""
|
||||
if entity.char_interval:
|
||||
start, end = entity.char_interval.start_pos, entity.char_interval.end_pos
|
||||
position_info = f" (pos: {start}-{end})"
|
||||
print(f"• {entity.extraction_class.capitalize()}: {entity.extraction_text}{position_info}")
|
||||
|
||||
# Save and visualize the results
|
||||
lx.io.save_annotated_documents([result], output_name="medical_ner_extraction.jsonl", output_dir=".")
|
||||
|
||||
# Generate the interactive visualization
|
||||
html_content = lx.visualize("medical_ner_extraction.jsonl")
|
||||
with open("medical_ner_visualization.html", "w") as f:
|
||||
if hasattr(html_content, 'data'):
|
||||
f.write(html_content.data) # For Jupyter/Colab
|
||||
else:
|
||||
f.write(html_content)
|
||||
|
||||
print("Interactive visualization saved to medical_ner_visualization.html")
|
||||
```
|
||||
|
||||

|
||||
|
||||
This will produce an output similar to:
|
||||
|
||||
```
|
||||
Input: Patient took 400 mg PO Ibuprofen q4h for two days.
|
||||
|
||||
Extracted entities:
|
||||
• Dosage: 400 mg (pos: 13-19)
|
||||
• Route: PO (pos: 20-22)
|
||||
• Medication: Ibuprofen (pos: 23-32)
|
||||
• Frequency: q4h (pos: 33-36)
|
||||
• Duration: for two days (pos: 37-49)
|
||||
Interactive visualization saved to medical_ner_visualization.html
|
||||
```
|
||||
|
||||
The interactive HTML visualization allows you to explore the extracted entities visually, with each entity type color-coded and clickable for detailed inspection.
|
||||
|
||||
## Relationship Extraction (RE)
|
||||
|
||||
For more complex extractions that involve relationships between entities, LangExtract can also extract structured relationships. This example shows how to extract medications and their associated attributes:
|
||||
|
||||
```python
|
||||
import langextract as lx
|
||||
|
||||
# Text with interleaved medication mentions
|
||||
input_text = """
|
||||
The patient was prescribed Lisinopril and Metformin last month.
|
||||
He takes the Lisinopril 10mg daily for hypertension, but often misses
|
||||
his Metformin 500mg dose which should be taken twice daily for diabetes.
|
||||
"""
|
||||
|
||||
# Define extraction prompt
|
||||
prompt_description = """
|
||||
Extract medications with their details, using attributes to group related information:
|
||||
|
||||
1. Extract entities in the order they appear in the text
|
||||
2. Each entity must have a 'medication_group' attribute linking it to its medication
|
||||
3. All details about a medication should share the same medication_group value
|
||||
"""
|
||||
|
||||
# Define example data with medication groups
|
||||
examples = [
|
||||
lx.data.ExampleData(
|
||||
text="Patient takes Aspirin 100mg daily for heart health and Simvastatin 20mg at bedtime.",
|
||||
extractions=[
|
||||
# First medication group
|
||||
lx.data.Extraction(
|
||||
extraction_class="medication",
|
||||
extraction_text="Aspirin",
|
||||
attributes={"medication_group": "Aspirin"} # Group identifier
|
||||
),
|
||||
lx.data.Extraction(
|
||||
extraction_class="dosage",
|
||||
extraction_text="100mg",
|
||||
attributes={"medication_group": "Aspirin"}
|
||||
),
|
||||
lx.data.Extraction(
|
||||
extraction_class="frequency",
|
||||
extraction_text="daily",
|
||||
attributes={"medication_group": "Aspirin"}
|
||||
),
|
||||
lx.data.Extraction(
|
||||
extraction_class="condition",
|
||||
extraction_text="heart health",
|
||||
attributes={"medication_group": "Aspirin"}
|
||||
),
|
||||
|
||||
# Second medication group
|
||||
lx.data.Extraction(
|
||||
extraction_class="medication",
|
||||
extraction_text="Simvastatin",
|
||||
attributes={"medication_group": "Simvastatin"}
|
||||
),
|
||||
lx.data.Extraction(
|
||||
extraction_class="dosage",
|
||||
extraction_text="20mg",
|
||||
attributes={"medication_group": "Simvastatin"}
|
||||
),
|
||||
lx.data.Extraction(
|
||||
extraction_class="frequency",
|
||||
extraction_text="at bedtime",
|
||||
attributes={"medication_group": "Simvastatin"}
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
result = lx.extract(
|
||||
text_or_documents=input_text,
|
||||
prompt_description=prompt_description,
|
||||
examples=examples,
|
||||
model_id="gemini-2.5-pro",
|
||||
api_key="your-api-key-here" # Optional if LANGEXTRACT_API_KEY environment variable is set
|
||||
)
|
||||
|
||||
# Display grouped medications
|
||||
print(f"Input text: {input_text.strip()}\n")
|
||||
print("Extracted Medications:")
|
||||
|
||||
# Group by medication
|
||||
medication_groups = {}
|
||||
for extraction in result.extractions:
|
||||
if not extraction.attributes or "medication_group" not in extraction.attributes:
|
||||
print(f"Warning: Missing medication_group for {extraction.extraction_text}")
|
||||
continue
|
||||
|
||||
group_name = extraction.attributes["medication_group"]
|
||||
medication_groups.setdefault(group_name, []).append(extraction)
|
||||
|
||||
# Print each medication group
|
||||
for med_name, extractions in medication_groups.items():
|
||||
print(f"\n* {med_name}")
|
||||
for extraction in extractions:
|
||||
position_info = ""
|
||||
if extraction.char_interval:
|
||||
start, end = extraction.char_interval.start_pos, extraction.char_interval.end_pos
|
||||
position_info = f" (pos: {start}-{end})"
|
||||
print(f" • {extraction.extraction_class.capitalize()}: {extraction.extraction_text}{position_info}")
|
||||
|
||||
# Save and visualize the results
|
||||
lx.io.save_annotated_documents(
|
||||
[result],
|
||||
output_name="medical_relationship_extraction.jsonl",
|
||||
output_dir="."
|
||||
)
|
||||
|
||||
# Generate the interactive visualization
|
||||
html_content = lx.visualize("medical_relationship_extraction.jsonl")
|
||||
with open("medical_relationship_visualization.html", "w") as f:
|
||||
if hasattr(html_content, 'data'):
|
||||
f.write(html_content.data) # For Jupyter/Colab
|
||||
else:
|
||||
f.write(html_content)
|
||||
|
||||
print("Interactive visualization saved to medical_relationship_visualization.html")
|
||||
```
|
||||
|
||||

|
||||
|
||||
This will produce output similar to:
|
||||
|
||||
```
|
||||
Input text: The patient was prescribed Lisinopril and Metformin last month.
|
||||
He takes the Lisinopril 10mg daily for hypertension, but often misses
|
||||
his Metformin 500mg dose which should be taken twice daily for diabetes.
|
||||
|
||||
Extracted Medications:
|
||||
|
||||
* Lisinopril
|
||||
• Medication: Lisinopril (pos: 28-38)
|
||||
• Dosage: 10mg (pos: 89-93)
|
||||
• Frequency: daily (pos: 94-99)
|
||||
• Condition: hypertension (pos: 104-116)
|
||||
|
||||
* Metformin
|
||||
• Medication: Metformin (pos: 43-52)
|
||||
• Dosage: 500mg (pos: 149-154)
|
||||
• Frequency: twice daily (pos: 182-193)
|
||||
• Condition: diabetes (pos: 198-206)
|
||||
Interactive visualization saved to medical_relationship_visualization.html
|
||||
```
|
||||
|
||||
The visualization highlights how the `medication_group` attributes connect related entities, making it easy to see which dosages, frequencies, and conditions belong to each medication. Each medication group is visually distinguished in the interactive display.
|
||||
|
||||
**Understanding Relationship Extraction:**
|
||||
This example demonstrates how attributes enable efficient relationship extraction. Using the `medication_group` attribute as a linking key, related entities are grouped together logically. This approach simplifies extracting connected information and eliminates the need for additional processing steps, while preserving the precise alignment between extracted text and its original location in the document. The interactive visualization makes these relationships immediately apparent, with connected entities sharing visual groupings and color coding.
|
||||
|
||||
## Key Features Demonstrated
|
||||
|
||||
- **Named Entity Recognition**: Extracts entities with their types (medication, dosage, route, etc.)
|
||||
- **Relationship Extraction**: Groups related entities using attributes
|
||||
- **Position Tracking**: Records exact positions of extracted entities in the source text
|
||||
- **Structured Output**: Organizes information in a format suitable for healthcare applications
|
||||
- **Interactive Visualization**: Generates HTML visualizations for exploring complex medical extractions with entity groupings and relationships clearly displayed
|
||||
@@ -0,0 +1,221 @@
|
||||
# Custom Output Schema Example
|
||||
|
||||
LangExtract usually derives provider schema constraints from examples. For
|
||||
advanced cases, pass `output_schema` to constrain the raw model output more
|
||||
directly. This example restricts a `status` attribute to the enum values
|
||||
`present` and `absent`.
|
||||
|
||||
Examples are optional when `output_schema` is provided. When examples are
|
||||
included, they still guide the prompt; `output_schema` replaces only the
|
||||
provider schema constraint. The schema must describe LangExtract's JSON output
|
||||
envelope with a top-level `extractions` array.
|
||||
|
||||
Gemini and OpenAI support `output_schema`. Ollama does not currently support
|
||||
user-provided output schemas.
|
||||
|
||||
The helper emits `additionalProperties: False` so schemas work with OpenAI
|
||||
strict structured outputs. Gemini receives user-provided schemas through its
|
||||
native JSON Schema field, so JSON Schema keywords such as
|
||||
`additionalProperties` are preserved.
|
||||
|
||||
## Full Pipeline Example
|
||||
|
||||
```python
|
||||
import langextract as lx
|
||||
|
||||
# Text with one affirmed and one negated condition.
|
||||
input_text = "Patient has hypertension. Patient denies diabetes."
|
||||
|
||||
# Define extraction prompt.
|
||||
prompt_description = """
|
||||
Extract medical conditions and classify each condition status as present or
|
||||
absent. Use exact text from the input for extraction_text.
|
||||
"""
|
||||
|
||||
# Define example data. The status values mirror the enum in output_schema.
|
||||
examples = [
|
||||
lx.data.ExampleData(
|
||||
text="Patient has asthma. Patient denies fever.",
|
||||
extractions=[
|
||||
lx.data.Extraction(
|
||||
extraction_class="condition",
|
||||
extraction_text="asthma",
|
||||
attributes={"status": "present"},
|
||||
),
|
||||
lx.data.Extraction(
|
||||
extraction_class="condition",
|
||||
extraction_text="fever",
|
||||
attributes={"status": "absent"},
|
||||
),
|
||||
],
|
||||
)
|
||||
]
|
||||
|
||||
# Build a LangExtract output envelope with an enum-constrained attribute.
|
||||
output_schema = lx.schema.extractions_schema(
|
||||
lx.schema.extraction_item_schema(
|
||||
"condition",
|
||||
attributes={
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["present", "absent"],
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
result = lx.extract(
|
||||
text_or_documents=input_text,
|
||||
prompt_description=prompt_description,
|
||||
examples=examples,
|
||||
model_id="gemini-3.5-flash",
|
||||
output_schema=output_schema,
|
||||
temperature=0.0,
|
||||
)
|
||||
|
||||
print(f"Input: {input_text}\n")
|
||||
print("Extracted conditions:")
|
||||
for extraction in result.extractions:
|
||||
status = extraction.attributes["status"]
|
||||
print(f"• {extraction.extraction_text}: {status}")
|
||||
```
|
||||
|
||||
This will produce output similar to:
|
||||
|
||||
```text
|
||||
Input: Patient has hypertension. Patient denies diabetes.
|
||||
|
||||
Extracted conditions:
|
||||
• hypertension: present
|
||||
• diabetes: absent
|
||||
```
|
||||
|
||||
## Multiple Extraction Classes
|
||||
|
||||
For heterogeneous extraction classes, pass multiple item schemas. The helper
|
||||
wraps them in `anyOf` under `extractions.items`:
|
||||
|
||||
```python
|
||||
output_schema = lx.schema.extractions_schema(
|
||||
lx.schema.extraction_item_schema("condition"),
|
||||
lx.schema.extraction_item_schema("medication"),
|
||||
)
|
||||
```
|
||||
|
||||
## Raw Schema Equivalent
|
||||
|
||||
For full control, pass a raw JSON schema dictionary. When targeting OpenAI
|
||||
strict mode, every object schema must declare `required` fields and
|
||||
`additionalProperties: False`.
|
||||
|
||||
Attribute objects use the `<extraction_class>_attributes` property name.
|
||||
LangExtract's resolver expects that suffix when parsing raw model output.
|
||||
Each extraction item should use extraction-class text keys such as
|
||||
`condition`; generic fields such as `extraction_class`, `extraction_text`,
|
||||
and `attributes` are not resolver output keys. Extraction class names ending
|
||||
in `_attributes` are reserved for attribute objects.
|
||||
|
||||
The full pipeline example above produces this equivalent envelope:
|
||||
|
||||
```python
|
||||
output_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"extractions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"condition": {"type": "string"},
|
||||
"condition_attributes": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["present", "absent"],
|
||||
}
|
||||
},
|
||||
"required": ["status"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
},
|
||||
"required": ["condition", "condition_attributes"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
}
|
||||
},
|
||||
"required": ["extractions"],
|
||||
"additionalProperties": False,
|
||||
}
|
||||
```
|
||||
|
||||
Use raw schemas when you need JSON Schema constructs that the helpers do not
|
||||
cover directly, such as custom `anyOf` variants. OpenAI strict structured
|
||||
outputs support `anyOf`; use `strict=False` only in lower-level provider code
|
||||
if you need to experiment with schema features outside OpenAI's strict subset.
|
||||
|
||||
## Optional Attributes
|
||||
|
||||
The helper marks every supplied attribute as required by default so the schema
|
||||
is compatible with OpenAI strict structured outputs. To allow an attribute to
|
||||
be absent in practice, make the value nullable while keeping the key required:
|
||||
|
||||
```python
|
||||
output_schema = lx.schema.extractions_schema(
|
||||
lx.schema.extraction_item_schema(
|
||||
"condition",
|
||||
attributes={
|
||||
"status": {
|
||||
"anyOf": [
|
||||
{"type": "string", "enum": ["present", "absent"]},
|
||||
{"type": "null"},
|
||||
]
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
If you need a schema where an attribute key may be omitted entirely, use a raw
|
||||
schema for that provider-specific shape.
|
||||
|
||||
## Errors and Pitfalls
|
||||
|
||||
- Invalid envelopes raise `InferenceConfigError` before provider construction.
|
||||
- `output_schema` can be passed with either `model_id`/`config` or a
|
||||
preconfigured `model` when the provider supports user schemas.
|
||||
- Examples are optional with `output_schema`. When supplied, keep example
|
||||
classes and attribute names aligned with the schema to avoid confusing the
|
||||
model.
|
||||
- `output_schema` requires raw JSON provider output. Leave `format_type` unset
|
||||
or set it to `lx.data.FormatType.JSON`, and do not force fences.
|
||||
- Keep the resolver's default `"_attributes"` suffix. Custom
|
||||
`attribute_suffix`/`extraction_attributes_suffix` settings are incompatible
|
||||
with the raw schema envelope.
|
||||
- Do not combine `output_schema` with provider schema kwargs such as
|
||||
`response_format`, `response_schema`, or `response_json_schema`.
|
||||
- When targeting Gemini 2.0 models, add Gemini's `propertyOrdering` keyword
|
||||
to object schemas that need an explicit property order. The LangExtract
|
||||
helpers stay provider-neutral and do not add that Gemini-specific extension.
|
||||
- Raw schemas must describe `extractions.items` inline, including each
|
||||
extraction text key and `<extraction_class>_attributes` object. LangExtract
|
||||
does not resolve `$ref` for those resolver keys before provider construction.
|
||||
- Use `anyOf`, not `oneOf`, for item unions. Gemini treats `oneOf` like
|
||||
`anyOf`, and OpenAI strict structured outputs reject `oneOf`.
|
||||
- `lx.schema.extraction_item_schema(..., additional_properties=False)` applies
|
||||
that setting to both the outer extraction item object and its nested
|
||||
`<extraction_class>_attributes` object.
|
||||
- OpenAI uses strict structured outputs by default with LangExtract's default
|
||||
schema name. The lower-level `OpenAISchema.from_schema_dict(...,
|
||||
schema_name=..., strict=False)` constructor is an escape hatch for callers
|
||||
configuring provider models directly.
|
||||
- LangExtract validates only the output envelope locally; the provider API
|
||||
validates the JSON schema itself. OpenAI strict mode requires every object
|
||||
to list all properties in `required` and set
|
||||
`additionalProperties: false` — the `lx.schema` helpers emit compliant
|
||||
schemas, and the OpenAI API reports the exact path of any violation in
|
||||
hand-written schemas. Schema size/depth limits, enum limits, and keyword
|
||||
support also vary by provider, model, and endpoint.
|
||||
- Avoid `stop`/`stop_sequences` with `output_schema`: stop sequences can
|
||||
truncate schema-constrained JSON mid-document while the response still
|
||||
reports a normal finish reason.
|
||||
Reference in New Issue
Block a user