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()