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
+222
View File
@@ -0,0 +1,222 @@
# Custom Provider Plugin Example
This example demonstrates how to create a custom provider plugin that extends LangExtract with your own model backend.
**Note**: This is an example included in the LangExtract repository for reference. It is not part of the LangExtract package and won't be installed when you `pip install langextract`.
**Automated Creation**: Instead of manually copying this example, use the [provider plugin generator script](../../scripts/create_provider_plugin.py):
```bash
python scripts/create_provider_plugin.py MyProvider --with-schema
```
This will create a complete plugin structure with all boilerplate code ready for customization.
## Structure
```
custom_provider_plugin/
├── pyproject.toml # Package configuration and metadata
├── README.md # This file
├── langextract_provider_example/ # Package directory
│ ├── __init__.py # Package initialization
│ ├── provider.py # Custom provider implementation
│ └── schema.py # Custom schema implementation (optional)
└── test_example_provider.py # Test script
```
## Key Components
### Provider Implementation (`provider.py`)
```python
from langextract.core import base_model
from langextract.providers import router
@router.register(
r'^gemini', # Pattern for model IDs this provider handles
)
class CustomGeminiProvider(base_model.BaseLanguageModel):
def __init__(self, model_id: str, **kwargs):
# Initialize your backend client
def infer(self, batch_prompts, **kwargs):
# Call your backend API and return results
```
### Package Configuration (`pyproject.toml`)
```toml
[project.entry-points."langextract.providers"]
custom_gemini = "langextract_provider_example:CustomGeminiProvider"
```
This entry point allows LangExtract to automatically discover your provider.
### Custom Schema Support (`schema.py`)
Providers can optionally implement custom schemas for structured output:
**Flow:** Examples → `from_examples()``to_provider_config()` → Provider kwargs → Inference
```python
from langextract.core import schema as core_schema
class CustomProviderSchema(core_schema.BaseSchema):
@classmethod
def from_examples(cls, examples_data, attribute_suffix="_attributes"):
# Analyze examples to find patterns
# Build schema based on extraction classes and attributes seen
return cls(schema_dict)
def to_provider_config(self):
# Convert schema to provider kwargs
return {
"response_schema": self._schema_dict,
"enable_structured_output": True
}
@property
def requires_raw_output(self):
# True = provider emits raw JSON, no markdown fences needed
return True
```
Then in your provider:
```python
class CustomProvider(base_model.BaseLanguageModel):
@classmethod
def get_schema_class(cls):
return CustomProviderSchema # Tell LangExtract about your schema
def __init__(self, **kwargs):
# Receive schema config in kwargs when use_schema_constraints=True
self.response_schema = kwargs.get('response_schema')
def infer(self, batch_prompts, **kwargs):
# Use schema during API calls
if self.response_schema:
config['response_schema'] = self.response_schema
```
## Installation
```bash
# Navigate to this example directory first
cd examples/custom_provider_plugin
# Install in development mode
pip install -e .
# Test the provider (must be run from this directory)
python test_example_provider.py
```
## Usage
Since this example registers the same pattern as the default Gemini provider, you must explicitly specify it:
```python
import langextract as lx
# Option A: build a model explicitly and pass it to extract()
config = lx.factory.ModelConfig(
model_id="gemini-3.5-flash",
provider="CustomGeminiProvider",
provider_kwargs={"api_key": "your-api-key"},
)
model = lx.factory.create_model(config)
result = lx.extract(
text_or_documents="Your text here",
model=model,
prompt_description="Extract key information",
examples=[...],
)
# Option B: let extract() build the model from a ModelConfig
result = lx.extract(
text_or_documents="Your text here",
config=lx.factory.ModelConfig(
model_id="gemini-3.5-flash",
provider="CustomGeminiProvider",
provider_kwargs={"api_key": "your-api-key"},
),
prompt_description="Extract key information",
examples=[...],
)
```
## Creating Your Own Provider - Step by Step
### 1. Copy and Rename
```bash
# Copy this example directory
cp -r examples/custom_provider_plugin/ ~/langextract-myprovider/
# Rename the package directory
cd ~/langextract-myprovider/
mv langextract_provider_example langextract_myprovider
```
### 2. Update Package Configuration
Edit `pyproject.toml`:
- Change `name = "langextract-myprovider"`
- Update description and author information
- Change entry point: `myprovider = "langextract_myprovider:MyProvider"`
### 3. Modify Provider Implementation
Edit `provider.py`:
- Change class name from `CustomGeminiProvider` to `MyProvider`
- Update `@router.register(...)` patterns to match your model IDs
- Replace Gemini API calls with your backend
- Add any provider-specific parameters
### 4. Add Schema Support (Optional)
Edit `schema.py`:
- Rename to `MyProviderSchema`
- Customize `from_examples()` for your extraction format
- Update `to_provider_config()` for your API requirements
- Implement `requires_raw_output` (abstract in `BaseSchema`) based on whether your provider emits raw JSON/YAML or fenced output
### 5. Install and Test
```bash
# Install in development mode
pip install -e .
# Test your provider
python -c "
from langextract.providers import load_plugins_once, router
load_plugins_once()
print('Provider registered:', any('myprovider' in str(e) for e in router.list_entries()))
"
```
### 6. Write Tests
- Test that your provider loads and handles basic inference
- Verify schema support works (if implemented)
- Test error handling for your specific API
### 7. Publish to PyPI and Share with Community
```bash
# Build package
python -m build
# Upload to PyPI
twine upload dist/*
```
**Share with the community:**
- Submit a PR to add your provider to the [Community Providers Registry](../../COMMUNITY_PROVIDERS.md)
- Open an issue on [LangExtract GitHub](https://github.com/google/langextract/issues) to announce your provider and get feedback
## Common Pitfalls to Avoid
1. **Forgetting to trigger plugin loading** - Plugins load lazily, use `load_plugins_once()` in tests
2. **Pattern conflicts** - Avoid patterns that conflict with built-in providers
3. **Missing dependencies** - List all requirements in `pyproject.toml`
4. **Schema mismatches** - Test schema generation with real examples
5. **Not handling None schema** - Provider must clear schema when `apply_schema(None)` is called (see provider.py for implementation)
## License
Apache License 2.0
@@ -0,0 +1,20 @@
# Copyright 2025 Google LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Example custom provider plugin for LangExtract."""
from langextract_provider_example.provider import CustomGeminiProvider
__all__ = ["CustomGeminiProvider"]
__version__ = "0.1.0"
@@ -0,0 +1,184 @@
# Copyright 2025 Google LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Minimal example of a custom provider plugin for LangExtract."""
from __future__ import annotations
import dataclasses
from typing import Any, Iterator, Sequence
from langextract_provider_example import schema as custom_schema
import langextract as lx
from langextract.core import base_model
from langextract.core import schema as core_schema
from langextract.core import types
from langextract.providers import router
@router.register(
r'^gemini', # Matches Gemini model IDs (same as default provider)
)
@dataclasses.dataclass(init=False)
class CustomGeminiProvider(base_model.BaseLanguageModel):
"""Example custom LangExtract provider implementation.
This demonstrates how to create a custom provider for LangExtract
that can intercept and handle model requests. This example wraps
the actual Gemini API to show how custom schemas integrate, but you
would replace the Gemini calls with your own API or model implementation.
Note: Since this registers the same pattern as the default Gemini provider,
you must explicitly specify this provider when creating a model:
config = lx.factory.ModelConfig(
model_id="gemini-3.5-flash",
provider="CustomGeminiProvider"
)
model = lx.factory.create_model(config)
"""
model_id: str
api_key: str | None
temperature: float
response_schema: dict[str, Any] | None = None
enable_structured_output: bool = False
_client: Any = dataclasses.field(repr=False, compare=False)
def __init__(
self,
model_id: str = 'gemini-3.5-flash',
api_key: str | None = None,
temperature: float = 0.0,
**kwargs: Any,
) -> None:
"""Initialize the custom provider.
Args:
model_id: The model ID.
api_key: API key for the service.
temperature: Sampling temperature.
**kwargs: Additional parameters.
"""
super().__init__()
# TODO: Replace with your own client initialization
try:
from google import genai # pylint: disable=import-outside-toplevel
except ImportError as e:
raise lx.exceptions.InferenceConfigError(
'This example requires google-genai package. '
'Install with: pip install google-genai'
) from e
self.model_id = model_id
self.api_key = api_key
self.temperature = temperature
# Schema kwargs from CustomProviderSchema.to_provider_config()
self.response_schema = kwargs.get('response_schema')
self.enable_structured_output = kwargs.get(
'enable_structured_output', False
)
# Store any additional kwargs for potential use
self._extra_kwargs = kwargs
if not self.api_key:
raise lx.exceptions.InferenceConfigError(
'API key required. Set GEMINI_API_KEY or pass api_key parameter.'
)
self._client = genai.Client(api_key=self.api_key)
@classmethod
def get_schema_class(cls) -> type[core_schema.BaseSchema] | None:
"""Return our custom schema class.
This allows LangExtract to use our custom schema implementation
when use_schema_constraints=True is specified.
Returns:
Our custom schema class that will be used to generate constraints.
"""
return custom_schema.CustomProviderSchema
def apply_schema(
self, schema_instance: core_schema.BaseSchema | None
) -> None:
"""Apply or clear schema configuration.
This method is called by LangExtract to dynamically apply schema
constraints after the provider is instantiated. It's important to
handle both the application of a new schema and clearing (None).
Args:
schema_instance: The schema to apply, or None to clear existing schema.
"""
super().apply_schema(schema_instance)
if schema_instance:
# Apply the new schema configuration
config = schema_instance.to_provider_config()
self.response_schema = config.get('response_schema')
self.enable_structured_output = config.get(
'enable_structured_output', False
)
else:
# Clear the schema configuration
self.response_schema = None
self.enable_structured_output = False
def infer(
self, batch_prompts: Sequence[str], **kwargs: Any
) -> Iterator[Sequence[types.ScoredOutput]]:
"""Run inference on a batch of prompts.
Args:
batch_prompts: Input prompts to process.
**kwargs: Additional generation parameters.
Yields:
Lists of ScoredOutputs, one per prompt.
"""
config = {
'temperature': kwargs.get('temperature', self.temperature),
}
# Add other parameters if provided
for key in ['max_output_tokens', 'top_p', 'top_k']:
if key in kwargs:
config[key] = kwargs[key]
# Apply schema constraints if configured
if self.response_schema and self.enable_structured_output:
# For Gemini, this ensures the model outputs JSON matching our schema
# Adapt this section based on your actual provider's API requirements
config['response_schema'] = self.response_schema
config['response_mime_type'] = 'application/json'
for prompt in batch_prompts:
try:
# TODO: Replace this with your own API/model calls
response = self._client.models.generate_content(
model=self.model_id, contents=prompt, config=config
)
output = response.text.strip()
yield [types.ScoredOutput(score=1.0, output=output)]
except Exception as e:
raise lx.exceptions.InferenceRuntimeError(
f'API error: {str(e)}', original=e
) from e
@@ -0,0 +1,163 @@
# Copyright 2025 Google LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Example custom schema implementation for provider plugins."""
from __future__ import annotations
from typing import Any, Sequence
import langextract as lx
from langextract.core import schema as core_schema
class CustomProviderSchema(core_schema.BaseSchema):
"""Example custom schema implementation for a provider plugin.
This demonstrates how plugins can provide their own schema implementations
that integrate with LangExtract's schema system. Custom schemas allow
providers to:
1. Generate provider-specific constraints from examples
2. Control output formatting and validation
3. Optimize for their specific model capabilities
This example generates a JSON schema from the examples and passes it to
the Gemini backend (which this example provider wraps) for structured output.
"""
def __init__(self, schema_dict: dict[str, Any], raw_output: bool = True):
"""Initialize the custom schema.
Args:
schema_dict: The generated JSON schema dictionary.
raw_output: Whether the provider emits raw JSON without fence markers
(True when JSON mode is guaranteed; False when output needs fencing).
"""
self._schema_dict = schema_dict
self._raw_output = raw_output
@classmethod
def from_examples(
cls,
examples_data: Sequence[lx.data.ExampleData],
attribute_suffix: str = "_attributes",
) -> CustomProviderSchema:
"""Generate schema from example data.
This method analyzes the provided examples to build a schema that
captures the structure of expected extractions. Called automatically
by LangExtract when use_schema_constraints=True.
Args:
examples_data: Example extractions to learn from.
attribute_suffix: Suffix for attribute fields (unused in this example).
Returns:
A configured CustomProviderSchema instance.
Example:
If examples contain extractions with class "condition" and attribute
"severity", the schema will constrain the model to only output those
specific classes and attributes.
"""
extraction_classes = set()
attribute_keys = set()
for example in examples_data:
for extraction in example.extractions:
extraction_classes.add(extraction.extraction_class)
if extraction.attributes:
attribute_keys.update(extraction.attributes.keys())
schema_dict = {
"type": "object",
"properties": {
"extractions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"extraction_class": {
"type": "string",
"enum": (
list(extraction_classes)
if extraction_classes
else None
),
},
"extraction_text": {"type": "string"},
"attributes": {
"type": "object",
"properties": {
key: {"type": "string"}
for key in attribute_keys
},
},
},
"required": ["extraction_class", "extraction_text"],
},
},
},
"required": ["extractions"],
}
# Remove enum if no classes found
if not extraction_classes:
del schema_dict["properties"]["extractions"]["items"]["properties"][
"extraction_class"
]["enum"]
return cls(schema_dict, raw_output=True)
def to_provider_config(self) -> dict[str, Any]:
"""Convert schema to provider-specific configuration.
This is called after from_examples() and returns kwargs that will be
passed to the provider's __init__ method. The provider can then use
these during inference.
Returns:
Dictionary of provider kwargs that will be passed to the model.
In this example, we return both the schema and a flag to enable
structured output mode.
Note:
These kwargs are merged with user-provided kwargs, with user values
taking precedence (caller-wins merge semantics).
"""
return {
"response_schema": self._schema_dict,
"enable_structured_output": True,
"output_format": "json",
}
@property
def requires_raw_output(self) -> bool:
"""Whether the provider emits raw JSON/YAML without fence markers.
Required abstract property of `BaseSchema`. Return True when the
provider guarantees syntactically valid JSON (so no fence markers
are needed), False when output should be wrapped in fences.
"""
return self._raw_output
@property
def schema_dict(self) -> dict[str, Any]:
"""Access the underlying schema dictionary.
Returns:
The JSON schema dictionary.
"""
return self._schema_dict
@@ -0,0 +1,38 @@
# Copyright 2025 Google LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "langextract-provider-example" # Change to your package name
version = "0.1.0" # Update version for releases
description = "Example custom provider plugin for LangExtract"
readme = "README.md"
requires-python = ">=3.10"
license = {text = "Apache-2.0"}
dependencies = [
# Uncomment when creating a standalone plugin package:
# "langextract", # Will install latest version
"google-genai>=0.2.0", # Replace with your backend's SDK
]
# Register the provider with LangExtract's plugin system
[project.entry-points."langextract.providers"]
custom_gemini = "langextract_provider_example:CustomGeminiProvider"
[tool.setuptools.packages.find]
where = ["."]
include = ["langextract_provider_example*"]
@@ -0,0 +1,58 @@
#!/usr/bin/env python3
# Copyright 2025 Google LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Simple test for the custom provider plugin."""
import os
import dotenv
# Import the provider to trigger registration with LangExtract
# Note: This manual import is only needed when running without installation.
# After `pip install -e .`, the entry point system handles this automatically.
from langextract_provider_example import CustomGeminiProvider # noqa: F401
import langextract as lx
def main():
"""Test the custom provider."""
dotenv.load_dotenv(override=True)
api_key = os.getenv("GEMINI_API_KEY") or os.getenv("LANGEXTRACT_API_KEY")
if not api_key:
print("Set GEMINI_API_KEY or LANGEXTRACT_API_KEY to test")
return
config = lx.factory.ModelConfig(
model_id="gemini-3.5-flash",
provider="CustomGeminiProvider",
provider_kwargs={"api_key": api_key},
)
model = lx.factory.create_model(config)
print(f"✓ Created {model.__class__.__name__}")
# Test inference
prompts = ["Say hello"]
results = list(model.infer(prompts))
if results and results[0]:
print(f"✓ Inference worked: {results[0][0].output[:50]}...")
else:
print("✗ No response")
if __name__ == "__main__":
main()
@@ -0,0 +1,257 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "header"
},
"source": [
"# Romeo and Juliet Text Extraction with LangExtract\n",
"\n",
"This notebook demonstrates extracting characters, emotions, and relationships from Shakespeare's Romeo and Juliet using LangExtract.\n",
"\n",
"[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/google/langextract/blob/main/examples/notebooks/romeo_juliet_extraction.ipynb)"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "setup_header"
},
"source": [
"## Setup"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "install"
},
"outputs": [],
"source": [
"# Install LangExtract\n",
"%pip install -q langextract"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "api_key"
},
"outputs": [],
"source": [
"# Set up your Gemini API key\n",
"# Get your key from: https://aistudio.google.com/app/apikey\n",
"import os\n",
"from getpass import getpass\n",
"\n",
"if 'GEMINI_API_KEY' not in os.environ:\n",
" os.environ['GEMINI_API_KEY'] = getpass('Enter your Gemini API key: ')"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "define_header"
},
"source": [
"## Define Extraction Task"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "setup_extraction"
},
"outputs": [],
"source": [
"import langextract as lx\n",
"import textwrap\n",
"\n",
"# Define the extraction task\n",
"prompt = textwrap.dedent(\"\"\"\\\n",
" Extract characters, emotions, and relationships in order of appearance.\n",
" Use exact text for extractions. Do not paraphrase or overlap entities.\n",
" Provide meaningful attributes for each entity to add context.\"\"\")\n",
"\n",
"# Provide a high-quality example\n",
"examples = [\n",
" lx.data.ExampleData(\n",
" text=\"ROMEO. But soft! What light through yonder window breaks? It is the east, and Juliet is the sun.\",\n",
" extractions=[\n",
" lx.data.Extraction(\n",
" extraction_class=\"character\",\n",
" extraction_text=\"ROMEO\",\n",
" attributes={\"emotional_state\": \"wonder\"}\n",
" ),\n",
" lx.data.Extraction(\n",
" extraction_class=\"emotion\",\n",
" extraction_text=\"But soft!\",\n",
" attributes={\"feeling\": \"gentle awe\"}\n",
" ),\n",
" lx.data.Extraction(\n",
" extraction_class=\"relationship\",\n",
" extraction_text=\"Juliet is the sun\",\n",
" attributes={\"type\": \"metaphor\"}\n",
" ),\n",
" ]\n",
" )\n",
"]"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "extract_header"
},
"source": [
"## Extract from Sample Text"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "simple_extraction"
},
"outputs": [],
"source": [
"# Simple extraction from a short text\n",
"input_text = \"Lady Juliet gazed longingly at the stars, her heart aching for Romeo\"\n",
"\n",
"result = lx.extract(\n",
" text_or_documents=input_text,\n",
" prompt_description=prompt,\n",
" examples=examples,\n",
" model_id=\"gemini-3.5-flash\",\n",
")\n",
"\n",
"# Display results\n",
"print(f\"Extracted {len(result.extractions)} entities:\\n\")\n",
"for extraction in result.extractions:\n",
" print(f\"• {extraction.extraction_class}: '{extraction.extraction_text}'\")\n",
" if extraction.attributes:\n",
" for key, value in extraction.attributes.items():\n",
" print(f\" - {key}: {value}\")"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "viz_header"
},
"source": [
"## Interactive Visualization"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "visualization"
},
"outputs": [],
"source": [
"# Save results to JSONL\n",
"lx.io.save_annotated_documents([result], output_name=\"romeo_juliet.jsonl\", output_dir=\".\")\n",
"\n",
"# Generate interactive visualization\n",
"html_content = lx.visualize(\"romeo_juliet.jsonl\")\n",
"\n",
"# Display in notebook\n",
"print(\"Interactive visualization (hover over highlights to see attributes):\")\n",
"html_content"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "save_viz"
},
"outputs": [],
"source": [
"# Save visualization to file (for downloading)\n",
"with open(\"romeo_juliet_visualization.html\", \"w\") as f:\n",
" # Handle both Jupyter (HTML object) and non-Jupyter (string) environments\n",
" if hasattr(html_content, 'data'):\n",
" f.write(html_content.data)\n",
" else:\n",
" f.write(html_content)\n",
"\n",
"print(\"✓ Visualization saved to romeo_juliet_visualization.html\")\n",
"print(\"You can download this file from the Files panel on the left.\")"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "experiment_header"
},
"source": [
"## Try Your Own Text\n",
"\n",
"Experiment with your own Shakespeare quotes or any literary text!"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "experiment"
},
"outputs": [],
"source": [
"# Try your own text\n",
"your_text = \"\"\"\n",
"JULIET: O Romeo, Romeo! wherefore art thou Romeo?\n",
"Deny thy father and refuse thy name;\n",
"Or, if thou wilt not, be but sworn my love,\n",
"And I'll no longer be a Capulet.\n",
"\"\"\"\n",
"\n",
"custom_result = lx.extract(\n",
" text_or_documents=your_text,\n",
" prompt_description=prompt,\n",
" examples=examples,\n",
" model_id=\"gemini-3.5-flash\",\n",
")\n",
"\n",
"print(\"Extractions from your text:\\n\")\n",
"for e in custom_result.extractions:\n",
" print(f\"• {e.extraction_class}: '{e.extraction_text}'\")\n",
" if e.attributes:\n",
" for key, value in e.attributes.items():\n",
" print(f\" - {key}: {value}\")"
]
}
],
"metadata": {
"colab": {
"name": "Romeo and Juliet Text Extraction with LangExtract",
"provenance": []
},
"kernelspec": {
"display_name": "venv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.5"
}
},
"nbformat": 4,
"nbformat_minor": 0
}
+35
View File
@@ -0,0 +1,35 @@
# Ignore Python cache
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
# Ignore version control
.git/
.gitignore
# Ignore OS files
.DS_Store
Thumbs.db
# Ignore virtual environments
venv/
env/
.venv/
# Ignore IDE files
.vscode/
.idea/
*.swp
*.swo
# Ignore test artifacts
.pytest_cache/
.coverage
htmlcov/
# Ignore build artifacts
build/
dist/
*.egg-info/
+23
View File
@@ -0,0 +1,23 @@
# Copyright 2025 Google LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM python:3.11-slim-bookworm
WORKDIR /app
RUN pip install langextract
COPY demo_ollama.py .
CMD ["python", "demo_ollama.py"]
+68
View File
@@ -0,0 +1,68 @@
# Ollama Examples
This directory contains examples for using LangExtract with Ollama for local LLM inference.
For setup instructions and documentation, see the [main README's Ollama section](../../README.md#using-local-llms-with-ollama).
## Quick Reference
**Option 1: Run locally**
```bash
# Install and start Ollama
ollama pull gemma2:2b
ollama serve # Keep this running in a separate terminal
# Run the demo
python demo_ollama.py
```
**Option 2: Run with Docker**
```bash
# Runs both Ollama and the demo in containers
docker-compose up
```
## Files
- `demo_ollama.py` - Comprehensive extraction examples demonstrating Ollama on README examples
- `docker-compose.yml` - Production-ready Docker setup with health checks
- `Dockerfile` - Container definition for LangExtract
## Configuration Options
### Timeout Settings
For slower models or large prompts, you may need to increase the timeout (default: 120 seconds):
```python
import langextract as lx
result = lx.extract(
text_or_documents=input_text,
prompt_description=prompt,
examples=examples,
model_id="llama3.1:70b", # Larger model may need more time
timeout=300, # 5 minutes
model_url="http://localhost:11434",
)
```
Or using ModelConfig:
```python
config = lx.factory.ModelConfig(
model_id="llama3.1:70b",
provider_kwargs={
"model_url": "http://localhost:11434",
"timeout": 300, # 5 minutes
}
)
```
## Model License
Ollama models come with their own licenses. For example:
- Gemma models: [Gemma Terms of Use](https://ai.google.dev/gemma/terms)
- Llama models: [Meta Llama License](https://llama.meta.com/llama-downloads/)
Please review the license for any model you use.
+561
View File
@@ -0,0 +1,561 @@
#!/usr/bin/env python3
# Copyright 2025 Google LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Comprehensive demo of Ollama integration with FormatHandler.
This example demonstrates:
- Using the pre-configured OLLAMA_FORMAT_HANDLER for consistent configuration
- Running multiple extraction examples with progress bars
- Generating interactive HTML visualizations
- Handling various extraction patterns (NER, relationships, dialogue extraction)
Prerequisites:
1. Install Ollama: https://ollama.com/
2. Pull the model: ollama pull gemma2:2b
3. Start Ollama: ollama serve
Usage:
python demo_ollama.py [--model MODEL_NAME]
Examples:
# Use default model (gemma2:2b)
python demo_ollama.py
# Use a different model
python demo_ollama.py --model llama3.2:3b
Output:
Results are saved to test_output/ directory (gitignored)
- JSONL files with extraction data
- Interactive HTML visualizations
"""
import argparse
import os
from pathlib import Path
import sys
import textwrap
import time
import traceback
import urllib.error
import urllib.request
import dotenv
import langextract as lx
from langextract.providers import ollama
dotenv.load_dotenv(override=True)
DEFAULT_MODEL = "gemma2:2b"
DEFAULT_OLLAMA_URL = os.environ.get("OLLAMA_HOST", "http://localhost:11434")
OUTPUT_DIR = "test_output"
def check_ollama_available(url: str = DEFAULT_OLLAMA_URL) -> bool:
"""Check if Ollama is available at the specified URL."""
try:
with urllib.request.urlopen(f"{url}/api/tags", timeout=2) as response:
return response.status == 200
except (urllib.error.URLError, TimeoutError):
return False
def ensure_output_directory() -> Path:
"""Create output directory if it doesn't exist."""
output_path = Path(OUTPUT_DIR)
output_path.mkdir(exist_ok=True)
return output_path
def print_header(title: str, width: int = 80) -> None:
"""Print a formatted header."""
print("\n" + "=" * width)
print(f" {title}")
print("=" * width)
def print_section(title: str, width: int = 60) -> None:
"""Print a formatted section."""
print(f"\n{title}")
print("-" * width)
def print_results_summary(extractions: list[lx.data.Extraction]) -> None:
"""Print a summary of extraction results."""
if not extractions:
print(" No extractions found")
return
class_counts = {}
for ext in extractions:
class_counts[ext.extraction_class] = (
class_counts.get(ext.extraction_class, 0) + 1
)
print(f" Total extractions: {len(extractions)}")
print(" By type:")
for cls, count in sorted(class_counts.items()):
print(f"{cls}: {count}")
def example_romeo_juliet(
model_id: str, model_url: str
) -> lx.data.AnnotatedDocument | None:
"""Romeo & Juliet character and emotion extraction example."""
print_section("Example 1: Romeo & Juliet - Characters and Emotions")
prompt = textwrap.dedent("""\
Extract characters, emotions, and relationships in order of appearance.
Use exact text for extractions. Do not paraphrase or overlap entities.
Provide meaningful attributes for each entity to add context.""")
examples = [
lx.data.ExampleData(
text=(
"ROMEO. But soft! What light through yonder window breaks? It is"
" the east, and Juliet is the sun."
),
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"},
),
lx.data.Extraction(
extraction_class="relationship",
extraction_text="Juliet is the sun",
attributes={"type": "metaphor"},
),
],
)
]
input_text = (
"Lady Juliet gazed longingly at the stars, her heart aching for Romeo"
)
print(f" Input: {input_text}")
print(f" Model: {model_id}")
print("\n Extracting...")
result = lx.extract(
text_or_documents=input_text,
prompt_description=prompt,
examples=examples,
model_id=model_id,
model_url=model_url,
resolver_params={"format_handler": ollama.OLLAMA_FORMAT_HANDLER},
show_progress=True,
)
print("\n Results:")
print_results_summary(result.extractions)
return result
def example_medication_ner(
model_id: str, model_url: str
) -> lx.data.AnnotatedDocument | None:
"""Medical named entity recognition example."""
print_section("Example 2: Medication Named Entity Recognition")
input_text = "Patient took 400 mg PO Ibuprofen q4h for two days."
prompt_description = (
"Extract medication information including medication name, dosage, route,"
" frequency, and duration in the order they appear in the text."
)
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"
),
lx.data.Extraction(
extraction_class="duration", extraction_text="for one week"
),
],
)
]
print(f" Input: {input_text}")
print(f" Model: {model_id}")
print("\n Extracting...")
result = lx.extract(
text_or_documents=input_text,
prompt_description=prompt_description,
examples=examples,
model_id=model_id,
model_url=model_url,
resolver_params={"format_handler": ollama.OLLAMA_FORMAT_HANDLER},
show_progress=True,
)
print("\n Results:")
print_results_summary(result.extractions)
return result
def example_medication_relationships(
model_id: str, model_url: str
) -> lx.data.AnnotatedDocument | None:
"""Medication relationship extraction with grouped attributes."""
print_section("Example 3: Medication Relationship Extraction")
input_text = textwrap.dedent("""
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.
""").strip()
prompt_description = textwrap.dedent("""
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
""").strip()
examples = [
lx.data.ExampleData(
text=(
"Patient takes Aspirin 100mg daily for heart health and"
" Simvastatin 20mg at bedtime."
),
extractions=[
lx.data.Extraction(
extraction_class="medication",
extraction_text="Aspirin",
attributes={"medication_group": "Aspirin"},
),
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"},
),
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"},
),
],
)
]
print(f" Input: {input_text[:80]}...")
print(f" Model: {model_id}")
print("\n Extracting...")
result = lx.extract(
text_or_documents=input_text,
prompt_description=prompt_description,
examples=examples,
model_id=model_id,
model_url=model_url,
resolver_params={"format_handler": ollama.OLLAMA_FORMAT_HANDLER},
show_progress=True,
)
print("\n Results:")
print_results_summary(result.extractions)
medication_groups = {}
for ext in result.extractions:
if ext.attributes and "medication_group" in ext.attributes:
group_name = ext.attributes["medication_group"]
medication_groups.setdefault(group_name, []).append(ext)
if medication_groups:
print("\n Grouped by medication:")
for med_name in sorted(medication_groups.keys()):
print(f" {med_name}: {len(medication_groups[med_name])} attributes")
return result
def example_shakespeare_dialogue(
model_id: str, model_url: str
) -> lx.data.AnnotatedDocument | None:
"""Extract character dialogue from Shakespeare play excerpt."""
print_section("Example 4: Shakespeare Dialogue Extraction")
long_text = textwrap.dedent("""
Act I, Scene I. Verona. A public place.
Enter SAMPSON and GREGORY, armed with swords and bucklers.
SAMPSON: Gregory, on my word, we'll not carry coals.
GREGORY: No, for then we should be colliers.
SAMPSON: I mean, an we be in choler, we'll draw.
GREGORY: Ay, while you live, draw your neck out of collar.
Enter ABRAHAM and BALTHASAR.
ABRAHAM: Do you bite your thumb at us, sir?
SAMPSON: I do bite my thumb, sir.
ABRAHAM: Do you bite your thumb at us, sir?
SAMPSON: No, sir, I do not bite my thumb at you, sir, but I bite my thumb, sir.
GREGORY: Do you quarrel, sir?
ABRAHAM: Quarrel, sir? No, sir.
Enter BENVOLIO.
BENVOLIO: Part, fools! Put up your swords. You know not what you do.
Enter TYBALT.
TYBALT: What, art thou drawn among these heartless hinds?
Turn thee, Benvolio; look upon thy death.
BENVOLIO: I do but keep the peace. Put up thy sword,
Or manage it to part these men with me.
TYBALT: What, drawn, and talk of peace? I hate the word,
As I hate hell, all Montagues, and thee.
Have at thee, coward!
""").strip()
prompt = (
"Extract all character names and their dialogue in order of appearance."
)
examples = [
lx.data.ExampleData(
text="JULIET: O Romeo, Romeo! Wherefore art thou Romeo?",
extractions=[
lx.data.Extraction(
extraction_class="character", extraction_text="JULIET"
),
lx.data.Extraction(
extraction_class="dialogue",
extraction_text="O Romeo, Romeo! Wherefore art thou Romeo?",
attributes={"speaker": "JULIET"},
),
],
)
]
print(f" Input: Romeo and Juliet Act I, Scene I ({len(long_text)} chars)")
print(f" Model: {model_id}")
print(" Note: Automatically chunked for longer text processing")
print("\n Extracting...")
result = lx.extract(
text_or_documents=long_text,
prompt_description=prompt,
examples=examples,
model_id=model_id,
model_url=model_url,
resolver_params={"format_handler": ollama.OLLAMA_FORMAT_HANDLER},
max_char_buffer=500,
show_progress=True,
)
print("\n Results:")
print_results_summary(result.extractions)
characters = set(
ext.extraction_text
for ext in result.extractions
if ext.extraction_class == "character"
)
if characters:
print("\n Characters found: " + ", ".join(sorted(characters)))
return result
def save_results(
results: list[tuple[str, lx.data.AnnotatedDocument | None]],
output_dir: Path,
) -> None:
"""Save all results to JSONL and generate HTML visualizations."""
print_header("Saving Results and Generating Visualizations")
saved_files = []
for name, result in results:
if result is None:
print(f" ✗ Skipping {name} (no result)")
continue
jsonl_file = f"{name}.jsonl"
jsonl_path = output_dir / jsonl_file
lx.io.save_annotated_documents(
[result], output_name=jsonl_file, output_dir=str(output_dir)
)
print(f" ✓ Saved {jsonl_path}")
html_file = f"{name}.html"
html_path = output_dir / html_file
try:
html_content = lx.visualize(str(jsonl_path))
with open(html_path, "w") as f:
if hasattr(html_content, "data"):
f.write(html_content.data)
else:
f.write(html_content)
print(f" ✓ Generated {html_path}")
saved_files.append((jsonl_path, html_path))
except Exception as e:
print(f" ✗ Failed to generate {html_path}: {e}")
return saved_files
def main():
"""Run all examples and generate outputs."""
parser = argparse.ArgumentParser(
description="Ollama + FormatHandler Demo",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument(
"--model",
default=DEFAULT_MODEL,
help=f"Ollama model to use (default: {DEFAULT_MODEL})",
)
parser.add_argument(
"--url",
default=DEFAULT_OLLAMA_URL,
help=f"Ollama server URL (default: {DEFAULT_OLLAMA_URL})",
)
parser.add_argument(
"--skip-examples",
nargs="+",
choices=["1", "2", "3", "4"],
help="Skip specific examples (e.g., --skip-examples 3 4)",
)
args = parser.parse_args()
skip_examples = set(args.skip_examples or [])
print_header("Ollama + FormatHandler Demo")
print("\nConfiguration:")
print(f" Model: {args.model}")
print(f" Server: {args.url}")
print(f" Output: {OUTPUT_DIR}/")
print(f" Format Handler: {ollama.OLLAMA_FORMAT_HANDLER}")
print("\nChecking Ollama server...")
if not check_ollama_available(args.url):
print(f"\n⚠️ ERROR: Ollama not available at {args.url}")
print("\nTroubleshooting:")
print(" 1. Install Ollama: https://ollama.com/")
print(" 2. Start server: ollama serve")
print(f" 3. Pull model: ollama pull {args.model}")
print("\nFor Docker setup, see examples/ollama/docker-compose.yml")
sys.exit(1)
print("✓ Ollama server is available")
output_dir = ensure_output_directory()
print("✓ Output directory ready: " + str(output_dir) + "/")
print_header("Running Examples")
results = []
try:
if "1" not in skip_examples:
result = example_romeo_juliet(args.model, args.url)
results.append(("romeo_juliet", result))
time.sleep(0.5)
if "2" not in skip_examples:
result = example_medication_ner(args.model, args.url)
results.append(("medication_ner", result))
time.sleep(0.5)
if "3" not in skip_examples:
result = example_medication_relationships(args.model, args.url)
results.append(("medication_relationships", result))
time.sleep(0.5)
if "4" not in skip_examples:
result = example_shakespeare_dialogue(args.model, args.url)
results.append(("shakespeare_dialogue", result))
except KeyboardInterrupt:
print("\n\n⚠️ Interrupted by user")
print("Saving completed results...")
except Exception as e:
print(f"\n\n✗ Error during execution: {e}")
traceback.print_exc()
print("\nSaving completed results...")
if results:
save_results(results, output_dir)
print_header("Summary")
successful = sum(1 for _, r in results if r is not None)
print(f"\n✓ Successfully ran {successful}/{len(results)} examples")
if results:
print(f"\nOutput files in {output_dir}/:")
for name, result in results:
if result is not None:
print(f"{name}.jsonl - Extraction data")
print(f"{name}.html - Interactive visualization")
print("\nTo view results:")
print(" open " + str(output_dir) + "/romeo_juliet.html")
print("\nOr serve locally:")
print(" python -m http.server 8000 --directory " + str(output_dir))
print(" Then visit http://localhost:8000")
if __name__ == "__main__":
main()
+42
View File
@@ -0,0 +1,42 @@
# Copyright 2025 Google LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
services:
ollama:
image: ollama/ollama:0.5.4
ports:
- "127.0.0.1:11434:11434" # Bind only to localhost for security
volumes:
- ollama-data:/root/.ollama # Cross-platform support
command: serve
healthcheck:
test: ["CMD", "ollama", "list"]
interval: 5s
timeout: 3s
retries: 5
start_period: 10s
langextract:
build: .
depends_on:
ollama:
condition: service_healthy
environment:
- OLLAMA_HOST=http://ollama:11434
volumes:
- .:/app
command: python demo_ollama.py
volumes:
ollama-data: