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

This commit is contained in:
wehub-resource-sync
2026-07-13 12:37:14 +08:00
commit 76d991c447
147 changed files with 43242 additions and 0 deletions
+158
View File
@@ -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.
+64
View File
@@ -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)
```
+177
View File
@@ -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:
![Romeo and Juliet Full Visualization](../_static/romeo_juliet_full.gif)
```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.
+252
View File
@@ -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")
```
![Medical NER Visualization](../_static/medication_entity.gif)
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")
```
![Medical Relationship Visualization](../_static/medication_entity_re.gif)
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
+221
View File
@@ -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.