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,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",
|
||||
"[](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
|
||||
}
|
||||
@@ -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/
|
||||
@@ -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"]
|
||||
@@ -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.
|
||||
Executable
+561
@@ -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()
|
||||
@@ -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:
|
||||
Reference in New Issue
Block a user