Files
wehub-resource-sync 76d991c447
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
chore: import upstream snapshot with attribution
2026-07-13 12:37:14 +08:00

439 lines
13 KiB
Python

#!/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.
"""LangExtract benchmark suite for performance and quality testing.
Measures tokenization speed and extraction quality across multiple languages
and text types. Automatically downloads test texts from Project Gutenberg
and generates comparative visualizations.
Usage:
# Run diverse text type benchmark (default)
python benchmarks/benchmark.py
# Test with specific model
python benchmarks/benchmark.py --model gemini-2.5-flash
python benchmarks/benchmark.py --model gemma2:2b # Local model via Ollama
# Generate comparison plots from existing results
python benchmarks/benchmark.py --compare
Requirements:
- Set GEMINI_API_KEY for cloud models
- Install Ollama for local model testing
- Results saved to benchmark_results/
"""
import argparse
from datetime import datetime
import json
import os
from pathlib import Path
import time
from typing import Any
import urllib.error
import dotenv
from benchmarks import config
from benchmarks import plotting
from benchmarks import utils
import langextract
from langextract import core
from langextract import data
from langextract import visualize
import langextract.io as lio
# Load API key from environment
dotenv.load_dotenv(override=True)
GEMINI_API_KEY = os.environ.get(
"GEMINI_API_KEY", os.environ.get("LANGEXTRACT_API_KEY")
)
class BenchmarkRunner:
"""Orchestrates benchmark execution and result collection."""
def __init__(self):
"""Initialize runner with timestamp and git metadata."""
self.timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
self.git_info = utils.get_git_info()
self.tokenizer = core.tokenizer.RegexTokenizer()
def set_tokenizer(self, tokenizer_type: str):
"""Set the tokenizer to use."""
if tokenizer_type.lower() == "unicode":
self.tokenizer = core.tokenizer.UnicodeTokenizer()
print("Using UnicodeTokenizer")
else:
self.tokenizer = core.tokenizer.RegexTokenizer()
print("Using RegexTokenizer (default)")
def print_header(self):
"""Print benchmark header."""
print("=" * config.DISPLAY.separator_width)
print("LANGEXTRACT BENCHMARK")
print("=" * config.DISPLAY.separator_width)
print(
f"Branch: {self.git_info['branch']} | Commit: {self.git_info['commit']}"
)
print("-" * config.DISPLAY.separator_width)
def benchmark_tokenization(self) -> list[dict[str, Any]]:
"""Measure tokenization throughput at different text sizes.
Returns:
List of dicts with words, tokens, timing, and throughput metrics.
"""
print("\nTokenization Performance")
print("-" * config.DISPLAY.subseparator_width)
results = []
for word_count in config.TOKENIZATION.default_text_sizes:
text = " ".join(["word"] * word_count)
_ = self.tokenizer.tokenize(text)
times = []
for _ in range(config.TOKENIZATION.benchmark_iterations):
start = time.perf_counter()
tokenized = self.tokenizer.tokenize(text)
elapsed = time.perf_counter() - start
times.append(elapsed)
avg_time = sum(times) / len(times)
avg_ms = avg_time * 1000
num_tokens = len(tokenized.tokens)
tokens_per_sec = num_tokens / avg_time if avg_time > 0 else 0
word_str = (
f"{word_count//1000:,}k" if word_count >= 1000 else f"{word_count:,}"
)
print(
f"{word_str:>6} words: {avg_ms:7.2f}ms "
f"({tokens_per_sec/1e6:.1f}M tokens/sec)"
)
results.append({
"words": word_count,
"tokens": num_tokens,
"avg_ms": avg_ms,
"tokens_per_sec": tokens_per_sec,
})
return results
def test_single_extraction(
self,
model_id: str = config.MODELS.default_model,
text_type: config.TextTypes = config.TextTypes.ENGLISH,
) -> dict[str, Any]:
"""Execute extraction test.
Args:
model_id: Model identifier (e.g., 'gemini-2.5-flash', 'gemma2:2b').
text_type: Language/text type to test.
Returns:
Dict with success status, timing, entity counts, and metrics.
"""
print("\nExtraction Test")
print("-" * config.DISPLAY.subseparator_width)
try:
# Get test text
test_text = utils.get_text_from_gutenberg(text_type)
test_text = utils.get_optimal_text_size(test_text, model_id)
print(f" Text: {len(test_text):,} characters ({text_type.value})")
print(f" Model: {model_id}")
# Analyze tokenization
tokenization_analysis = utils.analyze_tokenization(
test_text, self.tokenizer
)
print(
" Tokenization:"
f" {utils.format_tokenization_summary(tokenization_analysis)}"
)
# Get extraction config for text type
extraction_config = utils.get_extraction_example(text_type)
example = data.ExampleData(
text="MACBETH speaks to LADY MACBETH about Duncan.",
extractions=[
data.Extraction(
extraction_text="Macbeth", extraction_class="Character"
),
data.Extraction(
extraction_text="Lady Macbeth", extraction_class="Character"
),
data.Extraction(
extraction_text="Duncan", extraction_class="Character"
),
],
)
max_retries = 5
retry_delay = 3.0
# Retry logic for transient network/API failures
for attempt in range(max_retries):
try:
start_time = time.time()
result = langextract.extract(
text_or_documents=test_text,
model_id=model_id,
api_key=GEMINI_API_KEY,
prompt_description=extraction_config["prompt"],
examples=[example],
max_workers=config.MODELS.default_max_workers,
temperature=config.MODELS.default_temperature,
extraction_passes=config.MODELS.default_extraction_passes,
tokenizer=self.tokenizer,
)
elapsed = time.time() - start_time
break
except (ConnectionError, TimeoutError):
if attempt < max_retries - 1:
print(f" Retrying in {retry_delay}s...")
time.sleep(retry_delay)
retry_delay *= 1.5
continue
raise
print(f"Extraction completed in {elapsed:.1f}s")
grounded_entities = []
ungrounded_entities = []
if result.extractions:
for extraction in result.extractions:
is_grounded = (
extraction.char_interval
and extraction.char_interval.start_pos is not None
and extraction.char_interval.end_pos is not None
)
entity_text = extraction.extraction_text
if entity_text:
if is_grounded:
grounded_entities.append(entity_text)
else:
ungrounded_entities.append(entity_text)
unique_grounded = list(set(grounded_entities))
unique_ungrounded = list(set(ungrounded_entities))
print(f"Found {len(unique_grounded)} grounded entities")
if unique_ungrounded:
print(f" ({len(unique_ungrounded)} ungrounded entities ignored)")
if unique_grounded:
sample = unique_grounded[:5]
sample_str = ", ".join(sample) + (
"..." if len(unique_grounded) > 5 else ""
)
print(f" Sample: {sample_str}")
return {
"success": True,
"model": model_id,
"text_type": text_type.value,
"time_seconds": elapsed,
"entity_count": len(unique_grounded),
"ungrounded_count": len(unique_ungrounded),
"sample_entities": unique_grounded[:10],
"tokenization": tokenization_analysis,
config.EXTRACTION_RESULT_KEY: result,
}
except (urllib.error.URLError, RuntimeError) as e:
# Handle expected text download failures.
print(f"Failed: {e}")
return {
"success": False,
"model": model_id,
"text_type": text_type.value,
"error": str(e),
}
def test_diverse_text_types(
self, models: list[str] | None = None
) -> list[dict[str, Any]]:
"""Test extraction with diverse text types."""
print("\n" + "=" * config.DISPLAY.separator_width)
print("DIVERSE TEXT TYPE MODE")
print("=" * config.DISPLAY.separator_width)
if models is None:
models = [config.MODELS.default_model]
results = []
test_count = 0
for model_id in models:
print(f"\nTesting {model_id}")
print("-" * 30)
for text_type in config.TextTypes:
print(f"\n Testing {text_type.value} text...")
result = self.test_single_extraction(model_id, text_type)
results.append(result)
if result.get("success"):
test_count += 1
if test_count % 3 == 0:
print(
" Rate limit delay"
f" ({config.MODELS.gemini_rate_limit_delay}s)..."
)
time.sleep(config.MODELS.gemini_rate_limit_delay)
print(f"\nCompleted {test_count} successful tests")
return results
def save_results(self, results: dict[str, Any]):
"""Save results and create plots."""
results["timestamp"] = self.timestamp
results["git"] = self.git_info
json_path = config.PATHS.get_result_path(self.timestamp, "").with_suffix(
".json"
)
viz_dir = json_path.parent / "visualizations" / self.timestamp
viz_dir.mkdir(parents=True, exist_ok=True)
if config.RESULTS_KEY in results:
print(f"\nGenerating visualizations in: {viz_dir}")
for result in results[config.RESULTS_KEY]:
if result.get("success") and config.EXTRACTION_RESULT_KEY in result:
model_name = result["model"].replace("/", "_").replace(":", "_")
text_type = result["text_type"]
viz_name = f"{model_name}_{text_type}"
jsonl_path = viz_dir / f"{viz_name}.jsonl"
lio.save_annotated_documents(
[result[config.EXTRACTION_RESULT_KEY]],
output_name=jsonl_path.name,
output_dir=str(viz_dir),
)
html_content = visualize(str(jsonl_path))
html_path = viz_dir / f"{viz_name}.html"
with open(html_path, "w") as f:
f.write(getattr(html_content, "data", html_content))
# Remove extraction result objects before saving JSON
for result in results.get(config.RESULTS_KEY, []):
result.pop(config.EXTRACTION_RESULT_KEY, None)
with open(json_path, "w") as f:
json.dump(results, f, indent=2, default=str)
print(f"\nResults saved to: {json_path}")
plot_created = plotting.create_diverse_plots(results, json_path)
if plot_created:
print(f"Plot saved to: {json_path.with_suffix('.png')}")
else:
print(f"Warning: Failed to create plot for {json_path.name}")
def run_diverse_benchmark(self, models: list[str] | None = None):
"""Run benchmark."""
self.print_header()
tokenization_results = self.benchmark_tokenization()
diverse_results = self.test_diverse_text_types(models)
results = {
"tokenization": tokenization_results,
config.RESULTS_KEY: diverse_results,
}
self.save_results(results)
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(description="LangExtract Benchmark Suite")
parser.add_argument(
"--model",
type=str,
default=None,
help=f"Model to use (default: {config.MODELS.default_model})",
)
parser.add_argument(
"--tokenizer",
type=str,
choices=["regex", "unicode"],
default="regex",
help="Tokenizer to use (default: regex)",
)
parser.add_argument(
"--compare",
action="store_true",
help="Generate comparison plots from existing benchmark results",
)
args = parser.parse_args()
# Handle comparison mode
if args.compare:
results_dir = Path("benchmark_results")
json_files = sorted(results_dir.glob("benchmark_*.json"))
if len(json_files) < 2:
print(
"Need at least 2 benchmark results for comparison, found"
f" {len(json_files)}"
)
return
print(f"Found {len(json_files)} benchmark results to compare")
# Use last 10 results or all if less than 10
files_to_compare = json_files[-10:]
comparison_path = (
results_dir
/ f"comparison_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
)
plotting.create_comparison_plots(files_to_compare, comparison_path)
print(f"\nComparison plot saved to: {comparison_path}")
return
model_to_test = args.model or config.MODELS.default_model
if "gemini" in model_to_test.lower() and not GEMINI_API_KEY:
print(
f"Error: {model_to_test} requires GEMINI_API_KEY or LANGEXTRACT_API_KEY"
)
return
runner = BenchmarkRunner()
runner.set_tokenizer(args.tokenizer)
runner.run_diverse_benchmark([args.model] if args.model else None)
if __name__ == "__main__":
main()