diff --git a/benchmarks/__init__.py b/benchmarks/__init__.py index c4ebeb257..ab2ba5d7c 100644 --- a/benchmarks/__init__.py +++ b/benchmarks/__init__.py @@ -23,16 +23,16 @@ Performance Targets: __version__ = "0.2.0" +from .scenarios.conversations import ( + generate_agentic_conversation, + generate_rag_conversation, +) from .scenarios.tool_outputs import ( generate_api_responses, generate_database_rows, generate_log_entries, generate_search_results, ) -from .scenarios.conversations import ( - generate_agentic_conversation, - generate_rag_conversation, -) __all__ = [ # Data generators diff --git a/benchmarks/adversarial_ccr_tests.py b/benchmarks/adversarial_ccr_tests.py new file mode 100644 index 000000000..eaa624741 --- /dev/null +++ b/benchmarks/adversarial_ccr_tests.py @@ -0,0 +1,1939 @@ +#!/usr/bin/env python3 +""" +Adversarial CCR Tests - Designed to BREAK Our Assumptions + +These tests are intentionally malicious, edge-casey, and designed to expose +weaknesses in our compression and retrieval logic. + +Categories: +1. SEMANTIC ATTACKS: Data that tricks our heuristics +2. BOUNDARY CONDITIONS: Edge cases at limits +3. INJECTION ATTACKS: Malformed data designed to break parsing +4. RACE CONDITIONS: Concurrency attacks +5. MEMORY PRESSURE: Resource exhaustion +6. DECEPTIVE DATA: Items that look like one thing but are another + +Run with: python benchmarks/adversarial_ccr_tests.py +""" + +from __future__ import annotations + +import concurrent.futures +import gc +import hashlib +import json +import random +import sys +import threading +import time +import uuid +from dataclasses import dataclass, field +from typing import Any + +from headroom.cache.compression_feedback import ( + get_compression_feedback, + reset_compression_feedback, +) +from headroom.cache.compression_store import ( + CompressionStore, + RetrievalEvent, + get_compression_store, + reset_compression_store, +) +from headroom.transforms.smart_crusher import ( + SmartCrusherConfig, + smart_crush_tool_output, +) + + +@dataclass +class AdversarialResult: + """Result from an adversarial test.""" + + name: str + category: str + passed: bool = False + expected_behavior: str = "" + actual_behavior: str = "" + severity: str = "medium" # low, medium, high, critical + details: dict[str, Any] = field(default_factory=dict) + + +def run_test(func) -> AdversarialResult: + """Run a test and catch any exceptions.""" + try: + return func() + except Exception as e: + return AdversarialResult( + name=func.__name__, + category="exception", + passed=False, + expected_behavior="Test should complete without exception", + actual_behavior=f"Exception: {type(e).__name__}: {str(e)[:200]}", + severity="critical", + ) + + +# ============================================================================= +# CATEGORY 1: SEMANTIC ATTACKS +# ============================================================================= + + +def test_all_items_are_errors() -> AdversarialResult: + """ + ATTACK: Every single item is an error. + + If we keep ALL errors, we keep everything = no compression. + What SHOULD happen? Keep all? Sample errors? Fail gracefully? + """ + result = AdversarialResult( + name="All Items Are Errors", + category="semantic", + expected_behavior="Should handle gracefully, possibly skip compression", + severity="high", + ) + + # 1000 items, ALL are errors + items = [ + { + "id": i, + "status": "error", + "error_code": 500 + (i % 50), + "message": f"Error at position {i}: something went wrong", + } + for i in range(1000) + ] + + config = SmartCrusherConfig(max_items_after_crush=15) + original_json = json.dumps(items) + + compressed_json, was_modified, reason = smart_crush_tool_output(original_json, config) + compressed = json.loads(compressed_json) + + # What happened? + if len(compressed) == 1000: + result.actual_behavior = "Kept ALL 1000 items (no compression when all errors)" + result.passed = True # This is actually correct behavior! + elif len(compressed) == 15: + result.actual_behavior = f"Compressed to 15 items, lost {1000 - 15} errors!" + result.passed = False + else: + result.actual_behavior = f"Compressed to {len(compressed)} items" + result.passed = len(compressed) >= 100 # Should keep most errors + + result.details = { + "original": 1000, + "compressed": len(compressed), + "reason": reason, + } + + return result + + +def test_error_keyword_in_normal_data() -> AdversarialResult: + """ + ATTACK: Normal items contain "error" keyword in benign context. + + "The error rate for this metric is 0.001%" - NOT an error! + "Error handling documentation" - NOT an error! + """ + result = AdversarialResult( + name="Error Keyword False Positive", + category="semantic", + expected_behavior="Should NOT treat benign 'error' mentions as errors", + severity="medium", + ) + + items = [] + # 100 normal items with "error" in benign context + for i in range(100): + items.append( + { + "id": i, + "status": "success", # Clearly success! + "message": random.choice( + [ + f"Error rate: 0.00{i}%", + f"Error handling improved by {i}%", + f"Zero errors detected in batch {i}", + f"Error-free operation for {i} hours", + "Documentation: How to handle errors", + ] + ), + "value": i, + } + ) + + # Add 3 REAL errors + real_error_ids = [25, 50, 75] + for idx in real_error_ids: + items[idx] = { + "id": idx, + "status": "error", # This is a REAL error + "message": f"CRITICAL: System failure at {idx}", + "error_code": 500, + } + + config = SmartCrusherConfig(max_items_after_crush=15) + original_json = json.dumps(items) + + compressed_json, was_modified, _ = smart_crush_tool_output(original_json, config) + compressed = json.loads(compressed_json) + + # Count how many items with "error" in message were kept + items_with_error_word = len( + [item for item in compressed if "error" in str(item.get("message", "")).lower()] + ) + + # Count real errors kept + real_errors_kept = len([item for item in compressed if item.get("status") == "error"]) + + # False positives (keeping non-errors) are OK - conservative is good + # False negatives (missing real errors) are NOT OK + if real_errors_kept < 3: + result.actual_behavior = ( + f"Only kept {real_errors_kept}/3 real errors - missed actual errors!" + ) + result.passed = False + else: + # Keeping extra items with "error" word is fine - better safe than sorry + result.actual_behavior = f"Kept all {real_errors_kept} real errors (+ {items_with_error_word} with 'error' word - conservative is OK)" + result.passed = True + + result.details = { + "total_compressed": len(compressed), + "real_errors_kept": real_errors_kept, + "items_with_error_word": items_with_error_word, + } + + return result + + +def test_needle_looks_exactly_like_hay() -> AdversarialResult: + """ + ATTACK: The critical item has NO distinguishing features. + + In a list of 1000 users, user #456 is the one we need. + User #456 looks EXACTLY like every other user. + """ + result = AdversarialResult( + name="Needle Identical to Hay", + category="semantic", + expected_behavior="CCR retrieval should still find specific item by ID", + severity="high", + ) + + reset_compression_store() + store = get_compression_store() + + # 1000 identical-looking users + target_id = 456 + items = [ + { + "user_id": i, + "name": f"User {i}", + "status": "active", + "created": "2025-01-01", + } + for i in range(1000) + ] + + original_json = json.dumps(items) + config = SmartCrusherConfig(max_items_after_crush=15) + + compressed_json, was_modified, _ = smart_crush_tool_output(original_json, config) + + # Store for CCR + hash_key = store.store( + original=original_json, + compressed=compressed_json, + original_item_count=1000, + compressed_item_count=15, + tool_name="user_search", + ) + + # Try to find user 456 via search + search_results = store.search(hash_key, "user_id 456") + + found_target = any(item.get("user_id") == target_id for item in search_results) + + if found_target: + result.actual_behavior = "Found target user via CCR search" + result.passed = True + else: + # Try full retrieval as fallback + entry = store.retrieve(hash_key) + if entry: + all_items = json.loads(entry.original_content) + target_in_original = any(item.get("user_id") == target_id for item in all_items) + if target_in_original: + result.actual_behavior = "Search failed, but full retrieval works" + result.passed = True # CCR still provides recovery path + else: + result.actual_behavior = "Data lost entirely!" + result.passed = False + else: + result.actual_behavior = "CCR cache miss - data not found" + result.passed = False + + result.details = { + "target_id": target_id, + "search_results": len(search_results), + "found_target": found_target, + } + + return result + + +def test_anomaly_in_string_not_number() -> AdversarialResult: + """ + ATTACK: Anomaly is in a string field, not numeric. + + 999 items: region="us-east-1" + 1 item: region="DEPRECATED-DO-NOT-USE" + + SmartCrusher detects numeric anomalies, but what about string outliers? + """ + result = AdversarialResult( + name="String Anomaly Detection", + category="semantic", + expected_behavior="Should detect or preserve string outliers", + severity="medium", + ) + + items = [] + anomaly_idx = 500 + + for i in range(1000): + if i == anomaly_idx: + items.append( + { + "id": i, + "region": "DEPRECATED-DO-NOT-USE-CRITICAL-MIGRATION-REQUIRED", + "status": "active", + } + ) + else: + items.append( + { + "id": i, + "region": "us-east-1", + "status": "active", + } + ) + + config = SmartCrusherConfig(max_items_after_crush=20) + original_json = json.dumps(items) + + compressed_json, was_modified, _ = smart_crush_tool_output(original_json, config) + compressed = json.loads(compressed_json) + + # Check if anomaly was preserved + anomaly_preserved = any("DEPRECATED" in str(item.get("region", "")) for item in compressed) + + if anomaly_preserved: + result.actual_behavior = "String anomaly was preserved" + result.passed = True + else: + result.actual_behavior = "String anomaly was LOST - only numeric anomalies detected" + result.passed = False + + result.details = { + "compressed_count": len(compressed), + "anomaly_preserved": anomaly_preserved, + } + + return result + + +# ============================================================================= +# CATEGORY 2: BOUNDARY CONDITIONS +# ============================================================================= + + +def test_empty_array() -> AdversarialResult: + """ + ATTACK: Empty array input. + """ + result = AdversarialResult( + name="Empty Array", + category="boundary", + expected_behavior="Should return empty array unchanged", + severity="low", + ) + + config = SmartCrusherConfig() + compressed_json, was_modified, reason = smart_crush_tool_output("[]", config) + + if compressed_json == "[]" and not was_modified: + result.actual_behavior = "Correctly handled empty array" + result.passed = True + else: + result.actual_behavior = f"Unexpected result: {compressed_json[:100]}" + result.passed = False + + return result + + +def test_single_item_array() -> AdversarialResult: + """ + ATTACK: Array with exactly 1 item. + """ + result = AdversarialResult( + name="Single Item Array", + category="boundary", + expected_behavior="Should return single item unchanged", + severity="low", + ) + + items = [{"id": 1, "value": "only_one"}] + config = SmartCrusherConfig() + + compressed_json, was_modified, _ = smart_crush_tool_output(json.dumps(items), config) + compressed = json.loads(compressed_json) + + if len(compressed) == 1 and compressed[0].get("id") == 1: + result.actual_behavior = "Single item preserved" + result.passed = True + else: + result.actual_behavior = f"Unexpected: {len(compressed)} items" + result.passed = False + + return result + + +def test_exactly_max_items() -> AdversarialResult: + """ + ATTACK: Array with exactly max_items_after_crush items. + """ + result = AdversarialResult( + name="Exactly Max Items", + category="boundary", + expected_behavior="Should not compress when at exact limit", + severity="low", + ) + + config = SmartCrusherConfig(max_items_after_crush=15) + items = [{"id": i} for i in range(15)] # Exactly 15 + + compressed_json, was_modified, _ = smart_crush_tool_output(json.dumps(items), config) + compressed = json.loads(compressed_json) + + if len(compressed) == 15: + result.actual_behavior = "Kept all 15 items as expected" + result.passed = True + else: + result.actual_behavior = f"Changed count: {len(compressed)}" + result.passed = False + + return result + + +def test_max_items_plus_one() -> AdversarialResult: + """ + ATTACK: Array with max_items + 1. + + IMPORTANT: If data has high uniqueness and no importance signal, + crushability analysis correctly skips compression to avoid data loss. + This is the RIGHT behavior - don't blindly compress unique entities. + """ + result = AdversarialResult( + name="Max Items Plus One", + category="boundary", + expected_behavior="Skip compression for unique entities OR compress with signal", + severity="low", + ) + + config = SmartCrusherConfig(max_items_after_crush=15, min_items_to_analyze=5) + # Create items WITH a score field so compression can determine importance + items = [{"id": i, "value": f"item_{i}", "score": 1.0 - (i / 100)} for i in range(16)] + + compressed_json, was_modified, reason = smart_crush_tool_output(json.dumps(items), config) + compressed = json.loads(compressed_json) + + result.actual_behavior = f"Compressed to {len(compressed)} items ({reason})" + # With a score signal, we should compress to max_items + result.passed = len(compressed) <= 15 + + return result + + +def test_hash_collision_attempt() -> AdversarialResult: + """ + ATTACK: Try to create hash collisions in CCR store. + + We use SHA256[:16] - what if two different contents hash the same? + """ + result = AdversarialResult( + name="Hash Collision Attack", + category="boundary", + expected_behavior="Different content should not collide", + severity="high", + ) + + reset_compression_store() + get_compression_store() + + # Store many different contents + hashes = set() + collisions = 0 + + for i in range(10000): + content = json.dumps([{"unique_id": str(uuid.uuid4()), "index": i}]) + content_hash = hashlib.sha256(content.encode()).hexdigest()[:16] + + if content_hash in hashes: + collisions += 1 + hashes.add(content_hash) + + if collisions == 0: + result.actual_behavior = "No collisions in 10,000 entries" + result.passed = True + else: + result.actual_behavior = f"Found {collisions} hash collisions!" + result.passed = False + result.severity = "critical" + + result.details = {"entries_tested": 10000, "collisions": collisions} + + return result + + +def test_ttl_exact_boundary() -> AdversarialResult: + """ + ATTACK: Retrieve at exact TTL expiration moment. + """ + result = AdversarialResult( + name="TTL Exact Boundary", + category="boundary", + expected_behavior="Entry should expire cleanly at TTL", + severity="medium", + ) + + reset_compression_store() + store = CompressionStore(default_ttl=1) # 1 second TTL + + hash_key = store.store( + original='[{"id": 1}]', + compressed='[{"id": 1}]', + original_item_count=1, + compressed_item_count=1, + ) + + # Should exist immediately + exists_before = store.exists(hash_key) + + # Wait exactly at boundary + time.sleep(1.05) + + # Should be expired + exists_after = store.exists(hash_key) + entry = store.retrieve(hash_key) + + if exists_before and not exists_after and entry is None: + result.actual_behavior = "TTL expiration works correctly" + result.passed = True + else: + result.actual_behavior = ( + f"Before: {exists_before}, After: {exists_after}, Entry: {entry is not None}" + ) + result.passed = False + + return result + + +# ============================================================================= +# CATEGORY 3: INJECTION ATTACKS +# ============================================================================= + + +def test_json_injection_in_content() -> AdversarialResult: + """ + ATTACK: JSON that tries to break our parsing. + """ + result = AdversarialResult( + name="JSON Injection", + category="injection", + expected_behavior="Should handle malformed JSON gracefully", + severity="high", + ) + + # Various injection attempts + injections = [ + '{"id": 1, "evil": "}\\"]}', # Quote escape + '[{"id": 1}, null, {"id": 2}]', # Null in array + '[{"id": 1, "__proto__": {"admin": true}}]', # Prototype pollution + '[{"id": 1, "nested": {"deep": {"deeper": {"deepest": "value"}}}}]', + ] + + config = SmartCrusherConfig() + failures = [] + + for injection in injections: + try: + compressed, was_modified, _ = smart_crush_tool_output(injection, config) + # If it returns, it handled it + except Exception as e: + failures.append(f"{injection[:30]}: {type(e).__name__}") + + if not failures: + result.actual_behavior = "All injection attempts handled gracefully" + result.passed = True + else: + result.actual_behavior = f"Failures: {failures}" + result.passed = False + + return result + + +def test_headroom_marker_collision() -> AdversarialResult: + """ + ATTACK: Input data already contains __headroom_ fields. + """ + result = AdversarialResult( + name="Marker Field Collision", + category="injection", + expected_behavior="Should not confuse existing __headroom_ fields with our markers", + severity="high", + ) + + # Data that already has __headroom_ fields + items = [ + { + "id": i, + "__headroom_compressed": True, # Fake marker! + "__headroom_hash": "fakehash12345678", + "__headroom_stats": {"fake": True}, + } + for i in range(100) + ] + + config = SmartCrusherConfig(max_items_after_crush=15) + original_json = json.dumps(items) + + compressed_json, was_modified, _ = smart_crush_tool_output(original_json, config) + compressed = json.loads(compressed_json) + + # Check if our compression worked despite fake markers + if isinstance(compressed, list) and len(compressed) <= 20: + result.actual_behavior = "Compression worked despite fake markers" + result.passed = True + else: + result.actual_behavior = f"Unexpected result type or length: {type(compressed)}, {len(compressed) if isinstance(compressed, list) else 'N/A'}" + result.passed = False + + return result + + +def test_unicode_and_emoji_handling() -> AdversarialResult: + """ + ATTACK: Unicode edge cases in content. + """ + result = AdversarialResult( + name="Unicode/Emoji Handling", + category="injection", + expected_behavior="Should handle Unicode correctly", + severity="medium", + ) + + items = [ + {"id": 1, "message": "Error: 🔥 Server on fire 🔥", "status": "error"}, + {"id": 2, "message": "成功: 操作完成", "status": "success"}, + {"id": 3, "message": "Error: \u0000\u0001\u0002 null bytes", "status": "error"}, + {"id": 4, "message": "Ошибка: критический сбой", "status": "error"}, + {"id": 5, "message": "🎉🎊🎈" * 100, "status": "success"}, # Lots of emoji + ] + + for i in range(95): + items.append({"id": i + 6, "message": "Normal", "status": "success"}) + + config = SmartCrusherConfig(max_items_after_crush=15) + original_json = json.dumps(items, ensure_ascii=False) + + try: + compressed_json, was_modified, _ = smart_crush_tool_output(original_json, config) + compressed = json.loads(compressed_json) + + # Check if error items with unicode were preserved + errors_preserved = len([item for item in compressed if item.get("status") == "error"]) + + result.actual_behavior = f"Handled Unicode, {errors_preserved} errors preserved" + result.passed = errors_preserved >= 2 + + except Exception as e: + result.actual_behavior = f"Unicode handling failed: {e}" + result.passed = False + + return result + + +def test_extremely_long_strings() -> AdversarialResult: + """ + ATTACK: Items with extremely long string values. + """ + result = AdversarialResult( + name="Extremely Long Strings", + category="injection", + expected_behavior="Should handle without memory issues", + severity="medium", + ) + + # One item with a 10MB string + huge_string = "x" * (10 * 1024 * 1024) # 10MB + + items = [ + {"id": 0, "huge": huge_string, "status": "error"}, # Should be kept (error) + *[{"id": i, "normal": "small"} for i in range(1, 100)], + ] + + config = SmartCrusherConfig(max_items_after_crush=15) + + sys.getsizeof(items) + start_time = time.time() + + try: + original_json = json.dumps(items) + compressed_json, was_modified, _ = smart_crush_tool_output(original_json, config) + + elapsed = time.time() - start_time + + if elapsed > 30: + result.actual_behavior = f"Took too long: {elapsed:.1f}s" + result.passed = False + else: + result.actual_behavior = f"Handled 10MB string in {elapsed:.1f}s" + result.passed = True + + except MemoryError: + result.actual_behavior = "MemoryError on large string" + result.passed = False + result.severity = "critical" + finally: + del huge_string + del items + gc.collect() + + return result + + +def test_query_injection_in_search() -> AdversarialResult: + """ + ATTACK: Malicious search query. + """ + result = AdversarialResult( + name="Search Query Injection", + category="injection", + expected_behavior="Should sanitize search queries", + severity="high", + ) + + reset_compression_store() + store = get_compression_store() + + items = [{"id": i, "data": f"item {i}"} for i in range(100)] + + hash_key = store.store( + original=json.dumps(items), + compressed=json.dumps(items[:10]), + original_item_count=100, + compressed_item_count=10, + ) + + # Various injection attempts + malicious_queries = [ + "'; DROP TABLE items; --", + "", + "{{7*7}}", # Template injection + "${7*7}", # Expression injection + "\\x00\\x01\\x02", # Null bytes + "*" * 10000, # Long query + ".*", # Regex wildcard + "(a]", # Invalid regex + ] + + failures = [] + for query in malicious_queries: + try: + store.search(hash_key, query) + # If it returns without error, it handled the injection + except Exception as e: + failures.append(f"{query[:20]}: {type(e).__name__}") + + if not failures: + result.actual_behavior = "All malicious queries handled safely" + result.passed = True + else: + result.actual_behavior = f"Failures: {failures}" + result.passed = False + + return result + + +# ============================================================================= +# CATEGORY 4: RACE CONDITIONS +# ============================================================================= + + +def test_concurrent_store_same_content() -> AdversarialResult: + """ + ATTACK: Multiple threads storing identical content simultaneously. + """ + result = AdversarialResult( + name="Concurrent Store Same Content", + category="race", + expected_behavior="Should handle concurrent stores without data corruption", + severity="high", + ) + + reset_compression_store() + store = get_compression_store() + + content = json.dumps([{"id": i} for i in range(100)]) + + results = [] + errors = [] + + def store_content(): + try: + hash_key = store.store( + original=content, + compressed=content[:50], + original_item_count=100, + compressed_item_count=5, + ) + results.append(hash_key) + except Exception as e: + errors.append(str(e)) + + # 100 concurrent stores of same content + with concurrent.futures.ThreadPoolExecutor(max_workers=50) as executor: + futures = [executor.submit(store_content) for _ in range(100)] + concurrent.futures.wait(futures) + + if errors: + result.actual_behavior = f"Errors during concurrent store: {errors[:3]}" + result.passed = False + elif len(set(results)) != 1: + result.actual_behavior = f"Got different hashes for same content: {set(results)}" + result.passed = False + else: + result.actual_behavior = "All concurrent stores returned same hash" + result.passed = True + + return result + + +def test_concurrent_store_and_evict() -> AdversarialResult: + """ + ATTACK: Store while eviction is happening. + """ + result = AdversarialResult( + name="Concurrent Store and Evict", + category="race", + expected_behavior="Eviction should not corrupt concurrent stores", + severity="high", + ) + + reset_compression_store() + store = CompressionStore(max_entries=10) # Small capacity + + errors = [] + stored_hashes = [] + + def rapid_store(thread_id): + for i in range(50): + try: + content = json.dumps([{"thread": thread_id, "iteration": i}]) + hash_key = store.store( + original=content, + compressed=content, + original_item_count=1, + compressed_item_count=1, + ) + stored_hashes.append(hash_key) + except Exception as e: + errors.append(f"Thread {thread_id}, iter {i}: {e}") + + # 10 threads, each storing 50 items = 500 stores with max_entries=10 + with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor: + futures = [executor.submit(rapid_store, i) for i in range(10)] + concurrent.futures.wait(futures) + + if errors: + result.actual_behavior = f"Errors: {errors[:5]}" + result.passed = False + else: + result.actual_behavior = "500 stores with capacity 10 succeeded" + result.passed = True + + return result + + +def test_concurrent_feedback_updates() -> AdversarialResult: + """ + ATTACK: Multiple threads updating feedback simultaneously. + """ + result = AdversarialResult( + name="Concurrent Feedback Updates", + category="race", + expected_behavior="Feedback counts should be accurate under concurrency", + severity="high", + ) + + reset_compression_feedback() + feedback = get_compression_feedback() + + tool_name = "concurrent_test_tool" + expected_compressions = 1000 + expected_retrievals = 500 + + def record_compressions(): + for _ in range(expected_compressions // 10): + feedback.record_compression(tool_name, 100, 10) + + def record_retrievals(): + # 5 threads × 100 iterations = 500 retrievals + for i in range(expected_retrievals // 5): + event = RetrievalEvent( + hash=f"hash{i:012d}", + query=None, + items_retrieved=100, + total_items=100, + tool_name=tool_name, + timestamp=time.time(), + retrieval_type="full", + ) + feedback.record_retrieval(event) + + # 10 threads each doing compressions (1000/10=100 each), 5 doing retrievals (500/5=100 each) + with concurrent.futures.ThreadPoolExecutor(max_workers=15) as executor: + futures = [] + for _ in range(10): + futures.append(executor.submit(record_compressions)) + for _ in range(5): + futures.append(executor.submit(record_retrievals)) + concurrent.futures.wait(futures) + + patterns = feedback.get_all_patterns() + pattern = patterns.get(tool_name) + + if pattern is None: + result.actual_behavior = "Pattern not found" + result.passed = False + elif ( + pattern.total_compressions == expected_compressions + and pattern.total_retrievals == expected_retrievals + ): + result.actual_behavior = f"Exact counts: {pattern.total_compressions} compressions, {pattern.total_retrievals} retrievals" + result.passed = True + else: + result.actual_behavior = f"Count mismatch: {pattern.total_compressions} compressions (expected {expected_compressions}), {pattern.total_retrievals} retrievals (expected {expected_retrievals})" + result.passed = False + + result.details = { + "expected_compressions": expected_compressions, + "actual_compressions": pattern.total_compressions if pattern else 0, + "expected_retrievals": expected_retrievals, + "actual_retrievals": pattern.total_retrievals if pattern else 0, + } + + return result + + +# ============================================================================= +# CATEGORY 5: DECEPTIVE DATA +# ============================================================================= + + +def test_hidden_error_in_nested_structure() -> AdversarialResult: + """ + ATTACK: Error hidden deep in nested structure. + """ + result = AdversarialResult( + name="Hidden Error in Nested Structure", + category="deceptive", + expected_behavior="Should detect errors in nested objects", + severity="high", + ) + + items = [] + error_idx = 50 + + for i in range(100): + if i == error_idx: + # Error hidden deep inside + items.append( + { + "id": i, + "status": "success", # Top level says success! + "details": { + "level1": { + "level2": { + "actual_status": "CRITICAL_ERROR", + "error": True, + "message": "System failure", + } + } + }, + } + ) + else: + items.append( + { + "id": i, + "status": "success", + "details": {"level1": {"level2": {"actual_status": "ok"}}}, + } + ) + + config = SmartCrusherConfig(max_items_after_crush=15) + original_json = json.dumps(items) + + compressed_json, was_modified, _ = smart_crush_tool_output(original_json, config) + compressed = json.loads(compressed_json) + + # Check if the nested error was preserved + nested_error_found = any("CRITICAL_ERROR" in json.dumps(item) for item in compressed) + + if nested_error_found: + result.actual_behavior = "Nested error was detected and preserved" + result.passed = True + else: + result.actual_behavior = "Nested error was LOST - only top-level status checked" + result.passed = False + + return result + + +def test_misleading_score_field() -> AdversarialResult: + """ + ATTACK: Score field that doesn't indicate importance. + + Items with score=0.99 are spam, items with score=0.01 are critical. + """ + result = AdversarialResult( + name="Misleading Score Field", + category="deceptive", + expected_behavior="Should not blindly trust high scores", + severity="medium", + ) + + items = [] + critical_indices = [25, 50, 75] + + for i in range(100): + if i in critical_indices: + # LOW score but CRITICAL + items.append( + { + "id": i, + "score": 0.01, # Low score + "type": "critical_alert", + "message": "URGENT: Action required", + } + ) + else: + # HIGH score but SPAM + items.append( + { + "id": i, + "score": 0.99, # High score + "type": "spam", + "message": "Buy now! Limited offer!", + } + ) + + config = SmartCrusherConfig(max_items_after_crush=15) + original_json = json.dumps(items) + + compressed_json, was_modified, _ = smart_crush_tool_output(original_json, config) + compressed = json.loads(compressed_json) + + # Check what was kept + critical_kept = len([item for item in compressed if item.get("type") == "critical_alert"]) + spam_kept = len([item for item in compressed if item.get("type") == "spam"]) + + # We should preserve ALL critical items due to "critical" keyword detection + # The remaining slots can go to high-score items - that's acceptable + # The key guarantee: we NEVER lose items matching important keywords + if critical_kept < 3: + result.actual_behavior = f"Lost critical items! Only kept {critical_kept}/3 critical" + result.passed = False + else: + result.actual_behavior = f"Kept all {critical_kept} critical items (plus {spam_kept} spam) - keyword detection worked" + result.passed = True + + result.details = { + "critical_kept": critical_kept, + "spam_kept": spam_kept, + } + + return result + + +def test_timestamp_anomaly_not_value() -> AdversarialResult: + """ + ATTACK: Anomaly in timestamp, not in measured value. + + One entry is from the FUTURE - this is the anomaly! + """ + result = AdversarialResult( + name="Timestamp Anomaly", + category="deceptive", + expected_behavior="Should detect timestamp anomalies", + severity="medium", + ) + + items = [] + anomaly_idx = 50 + + for i in range(100): + if i == anomaly_idx: + # Future timestamp - something is wrong! + items.append( + { + "timestamp": "2030-01-01T00:00:00Z", # FUTURE! + "value": 50, # Normal value + "id": i, + } + ) + else: + items.append( + { + "timestamp": f"2025-01-{(i % 28) + 1:02d}T{(i % 24):02d}:00:00Z", + "value": 50 + (i % 10), # Normal variation + "id": i, + } + ) + + config = SmartCrusherConfig(max_items_after_crush=15) + original_json = json.dumps(items) + + compressed_json, was_modified, _ = smart_crush_tool_output(original_json, config) + compressed = json.loads(compressed_json) + + # Check if future timestamp was preserved + future_found = any("2030" in str(item.get("timestamp", "")) for item in compressed) + + if future_found: + result.actual_behavior = "Future timestamp anomaly preserved" + result.passed = True + else: + result.actual_behavior = "Timestamp anomaly LOST - only value anomalies detected" + result.passed = False + + return result + + +# ============================================================================= +# EXTREME STRESS TESTS - Designed to Break Assumptions +# ============================================================================= + + +def test_deeply_nested_structure() -> AdversarialResult: + """ + ATTACK: Extremely deep nesting to cause stack overflow. + + 100 levels of nested objects containing arrays. + """ + result = AdversarialResult( + name="Deep Nesting Attack", + category="extreme", + expected_behavior="Should handle deep nesting without stack overflow", + severity="critical", + ) + + # Build deeply nested structure + depth = 100 + inner = [{"id": i, "value": f"leaf_{i}"} for i in range(20)] + + current = inner + for level in range(depth): + current = {"level": level, "data": current} + + try: + config = SmartCrusherConfig(max_items_after_crush=10) + original_json = json.dumps(current) + + compressed_json, was_modified, reason = smart_crush_tool_output(original_json, config) + result.actual_behavior = f"Handled {depth} levels of nesting" + result.passed = True + except RecursionError as e: + result.actual_behavior = f"Stack overflow at depth {depth}: {e}" + result.passed = False + except Exception as e: + result.actual_behavior = f"Unexpected error: {type(e).__name__}: {e}" + result.passed = False + + return result + + +def test_nan_infinity_scores() -> AdversarialResult: + """ + ATTACK: Score fields with NaN, Infinity, -Infinity. + + These are valid JSON when serialized from Python but break comparisons. + """ + result = AdversarialResult( + name="NaN/Infinity Scores", + category="extreme", + expected_behavior="Should handle special float values gracefully", + severity="high", + ) + + items = [] + for i in range(50): + score = i / 10.0 + if i == 10: + score = float("nan") + elif i == 20: + score = float("inf") + elif i == 30: + score = float("-inf") + + items.append({"id": i, "score": score, "name": f"item_{i}"}) + + try: + config = SmartCrusherConfig(max_items_after_crush=15) + # Note: json.dumps will fail on NaN/Inf by default, use allow_nan + original_json = json.dumps(items, allow_nan=True) + + compressed_json, was_modified, reason = smart_crush_tool_output(original_json, config) + compressed = json.loads(compressed_json, parse_constant=lambda x: None) + + result.actual_behavior = f"Handled special floats, compressed to {len(compressed)} items" + result.passed = True + except (ValueError, TypeError) as e: + result.actual_behavior = f"Failed on special floats: {e}" + result.passed = False + except Exception as e: + result.actual_behavior = f"Unexpected error: {type(e).__name__}: {e}" + result.passed = False + + return result + + +def test_mixed_type_array() -> AdversarialResult: + """ + ATTACK: Array with mixed types (dicts, strings, numbers, nulls). + + SmartCrusher expects arrays of dicts - what happens with mixed? + """ + result = AdversarialResult( + name="Mixed Type Array", + category="extreme", + expected_behavior="Should handle or gracefully skip mixed arrays", + severity="medium", + ) + + mixed_array = [ + {"id": 1, "type": "dict"}, + "just a string", + 42, + None, + {"id": 2, "type": "dict"}, + ["nested", "array"], + True, + {"id": 3, "type": "dict"}, + ] + + try: + config = SmartCrusherConfig(max_items_after_crush=5) + original_json = json.dumps(mixed_array) + + compressed_json, was_modified, reason = smart_crush_tool_output(original_json, config) + + result.actual_behavior = f"Handled mixed array: modified={was_modified}, reason={reason}" + result.passed = True + except Exception as e: + result.actual_behavior = f"Crashed on mixed array: {type(e).__name__}: {e}" + result.passed = False + + return result + + +def test_catastrophic_regex_in_search() -> AdversarialResult: + """ + ATTACK: Search query designed to cause catastrophic backtracking. + + Pattern like (a+)+ on "aaaaaaaaaaaaaaaaaaaaaaaaaaab" can hang regex engines. + """ + result = AdversarialResult( + name="Regex Catastrophic Backtracking", + category="extreme", + expected_behavior="Should not hang on malicious search patterns", + severity="critical", + ) + + reset_compression_store() + store = get_compression_store() + + items = [{"id": i, "content": "a" * 50 + "b"} for i in range(100)] + + hash_key = store.store( + original=json.dumps(items), + compressed=json.dumps(items[:10]), + original_item_count=100, + compressed_item_count=10, + tool_name="regex_test", + ) + + # These patterns could cause catastrophic backtracking in naive regex + evil_patterns = [ + "(a+)+$", + "(a|aa)+$", + "(a+)+b", + "([a-zA-Z]+)*X", + ] + + try: + import signal + + def timeout_handler(signum, frame): + raise TimeoutError("Search took too long") + + # Set 2 second timeout + old_handler = signal.signal(signal.SIGALRM, timeout_handler) + signal.alarm(2) + + for pattern in evil_patterns: + # BM25 search doesn't use regex, so should be safe + store.search(hash_key, pattern) + + signal.alarm(0) + signal.signal(signal.SIGALRM, old_handler) + + result.actual_behavior = "Search completed without hanging" + result.passed = True + except TimeoutError: + result.actual_behavior = "Search hung on regex-like pattern" + result.passed = False + except Exception as e: + result.actual_behavior = f"Error: {type(e).__name__}: {e}" + result.passed = True # Failing safely is OK + + return result + + +def test_million_items() -> AdversarialResult: + """ + ATTACK: Array with 1 million items. + + Test memory and performance at scale. + """ + result = AdversarialResult( + name="Million Items Scale", + category="extreme", + expected_behavior="Should handle large arrays without OOM", + severity="high", + ) + + try: + # Create 100K items (not 1M to keep test reasonable) + item_count = 100_000 + items = [{"id": i, "value": i % 1000} for i in range(item_count)] + + config = SmartCrusherConfig(max_items_after_crush=15) + + start = time.time() + original_json = json.dumps(items) + compressed_json, was_modified, reason = smart_crush_tool_output(original_json, config) + elapsed = time.time() - start + + compressed = json.loads(compressed_json) + + result.actual_behavior = ( + f"Compressed {item_count} items to {len(compressed)} in {elapsed:.2f}s" + ) + result.passed = elapsed < 10.0 # Should complete in under 10 seconds + result.details = {"item_count": item_count, "elapsed_seconds": elapsed} + except MemoryError: + result.actual_behavior = "Out of memory" + result.passed = False + except Exception as e: + result.actual_behavior = f"Error: {type(e).__name__}: {e}" + result.passed = False + + return result + + +def test_item_with_thousands_of_fields() -> AdversarialResult: + """ + ATTACK: Items with 10,000 fields each. + + Field analysis iterates over all fields - what's the cost? + """ + result = AdversarialResult( + name="Thousands of Fields", + category="extreme", + expected_behavior="Should handle items with many fields", + severity="medium", + ) + + try: + field_count = 5000 + items = [] + for i in range(20): + item = {"id": i} + for f in range(field_count): + item[f"field_{f}"] = f"value_{f}_{i}" + items.append(item) + + config = SmartCrusherConfig(max_items_after_crush=10) + + start = time.time() + original_json = json.dumps(items) + compressed_json, was_modified, reason = smart_crush_tool_output(original_json, config) + elapsed = time.time() - start + + result.actual_behavior = f"Handled {field_count} fields/item in {elapsed:.2f}s" + result.passed = elapsed < 5.0 + except Exception as e: + result.actual_behavior = f"Error: {type(e).__name__}: {e}" + result.passed = False + + return result + + +def test_identical_items() -> AdversarialResult: + """ + ATTACK: All items are EXACTLY identical. + + Uniqueness detection should handle this edge case. + """ + result = AdversarialResult( + name="All Identical Items", + category="extreme", + expected_behavior="Should handle identical items efficiently", + severity="low", + ) + + # 1000 perfectly identical items + template = {"id": 1, "status": "ok", "value": 42, "message": "All good"} + items = [template.copy() for _ in range(1000)] + + try: + config = SmartCrusherConfig(max_items_after_crush=15) + + compressed_json, was_modified, reason = smart_crush_tool_output(json.dumps(items), config) + compressed = json.loads(compressed_json) + + result.actual_behavior = ( + f"Compressed {len(items)} identical items to {len(compressed)}: {reason}" + ) + # Should heavily compress since all items are the same + result.passed = len(compressed) <= 15 + except Exception as e: + result.actual_behavior = f"Error: {type(e).__name__}: {e}" + result.passed = False + + return result + + +def test_all_fields_none() -> AdversarialResult: + """ + ATTACK: Items where every field value is null/None. + """ + result = AdversarialResult( + name="All Null Values", + category="extreme", + expected_behavior="Should handle all-null items", + severity="low", + ) + + items = [{"id": None, "value": None, "status": None, "data": None} for _ in range(100)] + + try: + config = SmartCrusherConfig(max_items_after_crush=10) + + compressed_json, was_modified, reason = smart_crush_tool_output(json.dumps(items), config) + + result.actual_behavior = f"Handled all-null items: modified={was_modified}" + result.passed = True + except Exception as e: + result.actual_behavior = f"Error: {type(e).__name__}: {e}" + result.passed = False + + return result + + +def test_unicode_normalization_attack() -> AdversarialResult: + """ + ATTACK: Unicode strings that look identical but are different. + + "café" can be encoded as: + - c a f é (4 chars, é is U+00E9) + - c a f e ́ (5 chars, e + combining acute U+0301) + + These look identical but are different strings! + """ + result = AdversarialResult( + name="Unicode Normalization Attack", + category="extreme", + expected_behavior="Should handle unicode edge cases", + severity="medium", + ) + + # Two visually identical but byte-different strings + composed = "café" # é as single char + decomposed = "cafe\u0301" # e + combining accent + + items = [] + for i in range(50): + if i % 2 == 0: + items.append({"id": i, "name": composed, "type": "composed"}) + else: + items.append({"id": i, "name": decomposed, "type": "decomposed"}) + + # Add one special item + items[25] = {"id": 25, "name": composed, "type": "TARGET", "status": "error"} + + try: + config = SmartCrusherConfig(max_items_after_crush=15) + + compressed_json, was_modified, reason = smart_crush_tool_output(json.dumps(items), config) + compressed = json.loads(compressed_json) + + # Check if we kept the TARGET item + target_found = any(item.get("type") == "TARGET" for item in compressed) + + result.actual_behavior = f"Unicode handled, target found: {target_found}" + result.passed = target_found + except Exception as e: + result.actual_behavior = f"Error: {type(e).__name__}: {e}" + result.passed = False + + return result + + +def test_concurrent_reset_during_operation() -> AdversarialResult: + """ + ATTACK: Reset global state while operations are in progress. + """ + result = AdversarialResult( + name="Concurrent Reset Attack", + category="extreme", + expected_behavior="Should not crash on concurrent reset", + severity="high", + ) + + errors = [] + operations_completed = [0] + + def do_operations(): + for i in range(100): + try: + store = get_compression_store() + items = [{"id": j, "iter": i} for j in range(20)] + hash_key = store.store( + original=json.dumps(items), + compressed=json.dumps(items[:5]), + original_item_count=20, + compressed_item_count=5, + tool_name="reset_test", + ) + store.retrieve(hash_key) + store.search(hash_key, "test") + operations_completed[0] += 1 + except Exception as e: + errors.append(f"Op error: {type(e).__name__}: {e}") + + def do_resets(): + for _ in range(50): + try: + reset_compression_store() + reset_compression_feedback() + time.sleep(0.001) + except Exception as e: + errors.append(f"Reset error: {type(e).__name__}: {e}") + + try: + with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor: + futures = [] + for _ in range(5): + futures.append(executor.submit(do_operations)) + for _ in range(3): + futures.append(executor.submit(do_resets)) + + concurrent.futures.wait(futures) + + if errors: + result.actual_behavior = f"Errors during concurrent reset: {errors[:3]}" + result.passed = False + else: + result.actual_behavior = ( + f"Completed {operations_completed[0]} operations with concurrent resets" + ) + result.passed = True + except Exception as e: + result.actual_behavior = f"Crashed: {type(e).__name__}: {e}" + result.passed = False + + return result + + +def test_zero_byte_in_content() -> AdversarialResult: + """ + ATTACK: Null bytes (\\x00) embedded in strings. + + Can truncate strings in C-based systems. + """ + result = AdversarialResult( + name="Null Byte Injection", + category="extreme", + expected_behavior="Should preserve content with null bytes", + severity="high", + ) + + items = [] + for i in range(50): + # Embed null byte in various positions + if i == 10: + items.append({"id": i, "data": "before\x00after", "status": "error"}) + elif i == 20: + items.append({"id": i, "data": "\x00start", "status": "error"}) + elif i == 30: + items.append({"id": i, "data": "end\x00", "status": "error"}) + else: + items.append({"id": i, "data": "normal", "status": "ok"}) + + try: + config = SmartCrusherConfig(max_items_after_crush=15) + original_json = json.dumps(items) + + compressed_json, was_modified, _ = smart_crush_tool_output(original_json, config) + compressed = json.loads(compressed_json) + + # Check if null-byte items were preserved (they have status=error) + error_items = [item for item in compressed if item.get("status") == "error"] + + # Also verify the null bytes survived + null_byte_survived = any("\x00" in str(item.get("data", "")) for item in compressed) + + result.actual_behavior = ( + f"Kept {len(error_items)} error items, null bytes intact: {null_byte_survived}" + ) + result.passed = len(error_items) == 3 and null_byte_survived + except Exception as e: + result.actual_behavior = f"Error: {type(e).__name__}: {e}" + result.passed = False + + return result + + +def test_recursive_json_structure() -> AdversarialResult: + """ + ATTACK: Structure that references itself (via string representation). + + Not true circular reference (JSON doesn't support that), but deeply self-similar. + """ + result = AdversarialResult( + name="Self-Similar Structure", + category="extreme", + expected_behavior="Should handle self-similar data", + severity="low", + ) + + # Create structure where values contain JSON-like strings + items = [] + for i in range(50): + inner = json.dumps({"nested_id": i, "value": "inner"}) + items.append( + { + "id": i, + "data": inner, # JSON string inside JSON + "meta": json.dumps({"level": 1, "payload": inner}), # Double nested + } + ) + + try: + config = SmartCrusherConfig(max_items_after_crush=15) + + compressed_json, was_modified, reason = smart_crush_tool_output(json.dumps(items), config) + compressed = json.loads(compressed_json) + + result.actual_behavior = f"Handled self-similar structure: {len(compressed)} items" + result.passed = True + except Exception as e: + result.actual_behavior = f"Error: {type(e).__name__}: {e}" + result.passed = False + + return result + + +def test_extreme_numeric_values() -> AdversarialResult: + """ + ATTACK: Extreme numeric values that might overflow. + + Very large integers, very small floats, edge cases. + """ + result = AdversarialResult( + name="Extreme Numeric Values", + category="extreme", + expected_behavior="Should handle extreme numbers", + severity="medium", + ) + + items = [ + {"id": 0, "value": 0}, + {"id": 1, "value": -1}, + {"id": 2, "value": 2**63 - 1}, # Max int64 + {"id": 3, "value": -(2**63)}, # Min int64 + {"id": 4, "value": 2**64}, # Overflow int64 + {"id": 5, "value": 10**308}, # Near max float + {"id": 6, "value": 10**-308}, # Near min positive float + {"id": 7, "value": 0.1 + 0.2}, # Classic float precision issue + {"id": 8, "value": 1e-400}, # Underflow to 0 + {"id": 9, "score": 999999999999999999999}, # Very large score + ] + + # Add normal items + for i in range(10, 50): + items.append({"id": i, "value": i, "score": i / 100}) + + try: + config = SmartCrusherConfig(max_items_after_crush=15) + + compressed_json, was_modified, reason = smart_crush_tool_output(json.dumps(items), config) + compressed = json.loads(compressed_json) + + result.actual_behavior = f"Handled extreme numbers: {len(compressed)} items" + result.passed = True + except (OverflowError, ValueError) as e: + result.actual_behavior = f"Numeric error: {e}" + result.passed = False + except Exception as e: + result.actual_behavior = f"Error: {type(e).__name__}: {e}" + result.passed = False + + return result + + +def test_adversarial_field_names() -> AdversarialResult: + """ + ATTACK: Field names that might confuse our analysis. + + Fields named "__proto__", "constructor", "toString", etc. + """ + result = AdversarialResult( + name="Adversarial Field Names", + category="extreme", + expected_behavior="Should handle special field names", + severity="medium", + ) + + items = [] + for i in range(30): + items.append( + { + "id": i, + "__proto__": {"admin": True}, # Prototype pollution attempt + "constructor": "evil", + "toString": "hacked", + "__class__": "injected", + "hasOwnProperty": False, + "score": i / 10, + "status": "error" if i == 15 else "ok", + } + ) + + try: + config = SmartCrusherConfig(max_items_after_crush=10) + + compressed_json, was_modified, reason = smart_crush_tool_output(json.dumps(items), config) + compressed = json.loads(compressed_json) + + # Verify error item was kept + error_kept = any(item.get("status") == "error" for item in compressed) + + result.actual_behavior = f"Handled adversarial fields, error kept: {error_kept}" + result.passed = error_kept + except Exception as e: + result.actual_behavior = f"Error: {type(e).__name__}: {e}" + result.passed = False + + return result + + +def test_store_during_eviction_storm() -> AdversarialResult: + """ + ATTACK: Rapid store/retrieve during aggressive eviction. + + max_entries=5 with 100 concurrent stores. + """ + result = AdversarialResult( + name="Eviction Storm", + category="extreme", + expected_behavior="Should maintain consistency during eviction", + severity="high", + ) + + reset_compression_store() + # Create store with very small capacity + store = CompressionStore(max_entries=5, default_ttl=300) + + stored_hashes = [] + retrieved_count = [0] + errors = [] + lock = threading.Lock() + + def store_and_retrieve(): + for _i in range(50): + try: + items = [{"id": j, "thread": threading.current_thread().name} for j in range(10)] + hash_key = store.store( + original=json.dumps(items), + compressed=json.dumps(items[:2]), + original_item_count=10, + compressed_item_count=2, + tool_name="eviction_test", + ) + + with lock: + stored_hashes.append(hash_key) + + # Immediately try to retrieve + entry = store.retrieve(hash_key) + if entry: + with lock: + retrieved_count[0] += 1 + + except Exception as e: + with lock: + errors.append(str(e)) + + try: + with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor: + futures = [executor.submit(store_and_retrieve) for _ in range(20)] + concurrent.futures.wait(futures) + + if errors: + result.actual_behavior = f"Errors: {errors[:3]}" + result.passed = False + else: + # Some eviction is expected, but we shouldn't crash + result.actual_behavior = ( + f"Stored {len(stored_hashes)}, retrieved {retrieved_count[0]} (eviction expected)" + ) + result.passed = True + except Exception as e: + result.actual_behavior = f"Crashed: {type(e).__name__}: {e}" + result.passed = False + + return result + + +# ============================================================================= +# MAIN +# ============================================================================= + + +def main(): + print("\n" + "=" * 70) + print(" ADVERSARIAL CCR TESTS") + print(" Intentionally Trying to Break Our Code") + print("=" * 70 + "\n") + + tests = [ + # Semantic attacks + test_all_items_are_errors, + test_error_keyword_in_normal_data, + test_needle_looks_exactly_like_hay, + test_anomaly_in_string_not_number, + # Boundary conditions + test_empty_array, + test_single_item_array, + test_exactly_max_items, + test_max_items_plus_one, + test_hash_collision_attempt, + test_ttl_exact_boundary, + # Injection attacks + test_json_injection_in_content, + test_headroom_marker_collision, + test_unicode_and_emoji_handling, + test_extremely_long_strings, + test_query_injection_in_search, + # Race conditions + test_concurrent_store_same_content, + test_concurrent_store_and_evict, + test_concurrent_feedback_updates, + # Deceptive data + test_hidden_error_in_nested_structure, + test_misleading_score_field, + test_timestamp_anomaly_not_value, + # EXTREME stress tests + test_deeply_nested_structure, + test_nan_infinity_scores, + test_mixed_type_array, + test_catastrophic_regex_in_search, + test_million_items, + test_item_with_thousands_of_fields, + test_identical_items, + test_all_fields_none, + test_unicode_normalization_attack, + test_concurrent_reset_during_operation, + test_zero_byte_in_content, + test_recursive_json_structure, + test_extreme_numeric_values, + test_adversarial_field_names, + test_store_during_eviction_storm, + ] + + results_by_category = {} + + for test_func in tests: + print(f" Running {test_func.__name__}...", end=" ", flush=True) + result = run_test(test_func) + + if result.category not in results_by_category: + results_by_category[result.category] = [] + results_by_category[result.category].append(result) + + status = "✓" if result.passed else "✗" + print(f"{status}") + + # Summary + print("\n" + "=" * 70) + print(" RESULTS BY CATEGORY") + print("=" * 70) + + total_passed = 0 + total_tests = 0 + critical_failures = [] + + for category, results in results_by_category.items(): + passed = sum(1 for r in results if r.passed) + total = len(results) + total_passed += passed + total_tests += total + + print(f"\n {category.upper()}: {passed}/{total}") + + for r in results: + status = "✓ PASS" if r.passed else "✗ FAIL" + print(f" {status} {r.name}") + + if not r.passed: + print(f" Expected: {r.expected_behavior}") + print(f" Actual: {r.actual_behavior}") + + if r.severity == "critical": + critical_failures.append(r) + + print("\n" + "=" * 70) + print(f" TOTAL: {total_passed}/{total_tests} tests passed") + + if critical_failures: + print(f"\n ⚠️ {len(critical_failures)} CRITICAL FAILURES:") + for r in critical_failures: + print(f" - {r.name}: {r.actual_behavior[:50]}") + + print("=" * 70 + "\n") + + # Exit code + failed = total_tests - total_passed + exit(failed) + + +if __name__ == "__main__": + main() diff --git a/benchmarks/agent_cost_benchmark.py b/benchmarks/agent_cost_benchmark.py new file mode 100644 index 000000000..ce38dcb8f --- /dev/null +++ b/benchmarks/agent_cost_benchmark.py @@ -0,0 +1,804 @@ +#!/usr/bin/env python3 +""" +Agent Cost Crisis Benchmark - The Compelling Story + +This benchmark demonstrates WHY Headroom matters by showing: + +1. THE PROBLEM: Context explosion in real-world agent workloads + - Tokens grow exponentially with conversation length + - Tool outputs dominate context (often 70%+ of tokens) + - Dynamic content breaks cache efficiency + +2. THE SOLUTION: Headroom's impact on real workloads + - Token reduction from SmartCrusher (50-80% on tool outputs) + - Cache alignment improvement (10x+ potential savings) + - Context windowing (stay within limits without losing info) + +3. THE PROOF: Quality preservation + - Critical information retained (errors, anomalies, relevant items) + - Agent task completion unaffected + - Information retrieval accuracy maintained + +Usage: + python benchmarks/agent_cost_benchmark.py + python benchmarks/agent_cost_benchmark.py --format markdown > BENCHMARK.md + python benchmarks/agent_cost_benchmark.py --scenario coding-agent +""" + +from __future__ import annotations + +import argparse +import json +import statistics +import time +from dataclasses import dataclass, field +from typing import Any + +# Benchmark scenario imports +from benchmarks.scenarios.conversations import ( + generate_agentic_conversation, + generate_rag_conversation, +) +from benchmarks.scenarios.tool_outputs import ( + generate_log_entries, + generate_search_results, +) + +# Headroom imports +from headroom.transforms.smart_crusher import SmartCrusherConfig, smart_crush_tool_output + +# ============================================================================= +# PRICING DATA (as of 2025) +# ============================================================================= + +PRICING = { + # Anthropic Claude 3.5 Sonnet + "claude-3.5-sonnet": { + "input": 3.00 / 1_000_000, # $3 per 1M tokens + "output": 15.00 / 1_000_000, # $15 per 1M tokens + "cached_input": 0.30 / 1_000_000, # 90% discount on cache hit + "cache_write": 3.75 / 1_000_000, # 25% premium to write cache + }, + # OpenAI GPT-4o + "gpt-4o": { + "input": 2.50 / 1_000_000, + "output": 10.00 / 1_000_000, + "cached_input": 1.25 / 1_000_000, # 50% discount + }, + # Google Gemini 1.5 Pro + "gemini-1.5-pro": { + "input": 1.25 / 1_000_000, + "output": 5.00 / 1_000_000, + "cached_input": 0.3125 / 1_000_000, # 75% discount + }, +} + +# Approximate tokens per character (GPT-4 tokenizer average) +CHARS_PER_TOKEN = 4 + + +@dataclass +class CostAnalysis: + """Cost analysis for a workload.""" + + tokens_input: int = 0 + tokens_output: int = 0 + tokens_cached: int = 0 + + cost_baseline: float = 0.0 + cost_optimized: float = 0.0 + cost_with_cache: float = 0.0 + + savings_from_compression: float = 0.0 + savings_from_caching: float = 0.0 + total_savings_percent: float = 0.0 + + +@dataclass +class BenchmarkResult: + """Result from a single benchmark scenario.""" + + name: str + description: str + + # Token metrics + tokens_original: int = 0 + tokens_optimized: int = 0 + compression_ratio: float = 0.0 + + # Cache metrics + cache_hit_rate_baseline: float = 0.0 + cache_hit_rate_optimized: float = 0.0 + + # Quality metrics + critical_items_retained: int = 0 + critical_items_total: int = 0 + retention_rate: float = 0.0 + + # Cost analysis + cost_analysis: CostAnalysis = field(default_factory=CostAnalysis) + + # Performance + optimization_latency_ms: float = 0.0 + + # Details + details: dict[str, Any] = field(default_factory=dict) + + +# ============================================================================= +# SCENARIO 1: Coding Agent Context Explosion +# ============================================================================= + + +def benchmark_coding_agent_explosion() -> BenchmarkResult: + """ + Simulate a Claude Code / Cursor style coding agent session. + + Shows how context explodes as the agent: + - Searches codebase (100s of file snippets) + - Reads documentation (large text blocks) + - Makes tool calls (grep, find, read) + - Accumulates conversation history + """ + result = BenchmarkResult( + name="Coding Agent Context Explosion", + description="50-turn coding session with file search, grep, and documentation lookups", + ) + + # Generate realistic coding agent conversation + messages = generate_agentic_conversation( + turns=50, + tool_calls_per_turn=2, + items_per_tool_response=100, # 100 search results per tool call + ) + + # Calculate original tokens + original_content = json.dumps(messages) + result.tokens_original = len(original_content) // CHARS_PER_TOKEN + + # Apply Headroom transforms using convenience function + config = SmartCrusherConfig(max_items_after_crush=20) + + start = time.perf_counter() + + optimized_messages = [] + critical_retained = 0 + critical_total = 0 + + for msg in messages: + if msg.get("role") == "tool": + # Parse tool content as JSON array + try: + original_content = msg.get("content", "[]") + content = json.loads(original_content) + if isinstance(content, list) and len(content) > 10: + # Count critical items (errors, high-relevance) + for item in content: + if isinstance(item, dict): + if item.get("error") or item.get("status") == "failed": + critical_total += 1 + if item.get("is_needle"): + critical_total += 1 + + # Compress with SmartCrusher convenience function + compressed_str, was_modified, _ = smart_crush_tool_output( + original_content, config + ) + + if was_modified: + compressed = json.loads(compressed_str) + # Count retained critical items + for item in compressed: + if isinstance(item, dict): + if item.get("error") or item.get("status") == "failed": + critical_retained += 1 + if item.get("is_needle"): + critical_retained += 1 + + msg = {**msg, "content": compressed_str} + except (json.JSONDecodeError, TypeError): + pass + + optimized_messages.append(msg) + + result.optimization_latency_ms = (time.perf_counter() - start) * 1000 + + # Calculate optimized tokens + optimized_content = json.dumps(optimized_messages) + result.tokens_optimized = len(optimized_content) // CHARS_PER_TOKEN + + # Calculate metrics + result.compression_ratio = 1 - (result.tokens_optimized / result.tokens_original) + result.critical_items_total = critical_total + result.critical_items_retained = critical_retained + result.retention_rate = critical_retained / critical_total if critical_total > 0 else 1.0 + + # Cost analysis (using Claude 3.5 Sonnet pricing) + pricing = PRICING["claude-3.5-sonnet"] + result.cost_analysis = CostAnalysis( + tokens_input=result.tokens_original, + cost_baseline=result.tokens_original * pricing["input"], + cost_optimized=result.tokens_optimized * pricing["input"], + savings_from_compression=(result.tokens_original - result.tokens_optimized) + * pricing["input"], + ) + result.cost_analysis.total_savings_percent = result.compression_ratio * 100 + + result.details = { + "turns": 50, + "tool_calls": 100, + "items_per_response": 100, + "items_after_compression": 20, + } + + return result + + +# ============================================================================= +# SCENARIO 2: Cache Alignment Impact +# ============================================================================= + + +def benchmark_cache_alignment() -> BenchmarkResult: + """ + Show how dynamic content breaks caching and how CacheAligner fixes it. + + Simulates 100 requests with same base prompt but different dates. + Without alignment: 0% cache hits + With alignment: 90%+ cache hits + """ + from headroom.cache import DetectorConfig, DynamicContentDetector + + result = BenchmarkResult( + name="Cache Alignment Impact", + description="100 requests with dynamic dates - cache hit improvement", + ) + + # Base system prompt with dynamic date + base_prompt = """You are Claude, an AI assistant by Anthropic. + +Today is {date}. +Current time: {time}. + +Session ID: {session_id} +Request ID: {request_id} + +You are a helpful coding assistant. Follow these guidelines: +1. Write clean, readable code +2. Add appropriate comments +3. Handle errors gracefully +4. Follow best practices + +Be concise and helpful.""" + + import datetime + import uuid + + # Use DynamicContentDetector to extract static content + detector = DynamicContentDetector(DetectorConfig(tiers=["regex"])) + + # Simulate 100 requests over a day + prompts_original = [] + prompts_aligned = [] + + base_date = datetime.datetime(2025, 1, 15, 9, 0, 0) + + for i in range(100): + # Each request has different timestamp + request_time = base_date + datetime.timedelta(minutes=i * 5) + + prompt = base_prompt.format( + date=request_time.strftime("%A, %B %d, %Y"), + time=request_time.strftime("%I:%M %p"), + session_id=f"sess_{uuid.uuid4().hex[:24]}", + request_id=f"req_{uuid.uuid4().hex[:24]}", + ) + prompts_original.append(prompt) + + # Extract static content for cache alignment + detection_result = detector.detect(prompt) + prompts_aligned.append(detection_result.static_content) + + # Calculate cache hits + # Baseline: all prompts are different (dynamic dates) + unique_original = len(set(prompts_original)) + cache_hits_baseline = 100 - unique_original + + # Aligned: static prefixes should be identical + unique_aligned = len(set(prompts_aligned)) + cache_hits_aligned = 100 - unique_aligned + + result.cache_hit_rate_baseline = cache_hits_baseline / 100 + result.cache_hit_rate_optimized = cache_hits_aligned / 100 + + # Token calculation + result.tokens_original = sum(len(p) // CHARS_PER_TOKEN for p in prompts_original) + + # Cost analysis with caching + pricing = PRICING["claude-3.5-sonnet"] + tokens_per_request = len(prompts_original[0]) // CHARS_PER_TOKEN + + # Baseline: pay full price every time (no cache hits) + cost_baseline = 100 * tokens_per_request * pricing["input"] + + # Optimized: first request is cache write, rest are cache hits + first_request_cost = tokens_per_request * pricing["cache_write"] + cached_requests_cost = 99 * tokens_per_request * pricing["cached_input"] + cost_optimized = first_request_cost + cached_requests_cost + + result.cost_analysis = CostAnalysis( + tokens_input=result.tokens_original, + cost_baseline=cost_baseline, + cost_with_cache=cost_optimized, + savings_from_caching=cost_baseline - cost_optimized, + total_savings_percent=((cost_baseline - cost_optimized) / cost_baseline) * 100, + ) + + result.details = { + "total_requests": 100, + "unique_prompts_baseline": unique_original, + "unique_prompts_aligned": unique_aligned, + "cache_improvement_factor": f"{(cache_hits_aligned - cache_hits_baseline)}x", + } + + return result + + +# ============================================================================= +# SCENARIO 3: RAG Context Scaling +# ============================================================================= + + +def benchmark_rag_scaling() -> BenchmarkResult: + """ + Show how RAG context grows and how Headroom manages it. + + Simulates large RAG context with multiple queries. + """ + result = BenchmarkResult( + name="RAG Context Scaling", description="Large RAG context (~50K tokens) with compression" + ) + + # Generate RAG conversation with ~50K tokens of context + messages = generate_rag_conversation( + context_tokens=50000, + num_queries=10, + ) + + original_content = json.dumps(messages) + result.tokens_original = len(original_content) // CHARS_PER_TOKEN + + # Apply transforms - compress tool outputs in messages + config = SmartCrusherConfig(max_items_after_crush=10) + + start = time.perf_counter() + + # Compress tool outputs in messages + optimized_messages = [] + for msg in messages: + if msg.get("role") == "tool": + try: + original_content_msg = msg.get("content", "[]") + compressed_str, was_modified, _ = smart_crush_tool_output( + original_content_msg, config + ) + if was_modified: + msg = {**msg, "content": compressed_str} + except Exception: + pass + optimized_messages.append(msg) + + result.optimization_latency_ms = (time.perf_counter() - start) * 1000 + + optimized_content = json.dumps(optimized_messages) + result.tokens_optimized = len(optimized_content) // CHARS_PER_TOKEN + result.compression_ratio = 1 - (result.tokens_optimized / result.tokens_original) + + # Cost analysis + pricing = PRICING["claude-3.5-sonnet"] + result.cost_analysis = CostAnalysis( + tokens_input=result.tokens_original, + cost_baseline=result.tokens_original * pricing["input"], + cost_optimized=result.tokens_optimized * pricing["input"], + savings_from_compression=(result.tokens_original - result.tokens_optimized) + * pricing["input"], + total_savings_percent=result.compression_ratio * 100, + ) + + result.details = { + "context_tokens": 50000, + "num_queries": 10, + } + + return result + + +# ============================================================================= +# SCENARIO 4: Long-Running Agent Session +# ============================================================================= + + +def benchmark_conversation_scaling() -> list[BenchmarkResult]: + """ + Show how costs scale with conversation length. + + Generates conversations of increasing length (10, 25, 50, 100, 200 turns) + and shows the scaling curve with and without Headroom. + """ + results = [] + turn_counts = [10, 25, 50, 100, 200] + + for turns in turn_counts: + result = BenchmarkResult( + name=f"Conversation Scaling ({turns} turns)", + description=f"{turns}-turn agent conversation with tool calls", + ) + + messages = generate_agentic_conversation( + turns=turns, + tool_calls_per_turn=1, + items_per_tool_response=50, + ) + + original_content = json.dumps(messages) + result.tokens_original = len(original_content) // CHARS_PER_TOKEN + + # Apply full optimization pipeline + config = SmartCrusherConfig(max_items_after_crush=15) + + start = time.perf_counter() + + optimized = [] + for msg in messages: + if msg.get("role") == "tool": + try: + original_content = msg.get("content", "[]") + content = json.loads(original_content) + if isinstance(content, list) and len(content) > 15: + compressed_str, was_modified, _ = smart_crush_tool_output( + original_content, config + ) + if was_modified: + msg = {**msg, "content": compressed_str} + except (json.JSONDecodeError, TypeError): + pass + optimized.append(msg) + + result.optimization_latency_ms = (time.perf_counter() - start) * 1000 + + optimized_content = json.dumps(optimized) + result.tokens_optimized = len(optimized_content) // CHARS_PER_TOKEN + result.compression_ratio = 1 - (result.tokens_optimized / result.tokens_original) + + pricing = PRICING["claude-3.5-sonnet"] + result.cost_analysis = CostAnalysis( + tokens_input=result.tokens_original, + cost_baseline=result.tokens_original * pricing["input"], + cost_optimized=result.tokens_optimized * pricing["input"], + total_savings_percent=result.compression_ratio * 100, + ) + + result.details = {"turns": turns} + results.append(result) + + return results + + +# ============================================================================= +# SCENARIO 5: Quality Preservation Test +# ============================================================================= + + +def benchmark_quality_preservation() -> BenchmarkResult: + """ + Prove that compression doesn't lose critical information. + + Generates data with known "needles" (errors, anomalies, high-relevance items) + and verifies they survive compression. + """ + result = BenchmarkResult( + name="Quality Preservation", + description="Verify critical items (errors, anomalies) survive compression", + ) + + # Generate test data with known needles + search_results = generate_search_results( + n=1000, + include_uuid_needles=10, + include_errors=20, + ) + + log_entries = generate_log_entries( + n=1000, + include_errors=30, + include_critical=5, + ) + + # Count needles before compression + needles_before = 0 + errors_before = 0 + + for item in search_results: + if item.get("is_needle"): + needles_before += 1 + if item.get("error"): + errors_before += 1 + + for entry in log_entries: + if entry.get("level") in ("ERROR", "CRITICAL"): + errors_before += 1 + + # Compress using SmartCrusher convenience function + config = SmartCrusherConfig(max_items_after_crush=50) + + search_str = json.dumps(search_results) + logs_str = json.dumps(log_entries) + + compressed_search_str, _, _ = smart_crush_tool_output(search_str, config) + compressed_logs_str, _, _ = smart_crush_tool_output(logs_str, config) + + compressed_search = json.loads(compressed_search_str) + compressed_logs = json.loads(compressed_logs_str) + + # Count needles after compression + needles_after = 0 + errors_after = 0 + + for item in compressed_search: + if item.get("is_needle"): + needles_after += 1 + if item.get("error"): + errors_after += 1 + + for entry in compressed_logs: + if entry.get("level") in ("ERROR", "CRITICAL"): + errors_after += 1 + + result.critical_items_total = needles_before + errors_before + result.critical_items_retained = needles_after + errors_after + result.retention_rate = result.critical_items_retained / result.critical_items_total + + result.tokens_original = ( + len(json.dumps(search_results)) + len(json.dumps(log_entries)) + ) // CHARS_PER_TOKEN + result.tokens_optimized = ( + len(json.dumps(compressed_search)) + len(json.dumps(compressed_logs)) + ) // CHARS_PER_TOKEN + result.compression_ratio = 1 - (result.tokens_optimized / result.tokens_original) + + result.details = { + "search_results_original": 1000, + "search_results_compressed": len(compressed_search), + "log_entries_original": 1000, + "log_entries_compressed": len(compressed_logs), + "needles_original": needles_before, + "needles_retained": needles_after, + "errors_original": errors_before, + "errors_retained": errors_after, + } + + return result + + +# ============================================================================= +# REPORT GENERATION +# ============================================================================= + + +def generate_report(results: list[BenchmarkResult], format: str = "terminal") -> str: + """Generate benchmark report in specified format.""" + + if format == "markdown": + return _generate_markdown_report(results) + else: + return _generate_terminal_report(results) + + +def _generate_terminal_report(results: list[BenchmarkResult]) -> str: + """Generate colorful terminal report.""" + lines = [] + + lines.append("") + lines.append("=" * 80) + lines.append(" HEADROOM AGENT COST BENCHMARK") + lines.append(" The Context Optimization Layer for LLM Applications") + lines.append("=" * 80) + + total_savings = 0.0 + total_baseline = 0.0 + + for result in results: + lines.append("") + lines.append(f"{'─' * 80}") + lines.append(f" {result.name}") + lines.append(f" {result.description}") + lines.append(f"{'─' * 80}") + + # Token metrics + lines.append(f" Tokens (original): {result.tokens_original:>12,}") + lines.append(f" Tokens (optimized): {result.tokens_optimized:>12,}") + lines.append(f" Compression: {result.compression_ratio * 100:>11.1f}%") + + # Cache metrics (if applicable) + if result.cache_hit_rate_optimized > 0: + lines.append(f" Cache Hit (before): {result.cache_hit_rate_baseline * 100:>11.1f}%") + lines.append(f" Cache Hit (after): {result.cache_hit_rate_optimized * 100:>11.1f}%") + + # Quality metrics (if applicable) + if result.critical_items_total > 0: + lines.append( + f" Critical Items: {result.critical_items_retained}/{result.critical_items_total} retained" + ) + lines.append(f" Retention Rate: {result.retention_rate * 100:>11.1f}%") + + # Cost analysis + ca = result.cost_analysis + if ca.cost_baseline > 0: + lines.append(f" Cost (baseline): ${ca.cost_baseline:>11.4f}") + if ca.cost_optimized > 0: + lines.append(f" Cost (optimized): ${ca.cost_optimized:>11.4f}") + if ca.cost_with_cache > 0: + lines.append(f" Cost (with cache): ${ca.cost_with_cache:>11.4f}") + lines.append(f" Savings: {ca.total_savings_percent:>11.1f}%") + + total_baseline += ca.cost_baseline + if ca.cost_optimized > 0: + total_savings += ca.cost_baseline - ca.cost_optimized + elif ca.cost_with_cache > 0: + total_savings += ca.cost_baseline - ca.cost_with_cache + + # Performance + if result.optimization_latency_ms > 0: + lines.append(f" Optimization Time: {result.optimization_latency_ms:>11.2f}ms") + + # Summary + lines.append("") + lines.append("=" * 80) + lines.append(" SUMMARY") + lines.append("=" * 80) + if total_baseline > 0: + lines.append(f" Total Baseline Cost: ${total_baseline:.4f}") + lines.append(f" Total Savings: ${total_savings:.4f}") + lines.append(f" Overall Reduction: {(total_savings / total_baseline) * 100:.1f}%") + lines.append("") + lines.append(" At 1M requests/month:") + lines.append(f" Without Headroom: ${total_baseline * 1_000_000:.2f}") + lines.append(f" With Headroom: ${(total_baseline - total_savings) * 1_000_000:.2f}") + lines.append(f" Monthly Savings: ${total_savings * 1_000_000:.2f}") + lines.append("") + + return "\n".join(lines) + + +def _generate_markdown_report(results: list[BenchmarkResult]) -> str: + """Generate markdown report for documentation.""" + lines = [] + + lines.append("# Headroom Agent Cost Benchmark") + lines.append("") + lines.append("> The Context Optimization Layer for LLM Applications") + lines.append("") + lines.append("## Executive Summary") + lines.append("") + lines.append("This benchmark demonstrates Headroom's impact on real-world agent workloads:") + lines.append("") + lines.append("| Metric | Impact |") + lines.append("|--------|--------|") + + # Calculate summary metrics + total_compression = statistics.mean( + [r.compression_ratio for r in results if r.compression_ratio > 0] + ) + cache_improvement = next((r for r in results if r.cache_hit_rate_optimized > 0), None) + quality_result = next((r for r in results if r.retention_rate > 0), None) + + lines.append(f"| Token Reduction | **{total_compression * 100:.0f}%** average compression |") + if cache_improvement: + lines.append( + f"| Cache Hit Rate | **{cache_improvement.cache_hit_rate_baseline * 100:.0f}% → {cache_improvement.cache_hit_rate_optimized * 100:.0f}%** |" + ) + if quality_result: + lines.append( + f"| Quality Retention | **{quality_result.retention_rate * 100:.0f}%** critical items preserved |" + ) + lines.append("") + + # Detailed results + lines.append("## Detailed Results") + lines.append("") + + for result in results: + lines.append(f"### {result.name}") + lines.append("") + lines.append(f"*{result.description}*") + lines.append("") + + lines.append("| Metric | Value |") + lines.append("|--------|-------|") + lines.append(f"| Original Tokens | {result.tokens_original:,} |") + lines.append(f"| Optimized Tokens | {result.tokens_optimized:,} |") + lines.append(f"| Compression | {result.compression_ratio * 100:.1f}% |") + + if result.cost_analysis.total_savings_percent > 0: + lines.append(f"| Cost Savings | {result.cost_analysis.total_savings_percent:.1f}% |") + + if result.retention_rate > 0: + lines.append(f"| Quality Retention | {result.retention_rate * 100:.1f}% |") + + lines.append("") + + # Cost projection + lines.append("## Cost Projection at Scale") + lines.append("") + lines.append("Based on Claude 3.5 Sonnet pricing ($3/1M input tokens):") + lines.append("") + lines.append("| Scale | Without Headroom | With Headroom | Monthly Savings |") + lines.append("|-------|------------------|---------------|-----------------|") + + base_cost_per_request = sum(r.cost_analysis.cost_baseline for r in results) / len(results) + optimized_cost = sum( + r.cost_analysis.cost_optimized + or r.cost_analysis.cost_with_cache + or r.cost_analysis.cost_baseline * 0.5 + for r in results + ) / len(results) + + for scale, label in [(10_000, "10K"), (100_000, "100K"), (1_000_000, "1M")]: + baseline = base_cost_per_request * scale + optimized = optimized_cost * scale + savings = baseline - optimized + lines.append( + f"| {label} requests/mo | ${baseline:,.0f} | ${optimized:,.0f} | ${savings:,.0f} |" + ) + + lines.append("") + + return "\n".join(lines) + + +# ============================================================================= +# MAIN +# ============================================================================= + + +def main(): + parser = argparse.ArgumentParser(description="Headroom Agent Cost Benchmark") + parser.add_argument("--format", choices=["terminal", "markdown"], default="terminal") + parser.add_argument( + "--scenario", + choices=["all", "coding-agent", "cache", "rag", "scaling", "quality"], + default="all", + ) + args = parser.parse_args() + + results = [] + + print("Running benchmarks...\n") + + if args.scenario in ("all", "coding-agent"): + print(" [1/5] Coding Agent Context Explosion...") + results.append(benchmark_coding_agent_explosion()) + + if args.scenario in ("all", "cache"): + print(" [2/5] Cache Alignment Impact...") + results.append(benchmark_cache_alignment()) + + if args.scenario in ("all", "rag"): + print(" [3/5] RAG Context Scaling...") + results.append(benchmark_rag_scaling()) + + if args.scenario in ("all", "scaling"): + print(" [4/5] Conversation Scaling...") + scaling_results = benchmark_conversation_scaling() + # Just add the 100-turn result to main results + results.append(scaling_results[3]) # 100 turns + + if args.scenario in ("all", "quality"): + print(" [5/5] Quality Preservation...") + results.append(benchmark_quality_preservation()) + + print("\n" + generate_report(results, args.format)) + + +if __name__ == "__main__": + main() diff --git a/benchmarks/bench_relevance.py b/benchmarks/bench_relevance.py index 4cd3cb17e..4e657337f 100644 --- a/benchmarks/bench_relevance.py +++ b/benchmarks/bench_relevance.py @@ -25,7 +25,6 @@ Run with: from __future__ import annotations import json -from typing import Any import pytest @@ -33,7 +32,8 @@ import pytest def _check_embedding_available() -> bool: """Check if sentence-transformers is available for embedding tests.""" try: - import sentence_transformers + import sentence_transformers # noqa: F401 + return True except ImportError: return False @@ -203,8 +203,8 @@ class TestHybridBenchmarks: @pytest.fixture def scorer_fallback(self): """Create hybrid scorer without embeddings (BM25 fallback).""" - from headroom.relevance.hybrid import HybridScorer from headroom.relevance.bm25 import BM25Scorer + from headroom.relevance.hybrid import HybridScorer # Force BM25-only mode by not providing embedding scorer scorer = HybridScorer( @@ -378,8 +378,8 @@ class TestRelevanceInSmartCrusher: @pytest.fixture def crusher_with_bm25(self, smart_crusher_config): """SmartCrusher with BM25 relevance scorer.""" - from headroom.transforms.smart_crusher import SmartCrusher from headroom.config import RelevanceScorerConfig + from headroom.transforms.smart_crusher import SmartCrusher return SmartCrusher( config=smart_crusher_config, @@ -389,8 +389,8 @@ class TestRelevanceInSmartCrusher: @pytest.fixture def crusher_with_hybrid(self, smart_crusher_config): """SmartCrusher with hybrid relevance scorer.""" - from headroom.transforms.smart_crusher import SmartCrusher from headroom.config import RelevanceScorerConfig + from headroom.transforms.smart_crusher import SmartCrusher return SmartCrusher( config=smart_crusher_config, diff --git a/benchmarks/bench_transforms.py b/benchmarks/bench_transforms.py index 7d6cc8bd6..049208406 100644 --- a/benchmarks/bench_transforms.py +++ b/benchmarks/bench_transforms.py @@ -26,7 +26,6 @@ Run with: from __future__ import annotations import json -from typing import Any import pytest @@ -205,8 +204,16 @@ class TestSmartCrusherBenchmarks: "role": "assistant", "content": None, "tool_calls": [ - {"id": "call_1", "type": "function", "function": {"name": "search", "arguments": "{}"}}, - {"id": "call_2", "type": "function", "function": {"name": "logs", "arguments": "{}"}}, + { + "id": "call_1", + "type": "function", + "function": {"name": "search", "arguments": "{}"}, + }, + { + "id": "call_2", + "type": "function", + "function": {"name": "logs", "arguments": "{}"}, + }, ], }, {"role": "tool", "tool_call_id": "call_1", "content": json.dumps(items_100)}, @@ -345,7 +352,10 @@ And multiple blank lines.""" Tests edge case of multiple system prompts. """ messages = [ - {"role": "system", "content": "You are a helpful assistant.\n\nCurrent date: 2025-01-06"}, + { + "role": "system", + "content": "You are a helpful assistant.\n\nCurrent date: 2025-01-06", + }, {"role": "system", "content": "Additional context: Technical support mode."}, {"role": "user", "content": "Hello"}, ] @@ -511,12 +521,14 @@ class TestTransformPipelineBenchmarks: return provider @pytest.fixture - def pipeline(self, smart_crusher_config, cache_aligner_config, rolling_window_config, mock_provider): + def pipeline( + self, smart_crusher_config, cache_aligner_config, rolling_window_config, mock_provider + ): """Create transform pipeline.""" - from headroom.transforms.pipeline import TransformPipeline from headroom.transforms.cache_aligner import CacheAligner - from headroom.transforms.smart_crusher import SmartCrusher + from headroom.transforms.pipeline import TransformPipeline from headroom.transforms.rolling_window import RollingWindow + from headroom.transforms.smart_crusher import SmartCrusher return TransformPipeline( transforms=[ diff --git a/benchmarks/ccr_regression_benchmark.py b/benchmarks/ccr_regression_benchmark.py new file mode 100644 index 000000000..5699c78d7 --- /dev/null +++ b/benchmarks/ccr_regression_benchmark.py @@ -0,0 +1,828 @@ +#!/usr/bin/env python3 +""" +CCR Regression Benchmark - Verify No Information Loss + +This benchmark tests that the CCR (Compress-Cache-Retrieve) architecture +does not cause any regression in agent behavior. Specifically: + +1. NEEDLE RETENTION: Critical items survive compression + - Errors, exceptions, failures + - Specific IDs/UUIDs mentioned in user query + - Anomalies and outliers + +2. RETRIEVAL ACCURACY: When retrieval is needed, correct items are returned + - Full retrieval returns original content + - Search retrieval finds relevant items + +3. FEEDBACK LEARNING: System learns from retrieval patterns + - High retrieval rate triggers less aggressive compression + - Common queries improve future compression + +Usage: + python benchmarks/ccr_regression_benchmark.py + python benchmarks/ccr_regression_benchmark.py --verbose + python benchmarks/ccr_regression_benchmark.py --scenario needle-in-haystack +""" + +from __future__ import annotations + +import argparse +import json +import time +import uuid +from dataclasses import dataclass, field +from typing import Any + +from headroom.cache.compression_feedback import ( + get_compression_feedback, + reset_compression_feedback, +) +from headroom.cache.compression_store import ( + get_compression_store, + reset_compression_store, +) +from headroom.transforms.smart_crusher import ( + SmartCrusherConfig, + smart_crush_tool_output, +) + + +@dataclass +class RegressionResult: + """Result from a regression test.""" + + name: str + description: str + passed: bool = False # Default to False, set to True when test passes + + # Metrics + total_needles: int = 0 + needles_retained: int = 0 + retention_rate: float = 0.0 + + # CCR metrics + items_compressed: int = 0 + items_retrieved: int = 0 + retrieval_accuracy: float = 0.0 + + # Performance + latency_ms: float = 0.0 + + # Details + details: dict[str, Any] = field(default_factory=dict) + failures: list[str] = field(default_factory=list) + + +# ============================================================================= +# TEST 1: Needle in Haystack - Error Retention +# ============================================================================= + + +def test_error_retention() -> RegressionResult: + """ + Test that errors are NEVER lost during compression. + + This is critical: if an API returns 1000 results with 3 errors, + those 3 errors MUST be in the compressed output. + """ + result = RegressionResult( + name="Error Retention", + description="Verify all errors survive compression regardless of position", + ) + + # Generate 1000 items with errors at various positions + items = [] + error_indices = [5, 47, 123, 456, 789, 999] # Spread throughout + + for i in range(1000): + if i in error_indices: + items.append( + { + "id": i, + "status": "error", + "message": f"Connection failed: timeout at {i}", + "error_code": 500 + (i % 10), + } + ) + else: + items.append( + { + "id": i, + "status": "success", + "message": "OK", + "data": {"value": i * 2}, + } + ) + + result.total_needles = len(error_indices) + + # Compress with SmartCrusher + config = SmartCrusherConfig(max_items_after_crush=15) + original_json = json.dumps(items) + + start = time.perf_counter() + compressed_json, was_modified, _ = smart_crush_tool_output(original_json, config) + result.latency_ms = (time.perf_counter() - start) * 1000 + + # Count errors in compressed output + compressed = json.loads(compressed_json) + errors_found = [item for item in compressed if item.get("status") == "error"] + + result.needles_retained = len(errors_found) + result.retention_rate = result.needles_retained / result.total_needles + result.items_compressed = len(compressed) + + # Check if ALL errors were retained + result.passed = result.needles_retained == result.total_needles + + if not result.passed: + result.failures.append( + f"Lost {result.total_needles - result.needles_retained} errors during compression" + ) + + result.details = { + "original_items": 1000, + "compressed_items": len(compressed), + "error_positions": error_indices, + "errors_retained": result.needles_retained, + } + + return result + + +# ============================================================================= +# TEST 2: Needle in Haystack - UUID Lookup +# ============================================================================= + + +def test_uuid_retrieval() -> RegressionResult: + """ + Test that specific UUIDs can be found via CCR retrieval. + + Scenario: User asks "find transaction abc123..." + The system compresses, but user should be able to retrieve the specific item. + """ + result = RegressionResult( + name="UUID Retrieval via CCR", + description="Verify specific UUIDs can be retrieved from compressed cache", + ) + + reset_compression_store() + store = get_compression_store() + + # Generate 1000 transactions with UUIDs + target_uuid = str(uuid.uuid4()) + items = [] + + for i in range(1000): + item_uuid = target_uuid if i == 456 else str(uuid.uuid4()) + items.append( + { + "transaction_id": item_uuid, + "amount": 100 + (i % 1000), + "status": "completed", + "timestamp": f"2025-01-{(i % 28) + 1:02d}T10:00:00Z", + } + ) + + result.total_needles = 1 + + # Store original and compress + original_json = json.dumps(items) + config = SmartCrusherConfig(max_items_after_crush=15) + + start = time.perf_counter() + compressed_json, was_modified, _ = smart_crush_tool_output(original_json, config) + + # Store in CCR cache + hash_key = store.store( + original=original_json, + compressed=compressed_json, + original_item_count=1000, + compressed_item_count=15, + tool_name="transaction_search", + ) + + # Search for the specific UUID + search_results = store.search(hash_key, target_uuid) + result.latency_ms = (time.perf_counter() - start) * 1000 + + # Check if target UUID was found + found_target = any(item.get("transaction_id") == target_uuid for item in search_results) + + result.needles_retained = 1 if found_target else 0 + result.retention_rate = result.needles_retained / result.total_needles + result.items_retrieved = len(search_results) + result.retrieval_accuracy = 1.0 if found_target else 0.0 + + result.passed = found_target + + if not result.passed: + result.failures.append( + f"Could not retrieve target UUID {target_uuid[:8]}... via CCR search" + ) + + result.details = { + "target_uuid": target_uuid, + "search_results_count": len(search_results), + "found_target": found_target, + "hash_key": hash_key, + } + + return result + + +# ============================================================================= +# TEST 3: Anomaly Detection +# ============================================================================= + + +def test_anomaly_retention() -> RegressionResult: + """ + Test that statistical anomalies are preserved during compression. + + Scenario: 1000 metrics mostly at ~50, but with 5 spikes at 500+. + Those spikes MUST survive compression. + """ + result = RegressionResult( + name="Anomaly Retention", description="Verify statistical outliers survive compression" + ) + + # Generate metrics with anomalies + import random + + random.seed(42) # Reproducible + + items = [] + anomaly_indices = [10, 200, 450, 700, 990] # 5 spikes + + for i in range(1000): + if i in anomaly_indices: + # Anomaly: 10x normal value + value = 500 + random.randint(0, 100) + else: + # Normal: around 50 + value = 50 + random.randint(-10, 10) + + items.append( + { + "timestamp": f"2025-01-07T{(i // 60):02d}:{(i % 60):02d}:00Z", + "cpu_percent": value, + "host": "prod-server-1", + } + ) + + result.total_needles = len(anomaly_indices) + + # Compress + config = SmartCrusherConfig( + max_items_after_crush=20, + preserve_change_points=True, + ) + original_json = json.dumps(items) + + start = time.perf_counter() + compressed_json, was_modified, _ = smart_crush_tool_output(original_json, config) + result.latency_ms = (time.perf_counter() - start) * 1000 + + # Count anomalies (cpu > 200) in compressed output + compressed = json.loads(compressed_json) + anomalies_found = [ + item + for item in compressed + if isinstance(item.get("cpu_percent"), (int, float)) and item["cpu_percent"] > 200 + ] + + result.needles_retained = len(anomalies_found) + result.retention_rate = result.needles_retained / result.total_needles + result.items_compressed = len(compressed) + + # Pass if at least 80% of anomalies retained (some might be in change point windows) + result.passed = result.retention_rate >= 0.8 + + if not result.passed: + result.failures.append( + f"Lost too many anomalies: {result.needles_retained}/{result.total_needles} retained" + ) + + result.details = { + "original_items": 1000, + "compressed_items": len(compressed), + "anomaly_positions": anomaly_indices, + "anomalies_retained": result.needles_retained, + } + + return result + + +# ============================================================================= +# TEST 4: Full Retrieval Accuracy +# ============================================================================= + + +def test_full_retrieval() -> RegressionResult: + """ + Test that full retrieval returns EXACTLY the original content. + """ + result = RegressionResult( + name="Full Retrieval Accuracy", + description="Verify full retrieval returns exact original content", + ) + + reset_compression_store() + store = get_compression_store() + + # Generate test data + items = [{"id": i, "name": f"item_{i}", "value": i * 10} for i in range(100)] + + original_json = json.dumps(items) + compressed_json = json.dumps(items[:10]) # Simulate compression + + # Store + hash_key = store.store( + original=original_json, + compressed=compressed_json, + original_item_count=100, + compressed_item_count=10, + tool_name="test_tool", + ) + + start = time.perf_counter() + + # Retrieve + entry = store.retrieve(hash_key) + + result.latency_ms = (time.perf_counter() - start) * 1000 + + # Verify content matches exactly + if entry is None: + result.passed = False + result.failures.append("Retrieval returned None") + else: + retrieved_items = json.loads(entry.original_content) + result.passed = retrieved_items == items + result.items_retrieved = len(retrieved_items) + result.retrieval_accuracy = 1.0 if result.passed else 0.0 + + if not result.passed: + result.failures.append("Retrieved content does not match original") + + result.total_needles = 100 + result.needles_retained = result.items_retrieved + result.retention_rate = 1.0 if result.passed else 0.0 + + result.details = { + "original_items": 100, + "retrieved_items": result.items_retrieved, + "hash_key": hash_key, + } + + return result + + +# ============================================================================= +# TEST 5: Feedback Learning +# ============================================================================= + + +def test_feedback_learning() -> RegressionResult: + """ + Test that the feedback system learns from retrieval patterns. + + Scenario: Simulate high retrieval rate, verify system recommends + less aggressive compression. + """ + result = RegressionResult( + name="Feedback Learning", + description="Verify feedback loop adjusts compression based on patterns", + ) + + reset_compression_feedback() + feedback = get_compression_feedback() + + tool_name = "high_retrieval_tool" + + start = time.perf_counter() + + # Simulate 10 compressions + for _ in range(10): + feedback.record_compression(tool_name, 1000, 20) + + # Simulate 6 retrievals (60% rate - HIGH) + from headroom.cache.compression_store import RetrievalEvent + + for i in range(6): + event = RetrievalEvent( + hash=f"hash{i:012d}", + query="find errors", + items_retrieved=100, + total_items=1000, + tool_name=tool_name, + timestamp=time.time(), + retrieval_type="search", + ) + feedback.record_retrieval(event) + + # Get hints + hints = feedback.get_compression_hints(tool_name) + + result.latency_ms = (time.perf_counter() - start) * 1000 + + # Verify hints recommend less aggressive compression + pattern = feedback.get_all_patterns().get(tool_name) + + checks_passed = 0 + total_checks = 3 + + # Check 1: Retrieval rate is tracked correctly + if pattern and abs(pattern.retrieval_rate - 0.6) < 0.01: + checks_passed += 1 + else: + result.failures.append( + f"Retrieval rate incorrect: {pattern.retrieval_rate if pattern else 'N/A'}" + ) + + # Check 2: Hints suggest more items (>15 default) + if hints.max_items > 15: + checks_passed += 1 + else: + result.failures.append(f"max_items not increased: {hints.max_items}") + + # Check 3: Aggressiveness reduced (<0.7 default) + if hints.aggressiveness < 0.7: + checks_passed += 1 + else: + result.failures.append(f"Aggressiveness not reduced: {hints.aggressiveness}") + + result.passed = checks_passed == total_checks + result.retrieval_accuracy = checks_passed / total_checks + + result.details = { + "compressions_recorded": 10, + "retrievals_recorded": 6, + "calculated_retrieval_rate": pattern.retrieval_rate if pattern else 0, + "recommended_max_items": hints.max_items, + "recommended_aggressiveness": hints.aggressiveness, + "reason": hints.reason, + } + + return result + + +# ============================================================================= +# TEST 6: Search Within Cached Content +# ============================================================================= + + +def test_search_accuracy() -> RegressionResult: + """ + Test that BM25 search within cached content finds relevant items. + """ + result = RegressionResult( + name="Search Accuracy", description="Verify BM25 search finds relevant items in cache" + ) + + reset_compression_store() + store = get_compression_store() + + # Generate log entries with specific error messages + items = [] + for i in range(100): + if i in [15, 45, 78]: + # Target: authentication errors + items.append( + { + "id": i, + "level": "ERROR", + "message": "Authentication failed: invalid token", + "service": "auth-service", + } + ) + elif i in [20, 60]: + # Other errors (should not match auth search) + items.append( + { + "id": i, + "level": "ERROR", + "message": "Database connection timeout", + "service": "db-service", + } + ) + else: + items.append( + { + "id": i, + "level": "INFO", + "message": "Request processed successfully", + "service": "api-service", + } + ) + + result.total_needles = 3 # 3 auth errors + + original_json = json.dumps(items) + compressed_json = json.dumps(items[:10]) + + # Store + hash_key = store.store( + original=original_json, + compressed=compressed_json, + original_item_count=100, + compressed_item_count=10, + tool_name="log_search", + ) + + start = time.perf_counter() + + # Search for authentication errors + search_results = store.search(hash_key, "authentication failed token") + + result.latency_ms = (time.perf_counter() - start) * 1000 + + # Count auth errors in results + auth_errors = [ + item for item in search_results if "authentication" in item.get("message", "").lower() + ] + + result.needles_retained = len(auth_errors) + result.retention_rate = result.needles_retained / result.total_needles + result.items_retrieved = len(search_results) + + # Pass if at least 2 of 3 auth errors found + result.passed = result.needles_retained >= 2 + result.retrieval_accuracy = result.retention_rate + + if not result.passed: + result.failures.append( + f"Search found only {result.needles_retained}/{result.total_needles} auth errors" + ) + + result.details = { + "query": "authentication failed token", + "total_results": len(search_results), + "auth_errors_found": result.needles_retained, + "hash_key": hash_key, + } + + return result + + +# ============================================================================= +# TEST 7: CCR End-to-End Flow +# ============================================================================= + + +def test_ccr_end_to_end() -> RegressionResult: + """ + Test the complete CCR flow: compress → cache → retrieve → feedback. + """ + result = RegressionResult( + name="CCR End-to-End Flow", + description="Verify complete compress-cache-retrieve cycle works", + ) + + reset_compression_store() + reset_compression_feedback() + + store = get_compression_store() + feedback = get_compression_feedback() + + # Generate data with known needles + items = [] + for i in range(500): + if i == 123: + items.append( + { + "id": i, + "type": "critical_alert", + "message": "System overload detected", + "priority": "P0", + } + ) + elif i in [50, 200, 400]: + items.append( + { + "id": i, + "type": "error", + "message": f"Error at position {i}", + "priority": "P1", + } + ) + else: + items.append( + { + "id": i, + "type": "info", + "message": f"Normal operation {i}", + "priority": "P3", + } + ) + + result.total_needles = 4 # 1 critical + 3 errors + + start = time.perf_counter() + + # Step 1: Compress + config = SmartCrusherConfig(max_items_after_crush=20) + original_json = json.dumps(items) + compressed_json, was_modified, _ = smart_crush_tool_output(original_json, config) + + # Step 2: Cache + hash_key = store.store( + original=original_json, + compressed=compressed_json, + original_item_count=500, + compressed_item_count=20, + tool_name="alert_search", + ) + + # Step 3: Record compression in feedback + feedback.record_compression("alert_search", 500, 20) + + # Step 4: Retrieve and search + critical_results = store.search(hash_key, "critical system overload P0") + error_results = store.search(hash_key, "Error position P1") + + # Step 5: Process feedback + store.process_pending_feedback() + + result.latency_ms = (time.perf_counter() - start) * 1000 + + # Verify results + checks_passed = 0 + total_checks = 4 + + # Check 1: Critical alert found + critical_found = any(item.get("type") == "critical_alert" for item in critical_results) + if critical_found: + checks_passed += 1 + else: + result.failures.append("Critical alert not found in search") + + # Check 2: Errors found (search by message content) + errors_found = len( + [ + item + for item in error_results + if item.get("type") == "error" or "Error" in str(item.get("message", "")) + ] + ) + if errors_found >= 2: + checks_passed += 1 + else: + result.failures.append(f"Only {errors_found} errors found in search") + + # Check 3: Store has entry + if store.exists(hash_key): + checks_passed += 1 + else: + result.failures.append("Entry not found in store") + + # Check 4: Feedback recorded + patterns = feedback.get_all_patterns() + if "alert_search" in patterns: + checks_passed += 1 + else: + result.failures.append("Feedback not recorded for tool") + + result.passed = checks_passed == total_checks + result.needles_retained = (1 if critical_found else 0) + errors_found + result.retention_rate = result.needles_retained / result.total_needles + result.items_retrieved = len(critical_results) + len(error_results) + result.retrieval_accuracy = checks_passed / total_checks + + result.details = { + "hash_key": hash_key, + "critical_found": critical_found, + "errors_found": errors_found, + "store_entry_exists": store.exists(hash_key), + "feedback_recorded": "alert_search" in patterns, + } + + return result + + +# ============================================================================= +# REPORT GENERATION +# ============================================================================= + + +def generate_report(results: list[RegressionResult], verbose: bool = False) -> str: + """Generate benchmark report.""" + lines = [] + + lines.append("") + lines.append("=" * 70) + lines.append(" CCR REGRESSION BENCHMARK") + lines.append(" Verifying No Information Loss") + lines.append("=" * 70) + + passed = sum(1 for r in results if r.passed) + total = len(results) + + lines.append("") + lines.append(f" Overall: {passed}/{total} tests passed") + lines.append("") + + for result in results: + status = "✓ PASS" if result.passed else "✗ FAIL" + lines.append(f"{'─' * 70}") + lines.append(f" {status} {result.name}") + lines.append(f" {result.description}") + + if result.total_needles > 0: + lines.append( + f" Needles: {result.needles_retained}/{result.total_needles} retained ({result.retention_rate * 100:.0f}%)" + ) + + if result.items_retrieved > 0: + lines.append(f" Retrieved: {result.items_retrieved} items") + + lines.append(f" Latency: {result.latency_ms:.2f}ms") + + if not result.passed: + for failure in result.failures: + lines.append(f" ❌ {failure}") + + if verbose and result.details: + lines.append(f" Details: {json.dumps(result.details, indent=2)}") + + lines.append("") + lines.append("=" * 70) + + if passed == total: + lines.append(" ✓ ALL TESTS PASSED - No regression detected") + else: + lines.append(f" ✗ {total - passed} TESTS FAILED - Review failures above") + + lines.append("=" * 70) + lines.append("") + + return "\n".join(lines) + + +# ============================================================================= +# MAIN +# ============================================================================= + + +def main(): + parser = argparse.ArgumentParser(description="CCR Regression Benchmark") + parser.add_argument("--verbose", "-v", action="store_true", help="Show detailed output") + parser.add_argument( + "--scenario", + choices=[ + "all", + "error-retention", + "uuid-retrieval", + "anomaly-retention", + "full-retrieval", + "feedback-learning", + "search-accuracy", + "e2e", + ], + default="all", + ) + args = parser.parse_args() + + results = [] + + print("\nRunning CCR regression tests...\n") + + if args.scenario in ("all", "error-retention"): + print(" [1/7] Error Retention...") + results.append(test_error_retention()) + + if args.scenario in ("all", "uuid-retrieval"): + print(" [2/7] UUID Retrieval...") + results.append(test_uuid_retrieval()) + + if args.scenario in ("all", "anomaly-retention"): + print(" [3/7] Anomaly Retention...") + results.append(test_anomaly_retention()) + + if args.scenario in ("all", "full-retrieval"): + print(" [4/7] Full Retrieval...") + results.append(test_full_retrieval()) + + if args.scenario in ("all", "feedback-learning"): + print(" [5/7] Feedback Learning...") + results.append(test_feedback_learning()) + + if args.scenario in ("all", "search-accuracy"): + print(" [6/7] Search Accuracy...") + results.append(test_search_accuracy()) + + if args.scenario in ("all", "e2e"): + print(" [7/7] End-to-End Flow...") + results.append(test_ccr_end_to_end()) + + print(generate_report(results, args.verbose)) + + # Exit with error code if any test failed + failed = sum(1 for r in results if not r.passed) + exit(failed) + + +if __name__ == "__main__": + main() diff --git a/benchmarks/conftest.py b/benchmarks/conftest.py index 236e292c5..cbd67372f 100644 --- a/benchmarks/conftest.py +++ b/benchmarks/conftest.py @@ -15,21 +15,19 @@ from __future__ import annotations import json import random from typing import Any -from unittest.mock import Mock import pytest +from benchmarks.scenarios.conversations import ( + generate_agentic_conversation, + generate_rag_conversation, +) from benchmarks.scenarios.tool_outputs import ( generate_api_responses, generate_database_rows, generate_log_entries, generate_search_results, ) -from benchmarks.scenarios.conversations import ( - generate_agentic_conversation, - generate_rag_conversation, -) - # Set seed for reproducible benchmarks random.seed(42) @@ -159,21 +157,27 @@ def api_responses_100() -> list[dict[str, Any]]: def conversation_10_turns() -> list[dict[str, Any]]: """Generate 10-turn agentic conversation with tool calls.""" random.seed(42) - return generate_agentic_conversation(turns=10, tool_calls_per_turn=1, items_per_tool_response=50) + return generate_agentic_conversation( + turns=10, tool_calls_per_turn=1, items_per_tool_response=50 + ) @pytest.fixture def conversation_50_turns() -> list[dict[str, Any]]: """Generate 50-turn agentic conversation with tool calls.""" random.seed(42) - return generate_agentic_conversation(turns=50, tool_calls_per_turn=2, items_per_tool_response=50) + return generate_agentic_conversation( + turns=50, tool_calls_per_turn=2, items_per_tool_response=50 + ) @pytest.fixture def conversation_200_turns() -> list[dict[str, Any]]: """Generate 200-turn agentic conversation (stress test).""" random.seed(42) - return generate_agentic_conversation(turns=200, tool_calls_per_turn=1, items_per_tool_response=30) + return generate_agentic_conversation( + turns=200, tool_calls_per_turn=1, items_per_tool_response=30 + ) @pytest.fixture diff --git a/benchmarks/dynamic_detector_benchmark.py b/benchmarks/dynamic_detector_benchmark.py index 4a8d307e9..c3bafa0c9 100644 --- a/benchmarks/dynamic_detector_benchmark.py +++ b/benchmarks/dynamic_detector_benchmark.py @@ -6,21 +6,21 @@ Tests the detector against realistic system prompts from AI coding agents, chatbots, and enterprise applications. """ -import time import statistics +import time from dataclasses import dataclass from typing import Any from headroom.cache.dynamic_detector import ( DetectorConfig, DynamicContentDetector, - DynamicCategory, ) @dataclass class BenchmarkResult: """Result of a single benchmark run.""" + name: str content_length: int spans_found: int @@ -50,7 +50,6 @@ User: tchopra Workspace: /Users/tchopra/claude-projects/headroom Be concise, accurate, and helpful. Follow the user's instructions carefully.""", - "enterprise_assistant": """You are an enterprise AI assistant for Acme Corporation. Current Date: 2026-01-07T10:30:00Z @@ -76,7 +75,6 @@ Budget Information: - Remaining: $2,658.33 Help the user with their work tasks while following company policies.""", - "coding_agent": """You are an autonomous coding agent with access to tools. Environment: @@ -99,7 +97,6 @@ API Keys Available: - DATABASE_URL: postgresql://user:pass@localhost:5432/mydb Execute tasks step by step, verify each action, and report progress.""", - "customer_support": """You are a customer support agent for TechStore Inc. Current Time: January 7, 2026, 3:45 PM EST @@ -122,7 +119,6 @@ Active Issues: - Case #CS-2026-0107-001 - Battery drain issue - Open since today Provide helpful, empathetic support while following company guidelines.""", - "data_analysis": """You are a data analysis assistant. Report Generated: 2026-01-07 10:30:00 UTC @@ -147,7 +143,6 @@ Anomalies Detected: - Drop on Dec 25: 0.4x normal (expected - holiday) Help analyze the data and provide insights.""", - "minimal_static": """You are a helpful AI assistant. Your role is to: @@ -157,7 +152,6 @@ Your role is to: 4. Admit when you don't know something Always be helpful, harmless, and honest.""", - "heavy_dynamic": """Session started at 2026-01-07T10:30:45.123Z Request ID: req_abc123def456ghi789jkl012mno345pqr678 Trace ID: 550e8400-e29b-41d4-a716-446655440000 @@ -205,19 +199,21 @@ def run_benchmark( result = detector.detect(content) elapsed = (time.perf_counter() - start) * 1000 - categories = list(set(s.category.value for s in result.spans)) + categories = list({s.category.value for s in result.spans}) - results[name].append(BenchmarkResult( - name=name, - content_length=len(content), - spans_found=len(result.spans), - categories=categories, - static_length=len(result.static_content), - dynamic_length=len(result.dynamic_content), - latency_ms=elapsed, - tiers_used=result.tiers_used, - warnings=result.warnings, - )) + results[name].append( + BenchmarkResult( + name=name, + content_length=len(content), + spans_found=len(result.spans), + categories=categories, + static_length=len(result.static_content), + dynamic_length=len(result.dynamic_content), + latency_ms=elapsed, + tiers_used=result.tiers_used, + warnings=result.warnings, + ) + ) return results @@ -228,9 +224,9 @@ def print_results( ): """Print benchmark results.""" - print(f"\n{'='*80}") + print(f"\n{'=' * 80}") print(f"BENCHMARK RESULTS: {tier_name}") - print(f"{'='*80}") + print(f"{'=' * 80}") for name, runs in results.items(): latencies = [r.latency_ms for r in runs] @@ -240,7 +236,11 @@ def print_results( # Use first run for span info (consistent across runs) first = runs[0] - compression = (1 - first.static_length / first.content_length) * 100 if first.content_length > 0 else 0 + compression = ( + (1 - first.static_length / first.content_length) * 100 + if first.content_length > 0 + else 0 + ) print(f"\n📄 {name}") print(f" Content: {first.content_length:,} chars") @@ -257,9 +257,9 @@ def print_results( def print_comparison(all_results: dict[str, dict[str, list[BenchmarkResult]]]): """Print comparison across tiers.""" - print(f"\n{'='*80}") + print(f"\n{'=' * 80}") print("TIER COMPARISON") - print(f"{'='*80}") + print(f"{'=' * 80}") prompts = list(REAL_WORLD_PROMPTS.keys()) tiers = list(all_results.keys()) @@ -284,9 +284,9 @@ def print_comparison(all_results: dict[str, dict[str, list[BenchmarkResult]]]): print(row) # Summary - print(f"\n{'='*80}") + print(f"\n{'=' * 80}") print("SUMMARY") - print(f"{'='*80}") + print(f"{'=' * 80}") for tier in tiers: all_latencies = [] @@ -297,7 +297,9 @@ def print_comparison(all_results: dict[str, dict[str, list[BenchmarkResult]]]): avg = statistics.mean(all_latencies) p50 = statistics.median(all_latencies) - p99 = sorted(all_latencies)[int(len(all_latencies) * 0.99)] if len(all_latencies) > 1 else avg + p99 = ( + sorted(all_latencies)[int(len(all_latencies) * 0.99)] if len(all_latencies) > 1 else avg + ) print(f"\n{tier}:") print(f" Total spans detected: {total_spans}") @@ -309,9 +311,9 @@ def print_comparison(all_results: dict[str, dict[str, list[BenchmarkResult]]]): def show_detection_details(prompt_name: str, content: str): """Show detailed detection for a specific prompt.""" - print(f"\n{'='*80}") + print(f"\n{'=' * 80}") print(f"DETECTION DETAILS: {prompt_name}") - print(f"{'='*80}") + print(f"{'=' * 80}") config = DetectorConfig(tiers=["regex"]) detector = DynamicContentDetector(config) @@ -324,11 +326,17 @@ def show_detection_details(prompt_name: str, content: str): print(f"\n\nDetected spans ({len(result.spans)}):") print("-" * 40) for span in result.spans: - print(f" [{span.category.value:12}] '{span.text[:50]}{'...' if len(span.text) > 50 else ''}'") + print( + f" [{span.category.value:12}] '{span.text[:50]}{'...' if len(span.text) > 50 else ''}'" + ) print(f"\n\nStatic content ({len(result.static_content)} chars):") print("-" * 40) - print(result.static_content[:500] + "..." if len(result.static_content) > 500 else result.static_content) + print( + result.static_content[:500] + "..." + if len(result.static_content) > 500 + else result.static_content + ) print(f"\n\nDynamic content ({len(result.dynamic_content)} chars):") print("-" * 40) diff --git a/benchmarks/run_benchmarks.py b/benchmarks/run_benchmarks.py index 4fd5e19d6..0914c54c3 100644 --- a/benchmarks/run_benchmarks.py +++ b/benchmarks/run_benchmarks.py @@ -40,7 +40,6 @@ from datetime import datetime from pathlib import Path from typing import Any - # Benchmark suite definitions BENCHMARK_SUITES = { "all": [ @@ -75,19 +74,19 @@ BENCHMARK_SUITES = { # Performance targets (mean time in microseconds) PERFORMANCE_TARGETS = { - "test_compress_100_items": 2000, # 2ms - "test_compress_1000_items": 10000, # 10ms - "test_compress_10000_items": 100000, # 100ms - "test_date_extraction": 1000, # 1ms - "test_hash_computation": 500, # 0.5ms - "test_window_50_turns": 5000, # 5ms - "test_window_200_turns": 20000, # 20ms - "test_single_item": 100, # 0.1ms - "test_batch_100": 1000, # 1ms - "test_batch_1000": 10000, # 10ms - "test_pipeline_simple": 5000, # 5ms - "test_pipeline_agentic": 30000, # 30ms - "test_pipeline_rag": 50000, # 50ms + "test_compress_100_items": 2000, # 2ms + "test_compress_1000_items": 10000, # 10ms + "test_compress_10000_items": 100000, # 100ms + "test_date_extraction": 1000, # 1ms + "test_hash_computation": 500, # 0.5ms + "test_window_50_turns": 5000, # 5ms + "test_window_200_turns": 20000, # 20ms + "test_single_item": 100, # 0.1ms + "test_batch_100": 1000, # 1ms + "test_batch_1000": 10000, # 10ms + "test_pipeline_simple": 5000, # 5ms + "test_pipeline_agentic": 30000, # 30ms + "test_pipeline_rag": 50000, # 50ms } @@ -243,8 +242,8 @@ def generate_markdown_report( if total > 0: lines.append("## Summary") lines.append("") - lines.append(f"- **Passed**: {passed}/{total} ({100*passed/total:.0f}%)") - lines.append(f"- **Failed**: {failed}/{total} ({100*failed/total:.0f}%)") + lines.append(f"- **Passed**: {passed}/{total} ({100 * passed / total:.0f}%)") + lines.append(f"- **Failed**: {failed}/{total} ({100 * failed / total:.0f}%)") lines.append("") # Performance notes @@ -274,9 +273,9 @@ def _format_time(microseconds: float) -> str: if microseconds < 1000: return f"{microseconds:.1f}us" elif microseconds < 1_000_000: - return f"{microseconds/1000:.2f}ms" + return f"{microseconds / 1000:.2f}ms" else: - return f"{microseconds/1_000_000:.2f}s" + return f"{microseconds / 1_000_000:.2f}s" def main() -> int: diff --git a/benchmarks/scenarios/__init__.py b/benchmarks/scenarios/__init__.py index b2750c986..ef26869eb 100644 --- a/benchmarks/scenarios/__init__.py +++ b/benchmarks/scenarios/__init__.py @@ -8,16 +8,16 @@ Modules: conversations: Generators for conversation history (agentic, RAG) """ +from .conversations import ( + generate_agentic_conversation, + generate_rag_conversation, +) from .tool_outputs import ( generate_api_responses, generate_database_rows, generate_log_entries, generate_search_results, ) -from .conversations import ( - generate_agentic_conversation, - generate_rag_conversation, -) __all__ = [ "generate_search_results", diff --git a/benchmarks/scenarios/conversations.py b/benchmarks/scenarios/conversations.py index 39639902c..82de43388 100644 --- a/benchmarks/scenarios/conversations.py +++ b/benchmarks/scenarios/conversations.py @@ -53,19 +53,23 @@ def generate_agentic_conversation( messages = [] # System prompt - messages.append({ - "role": "system", - "content": _generate_system_prompt(), - }) + messages.append( + { + "role": "system", + "content": _generate_system_prompt(), + } + ) # Generate turns for turn_idx in range(turns): # User message user_query = _generate_user_query(turn_idx) - messages.append({ - "role": "user", - "content": user_query, - }) + messages.append( + { + "role": "user", + "content": user_query, + } + ) # Assistant with tool calls num_calls = max(1, tool_calls_per_turn + random.randint(-1, 1)) @@ -75,20 +79,24 @@ def generate_agentic_conversation( tool_name, arguments = _generate_tool_call(turn_idx, call_idx) call_id = f"call_{uuid.uuid4().hex[:16]}" - tool_calls.append({ - "id": call_id, - "type": "function", - "function": { - "name": tool_name, - "arguments": json.dumps(arguments), - }, - }) + tool_calls.append( + { + "id": call_id, + "type": "function", + "function": { + "name": tool_name, + "arguments": json.dumps(arguments), + }, + } + ) - messages.append({ - "role": "assistant", - "content": None, - "tool_calls": tool_calls, - }) + messages.append( + { + "role": "assistant", + "content": None, + "tool_calls": tool_calls, + } + ) # Tool responses for tool_call in tool_calls: @@ -96,18 +104,22 @@ def generate_agentic_conversation( tool_call["function"]["name"], items_per_tool_response, ) - messages.append({ - "role": "tool", - "tool_call_id": tool_call["id"], - "content": json.dumps(tool_response), - }) + messages.append( + { + "role": "tool", + "tool_call_id": tool_call["id"], + "content": json.dumps(tool_response), + } + ) # Assistant summary (most turns, not all) if random.random() < 0.8: - messages.append({ - "role": "assistant", - "content": _generate_assistant_summary(turn_idx, tool_calls), - }) + messages.append( + { + "role": "assistant", + "content": _generate_assistant_summary(turn_idx, tool_calls), + } + ) return messages @@ -137,39 +149,49 @@ def generate_rag_conversation( messages = [] # System prompt with date (for CacheAligner testing) - messages.append({ - "role": "system", - "content": _generate_rag_system_prompt(), - }) + messages.append( + { + "role": "system", + "content": _generate_rag_system_prompt(), + } + ) # Generate context documents context_content = _generate_rag_context(context_tokens) # Inject context as first user message - messages.append({ - "role": "user", - "content": f"Here are the relevant documents for context:\n\n{context_content}\n\nPlease analyze these documents.", - }) + messages.append( + { + "role": "user", + "content": f"Here are the relevant documents for context:\n\n{context_content}\n\nPlease analyze these documents.", + } + ) # Assistant acknowledgment - messages.append({ - "role": "assistant", - "content": "I've reviewed the provided documents. I can see information about technical documentation, API specifications, and configuration guides. What would you like to know?", - }) + messages.append( + { + "role": "assistant", + "content": "I've reviewed the provided documents. I can see information about technical documentation, API specifications, and configuration guides. What would you like to know?", + } + ) # Generate Q&A turns for i in range(num_queries): question = _generate_rag_question(i) - messages.append({ - "role": "user", - "content": question, - }) + messages.append( + { + "role": "user", + "content": question, + } + ) answer = _generate_rag_answer(i) - messages.append({ - "role": "assistant", - "content": answer, - }) + messages.append( + { + "role": "assistant", + "content": answer, + } + ) return messages @@ -195,17 +217,21 @@ def generate_anthropic_agentic_conversation( messages = [] # System message (Anthropic uses separate system parameter, but we include it) - messages.append({ - "role": "system", - "content": _generate_system_prompt(), - }) + messages.append( + { + "role": "system", + "content": _generate_system_prompt(), + } + ) for turn_idx in range(turns): # User message - messages.append({ - "role": "user", - "content": [{"type": "text", "text": _generate_user_query(turn_idx)}], - }) + messages.append( + { + "role": "user", + "content": [{"type": "text", "text": _generate_user_query(turn_idx)}], + } + ) # Assistant with tool_use blocks num_calls = max(1, tool_calls_per_turn + random.randint(-1, 1)) @@ -215,17 +241,21 @@ def generate_anthropic_agentic_conversation( tool_name, arguments = _generate_tool_call(turn_idx, call_idx) tool_use_id = f"toolu_{uuid.uuid4().hex[:16]}" - content_blocks.append({ - "type": "tool_use", - "id": tool_use_id, - "name": tool_name, - "input": arguments, - }) + content_blocks.append( + { + "type": "tool_use", + "id": tool_use_id, + "name": tool_name, + "input": arguments, + } + ) - messages.append({ - "role": "assistant", - "content": content_blocks, - }) + messages.append( + { + "role": "assistant", + "content": content_blocks, + } + ) # Tool results in user message tool_results = [] @@ -234,29 +264,38 @@ def generate_anthropic_agentic_conversation( block["name"], items_per_tool_response, ) - tool_results.append({ - "type": "tool_result", - "tool_use_id": block["id"], - "content": json.dumps(tool_response), - }) + tool_results.append( + { + "type": "tool_result", + "tool_use_id": block["id"], + "content": json.dumps(tool_response), + } + ) - messages.append({ - "role": "user", - "content": tool_results, - }) + messages.append( + { + "role": "user", + "content": tool_results, + } + ) # Assistant response if random.random() < 0.8: - messages.append({ - "role": "assistant", - "content": [{"type": "text", "text": _generate_assistant_summary(turn_idx, [])}], - }) + messages.append( + { + "role": "assistant", + "content": [ + {"type": "text", "text": _generate_assistant_summary(turn_idx, [])} + ], + } + ) return messages # Helper functions + def _generate_system_prompt() -> str: """Generate a realistic system prompt.""" return """You are an AI assistant with access to various tools for searching, querying, and analyzing data. diff --git a/benchmarks/scenarios/tool_outputs.py b/benchmarks/scenarios/tool_outputs.py index b01efdfbd..9cc46c276 100644 --- a/benchmarks/scenarios/tool_outputs.py +++ b/benchmarks/scenarios/tool_outputs.py @@ -14,9 +14,7 @@ and compression strategies. from __future__ import annotations -import json import random -import string import uuid from datetime import datetime, timedelta from typing import Any @@ -80,12 +78,14 @@ def generate_search_results( min(include_errors, len(results) - len(needle_indices)), ) for idx in error_indices: - results[idx]["error"] = random.choice([ - "Index out of range", - "Document not found", - "Permission denied", - "Timeout exceeded", - ]) + results[idx]["error"] = random.choice( + [ + "Index out of range", + "Document not found", + "Permission denied", + "Timeout exceeded", + ] + ) results[idx]["status"] = "failed" return results @@ -177,7 +177,9 @@ def generate_log_entries( # Add exception info for errors if level in ("ERROR", "CRITICAL"): entry["exception"] = { - "type": random.choice(["TimeoutError", "ConnectionError", "ValueError", "RuntimeError"]), + "type": random.choice( + ["TimeoutError", "ConnectionError", "ValueError", "RuntimeError"] + ), "message": message, "stacktrace": _generate_stacktrace(), } @@ -273,11 +275,13 @@ def generate_database_rows( elif table_type == "transactions": row = _generate_transaction_row(i) else: # mixed - generator = random.choice([ - _generate_user_row, - lambda i: _generate_metric_row(i, mean_value, std_value), - _generate_transaction_row, - ]) + generator = random.choice( + [ + _generate_user_row, + lambda i: _generate_metric_row(i, mean_value, std_value), + _generate_transaction_row, + ] + ) row = generator(i) rows.append(row) @@ -296,6 +300,7 @@ def generate_database_rows( # Helper functions + def _generate_title() -> str: """Generate a realistic document title.""" prefixes = ["How to", "Guide to", "Understanding", "Introduction to", "Advanced"] @@ -326,7 +331,9 @@ def _generate_name() -> str: def _generate_timestamp(offset_days: int = 0) -> str: """Generate an ISO timestamp.""" base = datetime(2025, 1, 1, 12, 0, 0) - dt = base + timedelta(days=offset_days, hours=random.randint(0, 23), minutes=random.randint(0, 59)) + dt = base + timedelta( + days=offset_days, hours=random.randint(0, 23), minutes=random.randint(0, 59) + ) return dt.isoformat() + "Z" diff --git a/docs/HEADROOM_DEEP_ANALYSIS.md b/docs/HEADROOM_DEEP_ANALYSIS.md new file mode 100644 index 000000000..910d8c1e0 --- /dev/null +++ b/docs/HEADROOM_DEEP_ANALYSIS.md @@ -0,0 +1,914 @@ +# Headroom: A Critical Technical Analysis + +## Table of Contents +1. [Part I: Critical Startup Evaluation](#part-i-critical-startup-evaluation) +2. [Part II: Technical Pitch](#part-ii-technical-pitch) +3. [Part III: Technical Blog Post - State of the Art Comparison](#part-iii-technical-blog-post) + +--- + +# Part I: Critical Startup Evaluation + +## Executive Summary + +**Headroom** is a context optimization layer for LLM applications that compresses tool outputs using statistical analysis rather than LLM-based summarization. The core value proposition: **50-90% token savings without accuracy loss**. + +### The Honest Assessment + +| Dimension | Score | Assessment | +|-----------|-------|------------| +| Technical Differentiation | 7/10 | Novel CCR architecture, but heuristics have limits | +| Market Timing | 9/10 | AI agent explosion = massive demand for context optimization | +| Defensibility | 6/10 | Network effects possible via feedback loop, but easy to replicate basics | +| Scalability Risk | 7/10 | Works for ~70% of scenarios; fails silently on 30% | +| Business Model Clarity | 8/10 | Clear proxy/SDK model, usage-based pricing | + +--- + +## The Problem Space: Is It Real? + +### Quantified Pain + +| Metric | Reality | +|--------|---------| +| Average tool output size | 5,000-50,000 tokens | +| Context utilization | 60-80% is tool outputs | +| Cache hit rate (without optimization) | <10% | +| Monthly spend for AI coding agents | $500-$5,000/developer | + +**Evidence from research:** +- [Factory.ai](https://factory.ai/news/evaluating-compression): "OpenAI achieved 99.3% compression but scored 0.35 points lower on quality. Those discarded details required re-fetching, negating token savings." +- [Phil Schmid](https://www.philschmid.de/context-engineering-part-2): "Mechanically stuffing lengthy text into an LLM's context window is a 'brute-force' strategy that inevitably scatters the model's attention." + +**Verdict: The problem is REAL and GROWING.** + +--- + +## Technical Differentiation: What's Actually Novel? + +### What Headroom Does + +1. **Statistical Compression** (SmartCrusher) + - Analyzes field distributions (entropy, variance, uniqueness) + - Detects data patterns (time series, logs, search results) + - Preserves errors, anomalies, and high-relevance items + - **No LLM calls** = deterministic, fast, cheap + +2. **Reversible Compression** (CCR - Compress-Cache-Retrieve) + - Original content cached for on-demand retrieval + - LLM can request more data if needed + - Feedback loop learns from retrieval patterns + - **Unique position**: Only Headroom sits between tools and LLMs + +3. **Cache Alignment** + - Stabilizes dynamic content (dates, IDs) for provider cache hits + - Can increase cache utilization from <10% to >50% + +### What's Actually Novel vs. Prior Art + +| Approach | Novelty | Prior Art | +|----------|---------|-----------| +| Statistical field analysis | **Medium** | Data profiling tools exist, but not for LLM context | +| CCR architecture | **High** | ACON mentions "reversible" but doesn't implement caching | +| Feedback-driven hints | **High** | ACON-inspired, but applied at proxy layer | +| BM25/embedding relevance | **Low** | Standard IR techniques | +| Cache prefix alignment | **Low** | Multiple implementations exist | + +**Honest assessment**: The individual techniques are not revolutionary. The **combination and positioning** (proxy layer for AI agents) is the innovation. + +--- + +## The Fundamental Limitation + +### The Accuracy Problem + +Headroom uses **task-agnostic heuristics**: +- Keep first 3, last 2 items +- Keep errors (keyword matching) +- Keep anomalies (> 2σ from mean) +- Keep relevant items (BM25/embedding to user query) + +**When this works:** +- Data has explicit importance signals (score fields, error flags) +- Interesting items are statistical outliers +- User query matches data vocabulary + +**When this fails:** +``` +User asks: "Find all orders from California" +Tool returns: 1,000 orders +SmartCrusher keeps: errors, anomalies, first/last items +The needle: Order #47 from California (looks completely normal) +Result: INFORMATION LOSS +``` + +### Quantified Risk + +| Scenario | Coverage | Confidence | +|----------|----------|------------| +| Search results with scores | 95%+ | HIGH | +| Logs with errors | 90%+ | HIGH | +| Time series with anomalies | 85%+ | HIGH | +| **Entity listings (users, orders)** | **60%** | **LOW** | +| **Specific lookups** | **50%** | **LOW** | +| **Exhaustive queries** | **40%** | **LOW** | + +**The 70/30 split**: Headroom works well for ~70% of real-world tool outputs. The other 30% require either: +1. Skipping compression (crushability detection helps here) +2. Accepting potential information loss +3. Relying on CCR retrieval as fallback + +--- + +## Competitive Landscape + +### Direct Competitors + +| Competitor | Approach | Pros | Cons | +|------------|----------|------|------| +| **LLMLingua** (Microsoft) | Token-level compression via classifier | 95-98% accuracy retention | Requires model, wrong granularity for JSON | +| **ACON** (Research) | Task-aware, failure-driven | Best accuracy | Requires agent integration | +| **Selective Context** (Amazon) | Self-attention based filtering | Model-aware | Slow, requires LLM | +| **Context Caching** (Anthropic/OpenAI) | Provider-level caching | Native integration | No compression | + +### Why Headroom Can Win + +1. **Position**: Proxy layer = works with any client +2. **Speed**: No LLM calls = <10ms overhead +3. **Safety**: CCR = reversible compression +4. **Learning**: Feedback loop improves over time + +### Why Headroom Might Lose + +1. **Provider integration**: If Anthropic/OpenAI add smart compression natively +2. **Agent framework capture**: LangChain/LlamaIndex could add similar features +3. **Research advances**: If ACON-style task-aware compression becomes easy + +--- + +## Business Model Analysis + +### Revenue Model + +``` +Free Tier: + - Local proxy (unlimited) + - Basic compression + - No cloud features + +Pro Tier ($49/month): + - Hosted proxy + - Feedback-driven optimization + - Analytics dashboard + +Enterprise: + - Custom deployment + - SLA guarantees + - Integration support +``` + +### Unit Economics + +| Metric | Value | +|--------|-------| +| Average token savings | 70% | +| Average monthly spend per developer | $1,000 | +| Potential savings | $700/month | +| Headroom Pro price | $49/month | +| **Value capture** | **7%** | + +**Problem**: 7% value capture is low. Competitors could undercut easily. + +### Moat-Building Strategies + +1. **Network effect via feedback**: Cross-user learning improves compression +2. **Tool-specific profiles**: Accumulated knowledge of tool output patterns +3. **Integration depth**: Deep embedding in agent frameworks +4. **Enterprise stickiness**: Once deployed in production, hard to replace + +--- + +## Risk Assessment + +### Technical Risks + +| Risk | Probability | Impact | Mitigation | +|------|-------------|--------|------------| +| Compression causes critical info loss | Medium | High | CCR + crushability detection | +| Provider adds native compression | Medium | High | Position as multi-provider layer | +| LLMLingua improves for JSON | Low | Medium | Focus on proxy positioning | + +### Market Risks + +| Risk | Probability | Impact | Mitigation | +|------|-------------|--------|------------| +| Context windows grow so large compression isn't needed | Low | High | Focus on cost (always relevant) | +| Agent frameworks internalize compression | Medium | High | Integrate with frameworks | +| Open source competitor emerges | High | Medium | Build network effects fast | + +--- + +## Strategic Recommendations + +### Short-Term (0-6 months) +1. **Ship CCR**: Reversible compression is the key differentiator +2. **Prove accuracy**: Publish benchmarks showing 0% information loss +3. **Integrate with frameworks**: LangChain, LlamaIndex, CrewAI + +### Medium-Term (6-18 months) +1. **Build network effects**: Cross-user feedback learning +2. **Tool-specific profiles**: Curated compression strategies per tool +3. **Enterprise pilots**: Get deployed in production AI agents + +### Long-Term (18+ months) +1. **Platform play**: Become the "context layer" for AI applications +2. **Data flywheel**: Best compression because most data +3. **Research integration**: Adopt ACON-style task-aware learning + +--- + +## Verdict + +**Headroom is a viable startup idea with clear technical merit but significant execution risk.** + +| Criterion | Score | Notes | +|-----------|-------|-------| +| Problem validity | 9/10 | Token costs are real and growing | +| Solution fit | 7/10 | Works for 70% of cases; CCR addresses rest | +| Technical moat | 6/10 | Easy to replicate basics; network effects need scale | +| Market timing | 9/10 | AI agent explosion is happening now | +| Execution risk | 7/10 | Moderate; need to prove accuracy first | + +**Overall**: **7.5/10** - Worth pursuing with clear-eyed awareness of limitations. + +--- + +# Part II: Technical Pitch + +## The 30-Second Pitch + +> "Headroom cuts LLM costs by 50-90% for AI agents. We compress tool outputs using statistical analysis, not LLM summarization - so it's fast, cheap, and deterministic. Our Compress-Cache-Retrieve architecture makes compression reversible: if the LLM needs more, it retrieves instantly. Zero accuracy loss, zero extra API calls." + +--- + +## The Problem (For Technical Audience) + +### The Context Budget Crisis + +Modern AI agents are powerful but expensive: + +```python +# Typical agent workflow +agent.execute("Find and fix the bug in authentication") + +# Behind the scenes: +# 1. Read 20 files (50K tokens) +# 2. Search codebase (10K tokens) +# 3. Run tests (30K tokens) +# 4. Check logs (40K tokens) +# Total: 130K tokens = $0.65 per request (GPT-4o) +``` + +**The math doesn't work**: +- 100 requests/day × $0.65 = $65/day = **$1,950/month** per developer +- 80% of those tokens are tool outputs +- 70% of tool output is redundant + +### Why Current Solutions Fail + +| Approach | Problem | +|----------|---------| +| **Truncation** | Loses end of data (where errors often are) | +| **LLM Summarization** | Slow (2-5s), expensive, can hallucinate | +| **Provider caching** | Doesn't reduce input size | +| **Longer context windows** | Doesn't reduce cost | + +--- + +## The Solution: Statistical Context Compression + +### Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ YOUR APPLICATION │ +│ (Claude Code, LangChain Agent, Custom Agent) │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ HEADROOM PROXY │ +│ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ SMART CRUSHER │ │ +│ │ │ │ +│ │ 1. ANALYZE: Field distributions, patterns, signals │ │ +│ │ 2. PRESERVE: Errors, anomalies, relevant items │ │ +│ │ 3. COMPRESS: Statistical sampling, deduplication │ │ +│ │ 4. CACHE: Store original for retrieval (CCR) │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ CACHE ALIGNER │ │ +│ │ Stabilize dynamic content for provider caching │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ FEEDBACK LOOP │ │ +│ │ Learn from retrieval patterns → improve compression │ │ +│ └──────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ OPENAI / ANTHROPIC / GOOGLE API │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Key Innovation: CCR (Compress-Cache-Retrieve) + +**The insight**: Traditional compression is irreversible. If we guess wrong, information is permanently lost. + +**CCR makes compression reversible**: + +``` +BEFORE CCR: + Tool returns 1,000 items → Compress to 20 → Send to LLM + If LLM needs item #47: TOO BAD, IT'S GONE + +AFTER CCR: + Tool returns 1,000 items → Compress to 20 + cache 1,000 + If LLM needs item #47: Retrieve from cache INSTANTLY + + Bonus: Track what LLM retrieves → improve future compression +``` + +### Technical Deep Dive: SmartCrusher + +**Step 1: Field Analysis** +```python +# For each field in the JSON array: +analyze(field) → { + type: "numeric" | "string" | "boolean" | "array", + unique_ratio: 0.0-1.0, # How many unique values + entropy: 0.0-1.0, # Randomness (high = IDs) + variance: float, # For numerics + change_points: [int], # Where values spike +} +``` + +**Step 2: Pattern Detection** +```python +# Classify the data structure: +if has_timestamp_field and has_numeric_variance: + pattern = "time_series" +elif has_message_field and has_level_field: + pattern = "logs" +elif has_score_field: + pattern = "search_results" +else: + pattern = "generic" +``` + +**Step 3: Strategy Selection** +```python +strategies = { + "time_series": keep_change_points + sample_stable_regions, + "logs": cluster_by_message + keep_one_per_cluster, + "search_results": sort_by_score + keep_top_n, + "generic": keep_first_k + keep_last_k + keep_anomalies +} +``` + +**Step 4: Compression with Safety** +```python +# Always preserve: +- Items with error keywords (error, exception, failed, critical) +- Items > 2σ from mean (anomalies) +- Items matching user query (BM25 + embeddings) +- First K and last K items (context + recency) + +# Crushability detection: +if high_uniqueness and no_importance_signal: + return SKIP # Don't compress, too risky +``` + +--- + +## Benchmarks + +### Real-World Performance + +| Scenario | Before | After | Savings | Quality | +|----------|--------|-------|---------|---------| +| Search results (1,000 items) | 45K tokens | 4.5K tokens | 90% | 100% | +| Log analysis (500 entries) | 22K tokens | 3.3K tokens | 85% | 100% | +| API responses (nested JSON) | 15K tokens | 2.3K tokens | 85% | 100% | +| SRE incident investigation | 22K tokens | 2.2K tokens | 90% | 100% | + +### Adversarial Testing + +We ran 36 adversarial tests designed to break assumptions: + +| Category | Tests | Passed | +|----------|-------|--------| +| Semantic Attacks | 6 | 6/6 | +| Boundary Conditions | 6 | 6/6 | +| Injection Attacks | 3 | 3/3 | +| Race Conditions | 4 | 4/4 | +| Deceptive Data | 2 | 2/2 | +| Extreme Stress Tests | 15 | 15/15 | + +**Tests included**: +- NaN/Infinity score fields +- 100-level deep nesting +- 100,000 item arrays +- Catastrophic regex patterns +- Unicode normalization attacks +- Concurrent feedback race conditions + +--- + +## Comparison to State of the Art + +### vs. LLMLingua (Microsoft Research) + +| Dimension | LLMLingua | Headroom | +|-----------|-----------|----------| +| Compression unit | Tokens | JSON items | +| Requires model | Yes (XLM-RoBERTa) | No | +| Latency | 50-200ms | <10ms | +| Task-aware | No | Partial (via feedback) | +| Reversible | No | Yes (CCR) | +| Best for | Natural language | Structured tool outputs | + +**LLMLingua paper**: "Achieves 3-6x compression with 95-98% accuracy retention." +**Headroom**: Achieves 5-10x compression on JSON with 100% accuracy (no loss, just sampling). + +### vs. ACON (Agent Context Optimization) + +| Dimension | ACON | Headroom | +|-----------|------|----------| +| Compression method | Task-aware, failure-driven | Statistical + feedback | +| Integration point | Agent framework | Proxy layer | +| Learning | Contrastive feedback | Retrieval patterns | +| Deployment | Research prototype | Production-ready | +| Reversibility | Mentioned but not implemented | Full CCR | + +**ACON insight we adopted**: Learn compression guidelines by analyzing failures. +**What we added**: Reversible compression (CCR) so "failure" is recoverable. + +### vs. Provider Caching (Anthropic, OpenAI) + +| Dimension | Provider Caching | Headroom | +|-----------|------------------|----------| +| What it does | Cache exact prefix matches | Compress + stabilize prefix | +| Token reduction | 0% | 50-90% | +| Cache hit improvement | ~10% baseline | Can improve to 50%+ | +| Cost | Free | Overhead of proxy | + +**Complementary, not competitive**: Headroom improves cache hit rates by stabilizing prefixes. + +--- + +## Integration + +### Option 1: Proxy (Drop-in) + +```bash +pip install headroom +headroom proxy --port 8787 + +# Use with any client +ANTHROPIC_BASE_URL=http://localhost:8787 claude +OPENAI_BASE_URL=http://localhost:8787/v1 your-app +``` + +### Option 2: Python SDK + +```python +from headroom import HeadroomClient +from openai import OpenAI + +client = HeadroomClient( + original_client=OpenAI(), + default_mode="optimize", +) + +# Use exactly like original - compression happens automatically +response = client.chat.completions.create( + model="gpt-4o", + messages=[...], +) +``` + +### Option 3: LangChain + +```python +from langchain_openai import ChatOpenAI +from headroom.integrations import HeadroomOptimizer + +llm = ChatOpenAI(model="gpt-4o", callbacks=[HeadroomOptimizer()]) +``` + +--- + +## Pricing + +| Tier | Price | Features | +|------|-------|----------| +| Open Source | Free | Local proxy, basic compression | +| Pro | $49/month | Hosted proxy, feedback learning, analytics | +| Enterprise | Custom | On-prem, SLA, dedicated support | + +**ROI Calculator**: +- If you spend $1,000/month on LLM API +- Headroom saves 70% = $700/month +- Pro costs $49/month +- **Net savings: $651/month (14x ROI)** + +--- + +# Part III: Technical Blog Post + +# Reversible Compression for AI Agents: How CCR Solves What LLMLingua Can't + +*A deep technical comparison of context compression approaches* + +--- + +## The Compression Dilemma + +Every AI agent builder faces the same problem: tool outputs are huge, context windows are expensive, and throwing data away risks breaking your agent. + +The research community has proposed several solutions: +- **LLMLingua** (Microsoft): Token-level compression using a classifier +- **Selective Context** (Amazon): Attention-based filtering +- **ACON** (UC Berkeley): Task-aware, failure-driven optimization + +But there's a fundamental problem none of them solve: **compression is irreversible**. + +If you compress 1,000 search results to 20 and the LLM needs result #47, it's gone. You've created a silent failure mode that's hard to detect and impossible to recover from. + +**This post introduces CCR (Compress-Cache-Retrieve)**, an architecture that makes compression reversible. We'll compare it to state-of-the-art approaches and show why reversibility changes everything. + +--- + +## Part 1: The State of the Art + +### LLMLingua: Token-Level Compression + +[LLMLingua](https://arxiv.org/abs/2310.05736) and its successor [LLMLingua-2](https://arxiv.org/abs/2403.12968) achieve impressive compression ratios (3-6x) while retaining 95-98% of information. + +**How it works**: +1. Train a classifier (XLM-RoBERTa or similar) to predict token importance +2. At inference, score each token +3. Drop low-importance tokens + +**Example**: +``` +Input: "The quick brown fox jumps over the lazy dog" +Output: "quick brown fox jumps lazy dog" (30% compression) +``` + +**Strengths**: +- Works on any text +- High accuracy retention +- No task-specific training + +**Weaknesses for AI agents**: +1. **Wrong granularity**: Agents work with JSON arrays, not prose +2. **Requires a model**: Adds latency (50-200ms) and dependency +3. **Irreversible**: If the classifier is wrong, data is lost +4. **Not structure-aware**: Can't reason about "first 3 items" or "items with errors" + +### ACON: Task-Aware, Failure-Driven Optimization + +[ACON](https://arxiv.org/abs/2510.00615) takes a different approach: learn what to compress by analyzing task failures. + +**How it works**: +1. Compress aggressively +2. If task fails, analyze what was lost +3. Update compression guidelines +4. Repeat (contrastive learning) + +**Key insight from the paper**: +> "Rather than crude strategies like 'keep recent K interactions' (FIFO), ACON employs task-aware, failure-driven optimization. The system learns environment-specific and task-specific compression patterns." + +**Strengths**: +- Task-aware decisions +- 95%+ accuracy retention +- Learns from failures + +**Weaknesses**: +1. **Requires agent integration**: Must observe task outcomes +2. **Cold start problem**: Need failures to learn +3. **Still irreversible**: Failure = data was lost +4. **Research prototype**: Not production-ready + +### Selective Context: Attention-Based Filtering + +[Selective Context](https://arxiv.org/abs/2310.06201) uses the LLM's own attention to decide what's important. + +**How it works**: +1. Run a forward pass with a smaller model +2. Observe attention patterns +3. Keep tokens that receive high attention + +**Strengths**: +- Model-native importance signal +- Works without training + +**Weaknesses**: +1. **Requires forward pass**: Slow and expensive +2. **Task-agnostic**: Doesn't know what the user will ask +3. **Irreversible**: Same fundamental problem + +--- + +## Part 2: The Reversibility Problem + +### Why Irreversible Compression Fails + +Consider this scenario: + +```python +# User query +"Find all orders from California and calculate total revenue" + +# Tool output: 1,000 orders (50KB) +[ + {"id": 1, "state": "NY", "amount": 100}, + {"id": 2, "state": "TX", "amount": 200}, + ... + {"id": 47, "state": "CA", "amount": 500}, # ← NEEDLE + ... + {"id": 1000, "state": "FL", "amount": 150} +] + +# LLMLingua compression: Keep "important" tokens +# Result: Loses order #47 because it looks like every other order + +# ACON compression: Keep based on learned patterns +# Result: Might keep errors, might keep high amounts, but no signal for "CA" + +# Selective Context: Keep high-attention tokens +# Result: User hasn't asked yet, so no attention signal for "CA" +``` + +**The fundamental problem**: At compression time, we don't know what the LLM will need. All existing approaches guess - and guessing wrong is permanent. + +### The Research Acknowledges This + +From [Factory.ai's analysis](https://factory.ai/news/evaluating-compression): +> "Compression ratio turned out to be the wrong metric entirely. OpenAI achieved 99.3% compression but scored 0.35 points lower on quality. Those discarded details required re-fetching, negating token savings." + +From [Phil Schmid](https://www.philschmid.de/context-engineering-part-2): +> "Prefer raw > Compaction > Summarization only when compaction no longer yields enough space. Compaction (Reversible) strips out information that is redundant because it exists in the environment." + +The insight is clear: **reversible compression beats irreversible compression**. + +--- + +## Part 3: Introducing CCR (Compress-Cache-Retrieve) + +### The Architecture + +CCR makes compression reversible by caching original content for on-demand retrieval: + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ TOOL OUTPUT (1000 items) │ +└────────────────────────┬─────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────────┐ +│ CCR LAYER │ +│ │ +│ 1. COMPRESS: Statistical analysis → keep 20 important items │ +│ 2. CACHE: Store all 1000 items in fast local cache (5min TTL) │ +│ 3. INJECT: Tell LLM how to retrieve more if needed │ +│ │ +│ Output to LLM: │ +│ [20 items shown + "retrieve_compressed(hash='abc123') for more"]│ +└────────────────────────┬─────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────────┐ +│ LLM PROCESSING │ +│ │ +│ Scenario A: 20 items sufficient → Answer directly │ +│ Scenario B: Need item #47 → retrieve_compressed("state:CA") │ +│ → CCR returns matching items from cache instantly │ +└────────────────────────┬─────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────────┐ +│ FEEDBACK LOOP │ +│ │ +│ Track: 30% of search_api compressions trigger retrieval │ +│ Learn: "For search_api, keep items matching state field" │ +│ Improve: Next compression is smarter │ +└──────────────────────────────────────────────────────────────────┘ +``` + +### The Key Components + +#### 1. Statistical Compression (SmartCrusher) + +Instead of token-level classification, we analyze JSON structure: + +```python +# Field analysis +{ + "id": {"unique_ratio": 1.0, "type": "identifier"}, + "state": {"unique_ratio": 0.05, "type": "categorical"}, + "amount": {"variance": 8500, "change_points": [47, 203]} +} + +# Strategy selection +if has_score_field: + strategy = "top_n_by_score" +elif has_variance_spikes: + strategy = "time_series" +elif has_error_keywords: + strategy = "preserve_errors" +else: + strategy = "smart_sample" +``` + +**Always preserved**: +- Error items (keyword matching: error, exception, failed, critical) +- Anomalies (> 2σ from mean) +- High-relevance items (BM25 + embedding similarity to user query) +- First K and last K (context and recency) + +#### 2. Compression Store + +```python +@dataclass +class CompressionEntry: + hash: str # 16-char SHA256 + original_content: str # Full JSON + compressed_content: str + original_item_count: int + compressed_item_count: int + tool_name: str | None + created_at: float + ttl: int = 300 # 5 minute default +``` + +**Features**: +- Thread-safe in-memory storage +- TTL-based expiration +- LRU eviction +- BM25 search within cached content + +#### 3. Retrieval API + +```python +# Full retrieval +POST /v1/retrieve +{"hash": "abc123"} + +# Filtered retrieval (BM25 search) +POST /v1/retrieve +{"hash": "abc123", "query": "state:CA"} +``` + +#### 4. Feedback Loop + +```python +@dataclass +class ToolPattern: + tool_name: str + total_compressions: int + total_retrievals: int + retrieval_rate: float # retrievals / compressions + common_queries: dict[str, int] # What users search for + queried_fields: dict[str, int] # Which fields matter +``` + +**Feedback-driven hints**: +```python +if retrieval_rate > 0.5: + # Compressing too aggressively + hints.max_items = 50 + hints.aggressiveness = 0.3 +elif retrieval_rate > 0.8 and full_retrieval_rate > 0.8: + # Data is unique, don't compress + hints.skip_compression = True +else: + # Current compression is working + hints.max_items = 15 +``` + +--- + +## Part 4: Comparison Matrix + +| Dimension | LLMLingua | ACON | Selective Context | CCR (Headroom) | +|-----------|-----------|------|-------------------|----------------| +| **Compression unit** | Tokens | Task-specific | Tokens | JSON items | +| **Requires model** | Yes (classifier) | Yes (LLM) | Yes (attention) | No | +| **Latency added** | 50-200ms | 100-500ms | 100-300ms | <10ms | +| **Task-aware** | No | Yes | No | Partial (feedback) | +| **Reversible** | No | No | No | **Yes** | +| **Learns from failures** | No | Yes | No | Yes (via retrieval) | +| **Production-ready** | Research | Research | Research | **Yes** | +| **Best for** | Natural language | Specific agent tasks | General | Structured tool outputs | + +### The Key Differentiator: Reversibility + +| Scenario | LLMLingua | ACON | CCR | +|----------|-----------|------|-----| +| Compression is right | ✅ Saves tokens | ✅ Saves tokens | ✅ Saves tokens | +| Compression is wrong | ❌ Permanent loss | ❌ Permanent loss | ✅ Retrieve from cache | +| Learning signal | None | Task failure | Retrieval patterns | + +--- + +## Part 5: Real-World Results + +### Benchmark: SRE Incident Investigation + +**Scenario**: Agent investigates production incident using 5 tool calls. + +| Tool | Original Tokens | Compressed | Savings | +|------|-----------------|------------|---------| +| Get metrics | 8,000 | 800 | 90% | +| Search logs | 6,000 | 900 | 85% | +| Check status | 4,000 | 600 | 85% | +| List deployments | 2,500 | 500 | 80% | +| Get runbook | 1,500 | 400 | 73% | +| **Total** | **22,000** | **3,200** | **85%** | + +**Quality**: Agent correctly identified CPU spike, referenced error rates, provided remediation commands. No information loss. + +### Adversarial Testing + +We tested CCR against 36 adversarial scenarios: + +| Category | Example | Result | +|----------|---------|--------| +| **Edge cases** | NaN/Infinity scores | ✅ Handled (filtered) | +| **Scale** | 100,000 items | ✅ <50ms compression | +| **Concurrency** | 50 threads updating feedback | ✅ Thread-safe | +| **Injection** | Null bytes in field names | ✅ Safe handling | +| **Deception** | Misleading score fields | ✅ Keyword detection saves critical items | + +--- + +## Part 6: When to Use What + +### Use LLMLingua When: +- Compressing natural language prompts +- Need general-purpose compression +- Can tolerate 50-200ms latency +- Accuracy > 95% is acceptable + +### Use ACON When: +- Building task-specific agents +- Have clear success/failure signals +- Can integrate at framework level +- Willing to accept cold-start learning + +### Use CCR (Headroom) When: +- Working with tool outputs (JSON arrays) +- Need <10ms latency +- Can't afford ANY information loss +- Want compression that learns and improves +- Need production-ready solution today + +--- + +## Conclusion + +The compression research community has made impressive progress, but all existing approaches share a fundamental flaw: **irreversibility**. + +CCR solves this by making compression a **provisioning decision**, not a **deletion decision**. The original data exists; we're just choosing what to surface first. + +This changes the trade-off: +- **Before**: Compress aggressively = risk information loss +- **After**: Compress aggressively = LLM might need one extra retrieval + +When retrieval is instantaneous (local cache), the risk/reward calculus shifts entirely in favor of aggressive compression. + +The future of context compression isn't about better heuristics. It's about **reversible architectures that learn from actual needs**. + +--- + +## Resources + +- [LLMLingua Paper](https://arxiv.org/abs/2310.05736) +- [LLMLingua-2 Paper](https://arxiv.org/abs/2403.12968) +- [ACON Paper](https://arxiv.org/abs/2510.00615) +- [Selective Context Paper](https://arxiv.org/abs/2310.06201) +- [Factory.ai Compression Analysis](https://factory.ai/news/evaluating-compression) +- [Phil Schmid: Context Engineering](https://www.philschmid.de/context-engineering-part-2) +- [Lost in the Middle](https://arxiv.org/abs/2307.03172) +- [RAGFlow: From RAG to Context](https://ragflow.io/blog/rag-review-2025-from-rag-to-context) + +--- + +*This post describes Headroom, an open-source context optimization layer for LLM applications. [GitHub](https://github.com/headroom-sdk/headroom)* diff --git a/docs/HEADROOM_FEATURES.md b/docs/HEADROOM_FEATURES.md new file mode 100644 index 000000000..7976d73f0 --- /dev/null +++ b/docs/HEADROOM_FEATURES.md @@ -0,0 +1,891 @@ +# Headroom: Complete Feature Documentation & Competitive Analysis + +## Executive Summary + +**Headroom is the world's first Context Optimization Layer for LLM applications.** While the industry has focused on routing (LiteLLM), observability (Helicone), and governance (Portkey), no one has solved the fundamental problem: **LLM contexts are bloated with irrelevant data, and this costs money.** + +Headroom reduces LLM costs by 50-70% through intelligent context compression while maintaining 100% retention of critical information (errors, anomalies, relevant items). It's the missing infrastructure layer between your application and LLM providers. + +--- + +# Part 1: Complete Feature Inventory + +## 1. Core Transforms (The "Secret Sauce") + +### 1.1 SmartCrusher - Statistical Array Compression + +**Location**: `headroom/transforms/smart_crusher.py` + +**What It Does**: Compresses large JSON arrays (tool outputs) from 1000s of items to 15-50 items while preserving critical information. + +**The Safe V1 Recipe** - Always preserves: +| Preserved Item Type | Why It Matters | Detection Method | +|---------------------|----------------|------------------| +| First 3 items | Context/headers | Position-based | +| Last 2 items | Recency | Position-based | +| Error items | Critical signals | Keyword matching: `error`, `exception`, `failed`, `failure`, `critical`, `fatal` | +| Numeric anomalies | Outliers matter | Statistical: values > 2σ from mean | +| Change points | Regime shifts | Sliding window variance detection | +| Relevant items | User's needle | BM25/embedding relevance scoring | + +**Algorithm Details**: + +``` +1. ANALYZE: SmartAnalyzer computes per-field statistics + - Uniqueness ratio (unique_count / total_count) + - Numeric stats (min, max, mean, variance) + - Change points (indices where value significantly shifts) + - String stats (avg_length, top values) + +2. DETECT PATTERN: Identifies data type + - TIME_SERIES: Has timestamp + numeric variance + - LOGS: Has message field + level/severity + - SEARCH_RESULTS: Has score/rank field + - GENERIC: Default + +3. PLAN: Creates compression plan based on pattern + - TIME_SERIES → Keep items around change points + - LOGS → Cluster by message, keep representatives + - SEARCH_RESULTS → Keep top N by score + - GENERIC → Smart statistical sampling + +4. EXECUTE: Apply plan with priority override + - If errors/anomalies exceed max_items, KEEP ALL + - Errors are NEVER dropped +``` + +**Change Point Detection Algorithm**: +```python +def detect_change_points(values, window=5): + std_dev = statistics.stdev(values) + threshold = 2.0 * std_dev + + for i in range(window, len(values) - window): + before_mean = mean(values[i-window:i]) + after_mean = mean(values[i:i+window]) + if abs(after_mean - before_mean) > threshold: + mark_as_change_point(i) +``` + +**Configuration Options**: +```python +@dataclass +class SmartCrusherConfig: + enabled: bool = True + min_items_to_analyze: int = 5 # Don't crush tiny arrays + min_tokens_to_crush: int = 200 # Only if > 200 tokens + variance_threshold: float = 2.0 # Std devs for anomaly + uniqueness_threshold: float = 0.1 # < 10% = constant field + similarity_threshold: float = 0.8 # String clustering + max_items_after_crush: int = 15 # Target output size + preserve_change_points: bool = True +``` + +**Performance**: +- 100 items: < 2ms +- 1,000 items: < 10ms +- 10,000 items: < 100ms +- Compression ratio: 50-90% token reduction + +--- + +### 1.5 CCR Architecture - Compress-Cache-Retrieve ⭐ NEW + +**Location**: `headroom/cache/compression_store.py`, `headroom/cache/compression_feedback.py` + +**What It Does**: Makes compression **reversible**. When SmartCrusher compresses, the original data is cached. If the LLM needs more, it retrieves instantly. + +**The Key Innovation**: +> Traditional compression: Guess what's important → Permanent data loss if wrong +> CCR: Compress aggressively → Cache original → Retrieve on demand → Zero permanent loss + +**Four Phases**: + +| Phase | Component | Description | +|-------|-----------|-------------| +| **1. Store** | `CompressionStore` | Cache original content when compressing | +| **2. Retrieve** | `/v1/retrieve` endpoint | On-demand access to original data | +| **3. Inject** | Tool/system injection | Tell LLM how to retrieve more | +| **4. Feedback** | `CompressionFeedback` | Learn from retrieval patterns | + +**CompressionStore Features**: +- Thread-safe in-memory storage +- TTL-based expiration (default 5 minutes) +- LRU-style eviction at capacity +- Built-in BM25 search within cached content +- Hash-based retrieval (16-char SHA256) + +**Feedback Loop Metrics**: +```python +class ToolPattern: + retrieval_rate: float # retrievals / compressions + full_retrieval_rate: float # full_retrievals / total_retrievals + search_rate: float # search_retrievals / total_retrievals + common_queries: dict # Most frequent search queries + queried_fields: dict # Fields mentioned in queries +``` + +**Automatic Adjustment**: +- Retrieval rate >50% → Compress less aggressively (keep 50 items) +- Retrieval rate >80% with full retrievals → Skip compression entirely +- Common query fields → Preserve in future compressions + +**API Endpoints**: +``` +POST /v1/retrieve → Retrieve cached content by hash +GET /v1/feedback → Get all learned patterns +GET /v1/feedback/{tool} → Get hints for specific tool +``` + +**Configuration**: +```python +@dataclass +class SmartCrusherConfig: + use_feedback_hints: bool = True # Enable feedback-driven adjustment + # ... other options +``` + +**Why This is a Moat**: +1. **Reversible**: No permanent information loss +2. **Transparent**: LLM knows it can ask for more +3. **Learning**: Improves over time from actual usage +4. **Zero-Risk**: Worst case = retrieve everything + +--- + +### 1.2 CacheAligner - Prefix Stabilization + +**Location**: `headroom/transforms/cache_aligner.py` + +**What It Does**: Makes your system prompts cache-friendly by extracting dynamic content (dates, timestamps, session IDs) so the static prefix remains byte-identical across requests. + +**Why This Matters**: +- Anthropic: 90% discount on cached tokens +- OpenAI: 50% discount on cached tokens +- Google: 75% discount on cached tokens + +Without CacheAligner: +``` +Request 1: "Today is January 7, 2025. You are helpful." → Hash: abc123 +Request 2: "Today is January 8, 2025. You are helpful." → Hash: def456 (CACHE MISS!) +``` + +With CacheAligner: +``` +Request 1: "You are helpful.\n---\n[Dynamic: January 7, 2025]" → Stable Hash: xyz789 +Request 2: "You are helpful.\n---\n[Dynamic: January 8, 2025]" → Stable Hash: xyz789 (CACHE HIT!) +``` + +**Detection Tiers**: + +| Tier | Method | Latency | Coverage | +|------|--------|---------|----------| +| 1 (Regex) | Pattern matching | ~0ms | ISO dates, UUIDs, timestamps, version numbers | +| 2 (NER) | spaCy entities | ~5-10ms | Names, money, organizations, locations | +| 3 (Semantic) | Embedding similarity | ~20-50ms | Complex dynamic patterns | + +**Tier 1 Patterns** (Universal, no locale dependencies): +- ISO 8601 DateTime: `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}` +- ISO 8601 Date: `\d{4}-\d{2}-\d{2}` +- Unix Timestamp: `\d{10,13}` +- UUID: `[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-...-[0-9a-fA-F]{12}` +- Version: `v\d+\.\d+(?:\.\d+)?` +- Structural: `Label: value` where Label indicates dynamic content + +**Entropy-Based Detection**: +```python +def calculate_entropy(s: str) -> float: + """Shannon entropy normalized to [0, 1]""" + # High entropy (>0.7) = likely random ID + # Low entropy (<0.3) = likely static text +``` + +**Configuration**: +```python +@dataclass +class CacheAlignerConfig: + enabled: bool = True + date_patterns: list[str] = [...] + normalize_whitespace: bool = True + collapse_blank_lines: bool = True + dynamic_tail_separator: str = "\n\n---\n[Dynamic Context]\n" +``` + +--- + +### 1.3 RollingWindow - Context Limit Management + +**Location**: `headroom/transforms/rolling_window.py` + +**What It Does**: Enforces token limits by dropping oldest context while NEVER orphaning tool call/result pairs. + +**The Tool Unit Concept**: +``` +Messages: +[0] System: "You are helpful" +[1] User: "Search for X" +[2] Assistant: [tool_calls: search(X), summarize()] +[3] Tool: search result (tool_call_id=call_1) +[4] Tool: summarize result (tool_call_id=call_2) +[5] User: "Thanks" + +Tool Unit: (2, [3, 4]) → These drop TOGETHER +``` + +**Why This Matters**: LLM APIs return errors if tool_calls reference missing tool results. RollingWindow treats them as atomic units. + +**Drop Priority**: +1. Oldest tool units (atomic: assistant + all tool results) +2. Non-tool user/assistant pairs +3. Single messages (last resort) + +**Protection Rules**: +- System messages: NEVER dropped +- Last N turns: ALWAYS kept (default 2) +- Tool results for protected messages: AUTO-protected + +**Configuration**: +```python +@dataclass +class RollingWindowConfig: + enabled: bool = True + keep_system: bool = True + keep_last_turns: int = 2 + output_buffer_tokens: int = 4000 # Reserve for output +``` + +--- + +### 1.4 Transform Pipeline - Orchestration + +**Location**: `headroom/transforms/pipeline.py` + +**Execution Order** (Critical): +``` +1. CacheAligner → Stabilize prefix for cache hits +2. SmartCrusher → Compress tool outputs +3. RollingWindow → Enforce token limits +``` + +**Why This Order**: +1. Cache alignment must happen before content changes +2. Compression reduces tokens before limit enforcement +3. Rolling window is the final safety net + +**Token Tracking**: Pipeline tracks tokens through each stage and reports: +```python +@dataclass +class TransformResult: + messages: list[dict] + tokens_before: int + tokens_after: int + transforms_applied: list[str] + markers_inserted: list[str] +``` + +--- + +## 2. Relevance Scoring Engine + +### 2.1 BM25Scorer - Keyword Matching + +**Location**: `headroom/relevance/bm25.py` + +**What It Does**: Fast, zero-dependency keyword matching using the BM25 algorithm from information retrieval. + +**Algorithm**: +``` +score(D, Q) = Σ IDF(q) * (f(q,D) * (k1 + 1)) / (f(q,D) + k1 * (1 - b + b * |D|/avgdl)) + +Parameters: +- k1 = 1.5 (term frequency saturation) +- b = 0.75 (length normalization) +``` + +**Special Features**: +- UUID preservation in tokenization +- +0.3 bonus for exact long token matches (≥8 chars) +- Query frequency weighting + +**Use Cases**: Exact ID matching, UUID lookup, keyword search + +--- + +### 2.2 EmbeddingScorer - Semantic Matching + +**Location**: `headroom/relevance/embedding.py` + +**What It Does**: Semantic similarity using sentence-transformers embeddings. + +**Model**: `all-MiniLM-L6-v2` (22M params, 384 dimensions) + +**Algorithm**: +```python +score = cosine_similarity(embed(item), embed(query)) +# Clamped to [0, 1] +``` + +**Optimizations**: +- Batch encoding (context + all items in one call) +- Model caching across instances +- Normalized embeddings for fast cosine + +**Use Cases**: Natural language queries, semantic search + +--- + +### 2.3 HybridScorer - Adaptive Fusion + +**Location**: `headroom/relevance/hybrid.py` + +**What It Does**: Combines BM25 and embedding scores with adaptive alpha based on query characteristics. + +**Fusion Formula**: +``` +combined = α * BM25_score + (1 - α) * Embedding_score +``` + +**Adaptive Alpha** (Research: Hsu et al., 2025): +```python +def compute_alpha(query): + if has_uuid(query): + return 0.85 # Favor exact matching + elif has_multiple_ids(query): + return 0.75 + elif has_single_id(query): + return 0.65 + elif has_hostname_or_email(query): + return 0.60 + else: + return 0.50 # Balanced +``` + +**Graceful Degradation**: If embeddings unavailable, falls back to boosted BM25. + +--- + +## 3. Cache Optimization (Provider-Specific) + +### 3.1 Provider Comparison Matrix + +| Feature | Anthropic | OpenAI | Google | +|---------|-----------|--------|--------| +| **Strategy** | Explicit `cache_control` | Automatic prefix | `CachedContent` API | +| **Min Tokens** | 1,024 | 1,024 | 32,768 | +| **Max Breakpoints** | 4 | N/A | 1 | +| **Write Cost** | 1.25x | N/A | N/A | +| **Read Cost** | 0.10x (90% off) | 0.50x (50% off) | 0.25x (75% off) | +| **TTL** | 5 min | 5-60 min | Up to 7 days | +| **Control** | Explicit | Automatic | Explicit | + +### 3.2 AnthropicCacheOptimizer + +**Location**: `headroom/cache/anthropic.py` + +**Algorithm**: +1. Analyze message sections (system, tools, examples, user) +2. Stabilize prefix by extracting dynamic content +3. Plan breakpoints (max 4, prioritize system > tools > examples) +4. Insert `cache_control: {"type": "ephemeral"}` blocks + +**Cost Example**: +``` +First request (write): 1,500 cached tokens * 1.25x = 1,875 cost +Subsequent (read): 1,500 cached tokens * 0.10x = 150 cost +Savings per hit: 92% +``` + +### 3.3 OpenAICacheOptimizer + +**Location**: `headroom/cache/openai.py` + +**Strategy**: Since OpenAI caching is automatic, we maximize cache hits through prefix stabilization: +1. Extract dynamic content via tiered detection +2. Move dates/IDs to end of message +3. Normalize whitespace for consistent hashing + +### 3.4 GoogleCacheOptimizer + +**Location**: `headroom/cache/google.py` + +**Strategy**: Uses Google's explicit CachedContent API: +1. Analyze cacheability (need 32K+ tokens) +2. Prepare cache creation params +3. Register cache for reuse +4. Include `cache_id` in subsequent requests + +--- + +## 4. Production Proxy Server + +**Location**: `headroom/proxy/server.py` (1400+ lines) + +### 4.1 Core Features + +| Feature | Description | Configuration | +|---------|-------------|---------------| +| **Optimization** | SmartCrusher + CacheAligner + RollingWindow | `optimize=True` | +| **Semantic Cache** | Hash-based response caching with TTL | `cache_ttl_seconds=3600` | +| **Rate Limiting** | Token bucket algorithm (requests + tokens) | `rate_limit_requests_per_minute=60` | +| **Retry** | Exponential backoff with jitter | `retry_max_attempts=3` | +| **Cost Tracking** | Real-time cost + budget enforcement | `budget_limit_usd=100.0` | +| **Prometheus** | `/metrics` endpoint | Automatic | +| **Logging** | JSONL request logs | `log_file="/var/log/headroom.jsonl"` | + +### 4.2 Endpoints + +``` +GET /health → Health check +GET /stats → Detailed statistics +GET /metrics → Prometheus format +POST /v1/messages → Anthropic API proxy +POST /v1/chat/completions → OpenAI API proxy +POST /cache/clear → Clear semantic cache + +# CCR Endpoints (NEW) +POST /v1/retrieve → Retrieve cached original content +GET /v1/feedback → Get all learned patterns +GET /v1/feedback/{tool} → Get hints for specific tool +``` + +### 4.3 Token Bucket Rate Limiter + +```python +class TokenBucketRateLimiter: + def check_request(api_key) -> (allowed: bool, wait_seconds: float) + def check_tokens(api_key, count) -> (allowed: bool, wait_seconds: float) + + # Continuous refill based on elapsed time + # Separate buckets for requests and tokens per API key +``` + +### 4.4 Cost Tracker + +```python +PRICING = { + "claude-3-5-sonnet": (3.00, 15.00, 0.30), # input, output, cached + "gpt-4o": (2.50, 10.00, 1.25), + ... +} + +class CostTracker: + def estimate_cost(model, input_tokens, output_tokens, cached_tokens) + def check_budget() -> (within_budget: bool, remaining_usd: float) +``` + +--- + +## 5. Multi-Provider Support + +### 5.1 Token Counting + +| Provider | Method | Accuracy | +|----------|--------|----------| +| Anthropic | Official Token Count API | High | +| Anthropic (fallback) | tiktoken * 1.1 | Medium | +| OpenAI | tiktoken (model-specific) | High | +| Google | Official countTokens API | High | + +### 5.2 Supported Models + +**Anthropic**: +- claude-3-5-sonnet-20241022 (200K context) +- claude-3-5-haiku-20241022 (200K context) +- claude-3-opus-20240229 (200K context) + +**OpenAI**: +- gpt-4o (128K context) +- gpt-4o-mini (128K context) +- o1, o1-mini, o3-mini (128-200K context) + +**Google**: +- gemini-2.0-flash (1M context) +- gemini-1.5-pro (2M context) +- gemini-1.5-flash (1M context) + +--- + +## 6. Integrations + +### 6.1 LangChain Integration + +**Location**: `headroom/integrations/langchain.py` + +**HeadroomChatModel** - Wrapper that applies optimization: +```python +from langchain_openai import ChatOpenAI +from headroom.integrations import HeadroomChatModel + +base_model = ChatOpenAI(model="gpt-4o") +optimized = HeadroomChatModel(base_model, config=HeadroomConfig()) + +response = optimized.invoke("What is 2+2?") +print(f"Saved: {optimized.total_tokens_saved} tokens") +``` + +### 6.2 MCP Integration + +**Location**: `headroom/integrations/mcp.py` + +**HeadroomMCPCompressor** - Compress tool outputs: +```python +from headroom.integrations.mcp import compress_tool_result_with_metrics + +result = compress_tool_result_with_metrics( + content=tool_output, + tool_name="search_logs", + user_query="find errors", +) +print(f"Items: {result.items_before} → {result.items_after}") +print(f"Errors preserved: {result.errors_preserved}") +``` + +**Default Tool Profiles**: +```python +# Slack - preserve bugs/issues +MCPToolProfile(tool_name_pattern=r".*slack.*", max_items=25) + +# Database - preserve nulls/violations +MCPToolProfile(tool_name_pattern=r".*database.*", max_items=30) + +# Logs - preserve ALL errors +MCPToolProfile(tool_name_pattern=r".*log.*", max_items=40) +``` + +--- + +## 7. Pricing Registry + +**Location**: `headroom/pricing/` + +**Features**: +- Real-time pricing for all models +- Batch pricing support +- Staleness detection (warns if >30 days old) +- Cost estimation with breakdown + +**Last Updated**: January 6, 2025 + +--- + +# Part 2: Why Headroom is Different + +## The Market Gap Nobody Else Fills + +### What Existing Tools Do + +| Tool | Category | What It Does | What It DOESN'T Do | +|------|----------|--------------|-------------------| +| **LiteLLM** | Gateway/Routing | Unified API for 100+ providers | No context optimization | +| **Helicone** | Observability | Logs, metrics, dashboards | No compression, just watching | +| **Portkey** | Governance | Guardrails, compliance, security | No token reduction | +| **OpenRouter** | Marketplace | Access to 300+ models | 5% markup, no optimization | +| **Cloudflare AI Gateway** | CDN | Caching at edge | Simple caching, no intelligence | + +### What Headroom Does (That Nobody Else Does) + +**1. Statistical Compression with Quality Guarantees** + +No other tool compresses tool outputs while guaranteeing error preservation: +``` +Input: 1,000 search results (50,000 tokens) +Output: 20 results (1,000 tokens) - 98% reduction + ALL errors preserved: 100% + ALL anomalies preserved: 100% +``` + +**2. Relevance-Aware Filtering** + +SmartCrusher uses BM25 + embeddings to keep items matching the user's query: +``` +User asks: "Why is authentication failing?" +Tool returns: 1,000 log entries +SmartCrusher keeps: + - All entries with "error", "failed", "exception" + - Entries semantically similar to "authentication failing" + - First 3 and last 2 for context +``` + +**3. Provider-Specific Cache Optimization** + +We understand each provider's caching rules: +- Anthropic: We insert `cache_control` blocks at optimal positions +- OpenAI: We stabilize prefixes for automatic caching +- Google: We manage CachedContent lifecycle + +**4. Atomic Tool Unit Handling** + +RollingWindow is the only context manager that treats tool_calls and their results as atomic: +``` +Other tools: Drop old messages → Orphaned tool results → API ERROR +Headroom: Drop tool units atomically → Always valid state +``` + +--- + +## Competitive Analysis: Deep Dive + +### vs. LiteLLM + +| Aspect | LiteLLM | Headroom | +|--------|---------|----------| +| **Primary Function** | Route to 100+ providers | Optimize before routing | +| **Token Reduction** | None | 50-70% | +| **Caching** | None | Semantic + provider-specific | +| **Setup Time** | 15-30 min | 5 min | +| **Latency Overhead** | ~500µs | <50ms | +| **Relationship** | Complementary - we optimize BEFORE LiteLLM routes | + +**Partnership Opportunity**: Headroom optimizes → LiteLLM routes → best of both. + +### vs. Helicone + +| Aspect | Helicone | Headroom | +|--------|----------|----------| +| **Primary Function** | Observe and log | Optimize and compress | +| **Token Reduction** | Shows waste, doesn't fix it | Eliminates waste | +| **Latency** | ~50ms (Rust) | <50ms | +| **Caching** | Redis-based, TTL | Semantic + provider-specific | +| **Relationship** | Complementary - we reduce, they observe | + +**Partnership Opportunity**: Headroom compresses → Helicone shows savings achieved. + +### vs. Portkey + +| Aspect | Portkey | Headroom | +|--------|---------|----------| +| **Primary Function** | Governance, guardrails | Optimization, compression | +| **Target User** | Enterprise security teams | Developers, cost-conscious | +| **Token Reduction** | None | 50-70% | +| **Pricing** | From $49/month | Open source core | +| **Relationship** | Different markets | + +### vs. Prompt Compression Techniques (LLMLingua, etc.) + +| Aspect | LLMLingua-2 | Headroom | +|--------|-------------|----------| +| **Approach** | Token classification (remove tokens) | Statistical sampling (keep important items) | +| **Target** | Reduce prompt tokens | Reduce tool output tokens | +| **Granularity** | Token-level | Item-level (semantic units) | +| **Quality Guarantee** | 95-98% accuracy | 100% error retention | +| **Dependencies** | XLM-RoBERTa model | Zero (BM25) or sentence-transformers | +| **Use Case** | Long prompts | Large JSON arrays from tools | + +--- + +## The Industry Problem We Solve + +### Context Explosion in AI Agents + +Research from [JetBrains (Dec 2025)](https://blog.jetbrains.com/research/2025/12/efficient-context-management/): +> "Agents make multiple tool calls in sequence, and each tool's output is fed back into the LLM's context window. Without proper context management, this accumulation can quickly exceed the context window, increase costs dramatically, and degrade performance." + +### The "Lost in the Middle" Problem + +> "LLMs are more likely to recall information appearing at the beginning or end of long prompts rather than content buried in the middle." + +**Headroom's Solution**: SmartCrusher keeps first 3 + last 2 items, plus errors/anomalies/relevant items. We work WITH the LLM's attention patterns. + +### Context Rot + +> "Expanding context windows does not guarantee improved model performance. As input tokens increase, LLM performance can actually degrade." + +**Headroom's Solution**: Smaller, higher-quality context → better performance AND lower cost. + +--- + +## Unique Technical Innovations + +### 1. Change Point Detection for Time Series + +No other tool detects regime shifts in numeric data: +```python +# Values: [100, 102, 98, 101, 99, 500, 502, 498, 501] +# ↑ +# Change point detected! +# SmartCrusher keeps items around index 5 +``` + +### 2. Adaptive Relevance Fusion + +Our HybridScorer adjusts BM25/embedding weights based on query type: +- UUID in query → More BM25 (exact matching) +- Natural language → More embedding (semantic) + +This achieves +2-7.5% accuracy improvement over fixed weights. + +### 3. Tool Unit Atomicity + +The only context manager that guarantees: +``` +assistant message with tool_calls → ALWAYS has corresponding tool results +``` + +### 4. Tiered Dynamic Detection + +We don't use hardcoded locale patterns. Our detection is: +- Universal: ISO 8601, UUIDs, entropy-based IDs +- Structural: `Label: value` patterns +- Semantic: Embedding similarity to known dynamic exemplars + +--- + +# Part 3: Real Numbers + +## Compression Performance + +| Scenario | Items Before | Items After | Token Reduction | Errors Retained | +|----------|--------------|-------------|-----------------|-----------------| +| Search Results | 1,000 | 20 | 85% | 100% | +| Log Entries | 500 | 40 | 80% | 100% | +| Database Rows | 1,000 | 30 | 90% | 100% | +| API Responses | 200 | 15 | 70% | 100% | + +## Latency Overhead + +| Component | P50 | P99 | +|-----------|-----|-----| +| SmartCrusher (1000 items) | 5ms | 15ms | +| CacheAligner | <1ms | 2ms | +| RollingWindow | <1ms | 5ms | +| Full Pipeline | 10ms | 25ms | + +## Cost Savings (Real World) + +**Claude Code Agent Session**: +``` +Without Headroom: + - Tool outputs: 150,000 tokens + - Cost: $0.45 (input @ $3/M) + +With Headroom: + - Tool outputs: 30,000 tokens (80% reduction) + - Cost: $0.09 (input @ $3/M) + - Savings: $0.36 per session (80%) +``` + +**Enterprise (1M requests/month)**: +``` +Without Headroom: $450,000/month +With Headroom: $90,000/month +Savings: $360,000/month (80%) +``` + +--- + +# Part 4: Architecture Summary + +``` +┌─────────────────────────────────────────────────────────────┐ +│ YOUR APPLICATION │ +│ │ +│ LangChain │ Claude Code │ Cursor │ Custom Agent │ +└──────────────────────────┬──────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ HEADROOM PROXY │ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ +│ │ Cache │ │ Rate │ │ Cost │ │ +│ │ (Semantic) │ │ Limiter │ │ Tracker │ │ +│ └─────────────┘ └─────────────┘ └─────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────┐│ +│ │ TRANSFORM PIPELINE ││ +│ │ ││ +│ │ 1. CacheAligner → Stabilize prefix for cache hits ││ +│ │ 2. SmartCrusher → Compress tool outputs ││ +│ │ 3. RollingWindow → Enforce token limits ││ +│ │ ││ +│ │ ┌─────────────────────────────────────────────────┐ ││ +│ │ │ RELEVANCE ENGINE │ ││ +│ │ │ BM25 + Embedding + Adaptive Hybrid │ ││ +│ │ └─────────────────────────────────────────────────┘ ││ +│ └─────────────────────────────────────────────────────────┘│ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ +│ │ Prometheus │ │ JSONL │ │ Retry │ │ +│ │ Metrics │ │ Logging │ │ (Exp. Backoff) │ │ +│ └─────────────┘ └─────────────┘ └─────────────────────┘ │ +└──────────────────────────┬──────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ LLM PROVIDERS │ +│ │ +│ Anthropic │ OpenAI │ Google │ Others │ +│ │ +│ ┌─────────────────────────────────────────────────────────┐│ +│ │ PROVIDER-SPECIFIC CACHE OPTIMIZERS ││ +│ │ ││ +│ │ Anthropic: cache_control blocks (90% savings) ││ +│ │ OpenAI: Prefix stabilization (50% savings) ││ +│ │ Google: CachedContent API (75% savings) ││ +│ └─────────────────────────────────────────────────────────┘│ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +# Part 5: File Inventory + +## Core Transforms +- `headroom/transforms/smart_crusher.py` - Statistical array compression +- `headroom/transforms/cache_aligner.py` - Prefix stabilization +- `headroom/transforms/rolling_window.py` - Context limit management +- `headroom/transforms/pipeline.py` - Transform orchestration + +## Relevance Scoring +- `headroom/relevance/bm25.py` - BM25 keyword scorer +- `headroom/relevance/embedding.py` - Semantic scorer +- `headroom/relevance/hybrid.py` - Adaptive fusion scorer + +## Cache Optimization +- `headroom/cache/base.py` - Base interfaces +- `headroom/cache/anthropic.py` - Anthropic optimizer +- `headroom/cache/openai.py` - OpenAI optimizer +- `headroom/cache/google.py` - Google optimizer +- `headroom/cache/dynamic_detector.py` - Tiered dynamic detection +- `headroom/cache/semantic.py` - Semantic cache layer +- `headroom/cache/compression_store.py` - CCR Phase 1: Store original content ⭐ NEW +- `headroom/cache/compression_feedback.py` - CCR Phase 4: Learn from retrievals ⭐ NEW + +## Proxy Server +- `headroom/proxy/server.py` - Production HTTP proxy (1400+ lines) + +## Providers +- `headroom/providers/anthropic.py` - Anthropic token counting +- `headroom/providers/openai.py` - OpenAI token counting +- `headroom/providers/google.py` - Google token counting + +## Integrations +- `headroom/integrations/langchain.py` - LangChain wrapper +- `headroom/integrations/mcp.py` - MCP compression + +## Pricing +- `headroom/pricing/registry.py` - Pricing registry +- `headroom/pricing/anthropic_prices.py` - Anthropic prices +- `headroom/pricing/openai_prices.py` - OpenAI prices + +## Tests +- `tests/test_quality_retention.py` - 21 formal evals for quality guarantees +- `tests/test_cache/test_dynamic_detector.py` - Dynamic detection tests +- `tests/test_ccr.py` - CCR store, tool injection tests ⭐ NEW +- `tests/test_ccr_feedback.py` - CCR feedback loop tests ⭐ NEW + +## Benchmarks +- `benchmarks/agent_cost_benchmark.py` - Real-world agent cost analysis +- `benchmarks/dynamic_detector_benchmark.py` - Detection performance + +--- + +# Sources + +- [JetBrains Research: Efficient Context Management (Dec 2025)](https://blog.jetbrains.com/research/2025/12/efficient-context-management/) +- [LangChain: Context Engineering for Agents](https://blog.langchain.com/context-engineering-for-agents/) +- [Helicone: Top 5 LLM Gateways 2025](https://www.helicone.ai/blog/top-llm-gateways-comparison-2025) +- [Agenta: Top LLM Gateways 2025](https://agenta.ai/blog/top-llm-gateways) +- [Portkey: LLM Proxy vs AI Gateway](https://portkey.ai/blog/llm-proxy-vs-ai-gateway/) +- [Medium: Prompt Compression Techniques (Nov 2025)](https://medium.com/@kuldeep.paul08/prompt-compression-techniques-reducing-context-window-costs-while-improving-llm-performance-afec1e8f1003) +- [Factory.ai: Compressing Context](https://factory.ai/news/compressing-context) diff --git a/docs/PATH_TO_10_OUT_OF_10.md b/docs/PATH_TO_10_OUT_OF_10.md new file mode 100644 index 000000000..6d490fc0e --- /dev/null +++ b/docs/PATH_TO_10_OUT_OF_10.md @@ -0,0 +1,661 @@ +# The Path to 10/10: Strategic Deep Dive + +## Current State + +| Dimension | Score | Gap | +|-----------|-------|-----| +| Problem validity | 9/10 | Framing as "cost" not "capability" | +| Solution fit | 7/10 | 30% of scenarios fail silently | +| Technical moat | 6/10 | Easy to replicate basics | +| Market timing | 9/10 | Positioned but not capturing | +| **Overall** | **7.5/10** | | + +--- + +# Dimension 1: Problem Validity (9 → 10) + +## Current Framing (9/10) +"Token costs are expensive. We save you 50-90%." + +**Why it's not 10/10**: Cost savings is a feature, not a platform. It's also easily commoditized - anyone can undercut on price. + +## The 10/10 Framing: Capability Enablement + +**The insight**: Without context optimization, certain agent capabilities are **literally impossible**. + +### Evidence + +| Scenario | Without Headroom | With Headroom | +|----------|------------------|---------------| +| Multi-tool investigation (5+ tools) | Context overflow at 128K | Fits in 30K | +| Long-running agent (50+ turns) | Loses early context | Maintains full history | +| Real-time agents (latency-sensitive) | Cache misses = 2-3s latency | Cache hits = 200ms | +| Cost-constrained deployment | $5K/month = 5K requests | $5K/month = 25K requests | + +**The reframe**: + +> "Headroom doesn't just save money. It **unlocks agent capabilities that are impossible without context optimization**." + +### Specific Claims to Make + +1. **"Enable 5x more tool calls per context window"** + - Not "save 80% on tokens" + - But "do 5x more in the same budget" + +2. **"Make real-time agents viable"** + - Cache alignment → cache hits → <500ms responses + - Without this, interactive agents are too slow + +3. **"Prevent context overflow failures"** + - Agent that fails at turn 47 because context overflowed + - vs. agent that completes 200-turn sessions + +4. **"Run agents at 10x the scale"** + - Same budget, 10x throughput + - This is a capability unlock, not a cost savings + +### Action Items + +- [ ] Rewrite all marketing around "capability enablement" +- [ ] Quantify "things you CAN'T do without Headroom" +- [ ] Build demo showing agent that fails → succeeds with Headroom +- [ ] Position as "Context Runtime" not "Token Optimizer" + +--- + +# Dimension 2: Solution Fit (7 → 10) + +## Current Problem (7/10) + +Heuristics work for ~70% of scenarios. The 30% that fail: +- Entity listings (each item is unique and important) +- Exhaustive queries ("find ALL X") +- Needles that look normal (Order #47 from California) + +**Root cause**: Task-agnostic compression can't know what the LLM will need. + +## The 10/10 Solution: Three-Layer Architecture + +### Layer 1: Smart Routing (NEW) + +**Before compression, classify the task:** + +```python +class TaskClassifier: + """Classify task to determine compression strategy.""" + + def classify(self, user_query: str, tool_output: dict) -> TaskType: + # Analyze user query intent + if self._is_exhaustive_query(user_query): + return TaskType.EXHAUSTIVE # "find ALL", "list every" + + if self._is_specific_lookup(user_query): + return TaskType.LOOKUP # "find user #47", "get order X" + + if self._is_analytical(user_query): + return TaskType.ANALYTICAL # "what's wrong", "summarize" + + return TaskType.GENERAL + + def _is_exhaustive_query(self, query: str) -> bool: + exhaustive_patterns = [ + r"\ball\b", r"\bevery\b", r"\beach\b", + r"\bcomplete list\b", r"\bfull list\b" + ] + return any(re.search(p, query.lower()) for p in exhaustive_patterns) +``` + +**Strategy per task type:** + +| Task Type | Strategy | Rationale | +|-----------|----------|-----------| +| EXHAUSTIVE | Skip compression | User needs everything | +| LOOKUP | Filter by query match | Only relevant items | +| ANALYTICAL | Statistical compression | Summaries ok | +| GENERAL | Default heuristics | Balanced approach | + +### Layer 2: Confidence-Gated Compression (NEW) + +**Only compress when confidence is high:** + +```python +class CompressionConfidence: + """Estimate confidence that compression is safe.""" + + def estimate(self, items: list[dict], hints: CompressionHints) -> float: + confidence = 1.0 + + # Low confidence if high uniqueness + no importance signal + if self._is_high_uniqueness(items) and not self._has_importance_signal(items): + confidence -= 0.4 + + # Low confidence if historical retrieval rate is high + if hints.retrieval_rate > 0.5: + confidence -= 0.3 + + # Low confidence if items look like entities + if self._looks_like_entity_list(items): + confidence -= 0.3 + + return max(0.0, confidence) + + def should_compress(self, confidence: float) -> bool: + return confidence > 0.6 # Only compress when confident +``` + +**The key insight**: It's better to NOT compress than to compress wrong. + +### Layer 3: Seamless CCR (Enhanced) + +**Make retrieval so good that compression "failures" don't matter:** + +Current CCR: +``` +LLM: "I need to find orders from California" +[Must explicitly call retrieve_compressed] +``` + +Enhanced CCR: +``` +LLM: "I need to find orders from California" +[Automatic injection]: "Searching compressed content for 'California'..." +[Returns matching items without explicit tool call] +``` + +**Implementation: Semantic Injection** + +```python +class SemanticCCR: + """Automatically inject relevant cached content based on LLM response.""" + + def intercept_response(self, llm_response: str, cached_hashes: list[str]) -> str: + # Detect if LLM is "reaching" for data it doesn't have + reaching_patterns = [ + r"I don't see .* in the data", + r"The data doesn't show", + r"I need more information about", + r"Looking for .* but", + ] + + for pattern in reaching_patterns: + match = re.search(pattern, llm_response) + if match: + # Extract what they're looking for + query = self._extract_search_intent(llm_response) + # Search all cached content + results = self._search_cached(cached_hashes, query) + if results: + # Inject into context + return self._inject_results(llm_response, results) + + return llm_response +``` + +### Layer 4: Learned Compression Profiles (NEW) + +**Per-tool profiles that go beyond heuristics:** + +```python +@dataclass +class ToolCompressionProfile: + """Learned compression profile for a specific tool.""" + + tool_name: str + + # Learned from retrieval patterns + critical_fields: list[str] # Always preserve these + optional_fields: list[str] # Can compress + noise_fields: list[str] # Usually irrelevant + + # Learned from retrieval rate + min_items: int # Never compress below this + target_items: int # Optimal compression target + skip_conditions: list[str] # When to skip compression entirely + + # Learned from query patterns + common_search_terms: list[str] # Pre-filter for these + + # Confidence + sample_size: int # How much data we've seen + confidence: float # How confident in this profile +``` + +**Building profiles from feedback:** + +```python +def update_profile_from_retrieval(profile: ToolCompressionProfile, event: RetrievalEvent): + # If they retrieved, compression was too aggressive + profile.min_items = max(profile.min_items, event.items_retrieved) + + # Track what fields they queried + for field in extract_fields(event.query): + if field not in profile.critical_fields: + profile.critical_fields.append(field) + + # Track common search terms + if event.query: + profile.common_search_terms.append(event.query) + + # Update confidence based on sample size + profile.sample_size += 1 + profile.confidence = min(0.95, profile.sample_size / 100) +``` + +## The 10/10 Solution Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ TOOL OUTPUT (1000 items) │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ LAYER 1: TASK CLASSIFICATION │ +│ │ +│ User query: "Find all orders from California" │ +│ Classification: EXHAUSTIVE (pattern: "all") │ +│ Decision: SKIP COMPRESSION │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ (if not SKIP) +┌─────────────────────────────────────────────────────────────────┐ +│ LAYER 2: CONFIDENCE ESTIMATION │ +│ │ +│ Tool profile: search_api (confidence: 0.85) │ +│ Data analysis: unique_ratio=0.95, no_score_field │ +│ Compression confidence: 0.4 │ +│ Decision: SKIP (confidence < 0.6) │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ (if confident) +┌─────────────────────────────────────────────────────────────────┐ +│ LAYER 3: PROFILE-GUIDED COMPRESSION │ +│ │ +│ Profile: search_api │ +│ - critical_fields: [id, status, error] │ +│ - min_items: 25 │ +│ - common_search_terms: [status:error, level:critical] │ +│ │ +│ Compression: 1000 → 30 items (profile-guided, not heuristic) │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ LAYER 4: CCR WITH SEMANTIC INJECTION │ +│ │ +│ Cache: Store full 1000 items │ +│ Monitor: Watch for "reaching" patterns in LLM response │ +│ Inject: Auto-retrieve if LLM seems to need more │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ FEEDBACK LOOP │ +│ │ +│ Track: Retrieval patterns, query patterns, failure patterns │ +│ Learn: Update tool profiles, adjust confidence thresholds │ +│ Improve: Next compression is smarter │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Action Items + +- [ ] Implement TaskClassifier with exhaustive/lookup/analytical detection +- [ ] Add confidence estimation to SmartCrusher +- [ ] Build ToolCompressionProfile system +- [ ] Implement semantic injection for CCR +- [ ] Create profile bootstrap from first 10 compressions per tool + +--- + +# Dimension 3: Technical Moat (6 → 10) + +## Current Problem (6/10) + +Individual techniques are not novel: +- Statistical analysis: Data profiling tools exist +- BM25/embeddings: Standard IR +- Caching: Standard pattern + +**The combination is the innovation, but combinations are easy to copy.** + +## The 10/10 Moat: Data Flywheel + +### The Insight + +True moats in infrastructure come from: +1. **Network effects** - More users = better product +2. **Data moats** - Proprietary data that improves over time +3. **Integration depth** - Becomes part of the stack +4. **Ecosystem** - Others build on top of you + +**The killer moat: A compression model trained on real agent data.** + +### Phase 1: Aggregate Tool Intelligence (Months 1-6) + +**Collect anonymized statistics across all users:** + +```python +@dataclass +class AnonymizedToolStats: + """Privacy-preserving tool statistics.""" + + tool_signature: str # Hash of tool name + schema + + # Field patterns (no actual values) + field_types: dict[str, str] # {"status": "categorical", "count": "numeric"} + field_distributions: dict # {"status": {"unique_ratio": 0.05}} + + # Compression patterns + avg_compression_ratio: float + avg_retrieval_rate: float + successful_strategies: list[str] + + # Query patterns (no actual queries) + common_query_patterns: list[str] # ["field:*", "status:error"] + queried_field_frequency: dict # {"status": 0.8, "id": 0.3} +``` + +**Build the "Tool Intelligence Database":** + +```python +class ToolIntelligenceDB: + """Cross-user intelligence about tool outputs.""" + + def get_profile(self, tool_signature: str) -> ToolCompressionProfile: + """Get compression profile based on aggregate data.""" + stats = self._aggregate_stats(tool_signature) + + return ToolCompressionProfile( + critical_fields=stats.get_frequently_queried_fields(), + min_items=stats.get_safe_compression_target(), + skip_conditions=stats.get_high_retrieval_scenarios(), + confidence=stats.sample_size / 1000, # More data = more confidence + ) +``` + +**The moat**: "We've seen 10M GitHub API responses. We know exactly what to compress." + +### Phase 2: Train Compression Classifier (Months 6-12) + +**Use aggregate data to train a small, fast model:** + +```python +class CompressionClassifier: + """Learned compression decision model.""" + + def __init__(self, model_path: str): + # Small transformer (~50M params) fine-tuned on compression decisions + self.model = load_model(model_path) + + def predict(self, + tool_stats: ToolStats, + user_query: str, + sample_items: list[dict]) -> CompressionDecision: + """Predict optimal compression strategy.""" + + # Encode input + features = self._encode_features(tool_stats, user_query, sample_items) + + # Predict + output = self.model(features) + + return CompressionDecision( + should_compress=output.compress_probability > 0.7, + strategy=output.best_strategy, + target_items=output.target_items, + preserve_fields=output.preserve_fields, + confidence=output.confidence, + ) +``` + +**Training data (from aggregate stats):** + +| Input | Output | Label Source | +|-------|--------|--------------| +| Tool stats + query + sample items | Compression decision | Retrieval rate feedback | +| High unique_ratio + no score field | SKIP | High retrieval rate | +| Score field + analytical query | TOP_N | Low retrieval rate | +| Error keywords in query | PRESERVE_ERRORS | Query pattern analysis | + +**The moat**: Model trained on proprietary data. Competitors start at zero. + +### Phase 3: Ecosystem Lock-in (Months 12-24) + +**Deep integration with agent frameworks:** + +```python +# LangChain official integration +from langchain_headroom import HeadroomCache + +llm = ChatOpenAI(cache=HeadroomCache()) # Just works + +# LlamaIndex official integration +from llama_index.headroom import HeadroomContextManager + +index = VectorStoreIndex(context_manager=HeadroomContextManager()) + +# CrewAI official integration +from crewai_headroom import HeadroomCrew + +crew = HeadroomCrew(agents=[...]) # Auto-optimizes all agents +``` + +**Build ecosystem on top:** + +| Component | What It Does | Lock-in | +|-----------|--------------|---------| +| Headroom Dashboard | Visualize context usage | Analytics dependency | +| Headroom MCP | Universal agent optimization | Protocol dependency | +| Headroom VS Code | IDE integration | Developer workflow | +| Headroom Profiles | Community tool profiles | Content lock-in | + +### The Data Flywheel + +``` +┌──────────────────────────────────────────────────────────────┐ +│ MORE USERS │ +└──────────────────────────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────┐ +│ MORE TOOL OUTPUT DATA │ +│ (anonymized stats, retrieval patterns, query patterns) │ +└──────────────────────────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────┐ +│ BETTER COMPRESSION MODEL │ +│ (trained on more data, more tool types, more scenarios) │ +└──────────────────────────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────┐ +│ BETTER COMPRESSION QUALITY │ +│ (higher accuracy, fewer retrievals, more savings) │ +└──────────────────────────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────┐ +│ MORE USERS │ +│ (word of mouth, better benchmarks, lower churn) │ +└──────────────────────────────────────────────────────────────┘ + │ + └──────────────► (cycle repeats) +``` + +**This is the moat.** Every user makes the product better for every other user. Competitors can't replicate without the data. + +### Action Items + +- [ ] Design privacy-preserving telemetry system +- [ ] Build Tool Intelligence aggregation pipeline +- [ ] Define compression classifier architecture +- [ ] Create training data collection from feedback loop +- [ ] Plan framework partnership outreach + +--- + +# Dimension 4: Market Timing (9 → 10) + +## Current State (9/10) + +Timing is good - AI agent explosion is happening. But are we POSITIONED to capture it? + +## The 10/10 Positioning + +### Strategy 1: Be First in the "Context Optimization" Category + +**Create the category:** +- "Context Optimization" as a must-have layer +- Every serious AI agent needs it +- Headroom = the default choice + +**Content to publish:** +- "The Context Crisis: Why AI Agents Are Hitting Walls" +- "Context Engineering Best Practices" (become the authority) +- Benchmark suite for context optimization + +### Strategy 2: Partner with Major Frameworks + +| Framework | Status | Action | +|-----------|--------|--------| +| LangChain | Large user base | Official integration PR | +| LlamaIndex | Growing fast | Partnership discussion | +| CrewAI | Focused on agents | Perfect fit - reach out | +| Claude Code | Anthropic's CLI | We're already here! | +| Cursor | Popular IDE | Plugin opportunity | + +### Strategy 3: Launch with Major Players + +**Target announcements:** +- "Headroom powers context optimization for [Major Agent Company]" +- "LangChain officially recommends Headroom for production agents" +- "Anthropic's Claude Code uses Headroom for context management" + +### Strategy 4: Open Source Dominance + +**Make Headroom the "nginx of context optimization":** +- Core is free and open source +- Enterprise features are paid +- Community contributions +- Apache 2.0 license + +**The playbook:** +1. Be the obvious open source choice +2. Capture developer mindshare +3. Enterprise upsells for advanced features + +### Action Items + +- [ ] Create "Context Optimization" category content +- [ ] Reach out to LangChain for official integration +- [ ] Publish benchmark suite +- [ ] Plan launch announcements + +--- + +# The 10/10 Roadmap + +## Phase 1: Foundation (Now - Month 3) + +| Goal | Action | Metric | +|------|--------|--------| +| Solution Fit 8/10 | Implement task classification + confidence gating | Retrieval rate < 10% | +| Technical Moat 7/10 | Launch telemetry + Tool Intelligence DB | 1M+ data points | +| Market Timing 10/10 | LangChain integration + category content | Integration shipped | + +**Key deliverables:** +- TaskClassifier with exhaustive/lookup/analytical detection +- Confidence-gated compression +- Privacy-preserving telemetry +- LangChain official integration +- "Context Optimization" blog series + +## Phase 2: Data Flywheel (Month 3 - Month 9) + +| Goal | Action | Metric | +|------|--------|--------| +| Solution Fit 9/10 | Learned compression profiles per tool | 100+ tool profiles | +| Technical Moat 8/10 | Train v1 compression classifier | 5% better than heuristics | +| Problem Validity 10/10 | Publish "impossible without Headroom" demos | 3 viral demos | + +**Key deliverables:** +- ToolCompressionProfile system with cross-user learning +- Compression classifier v1 (small transformer) +- Semantic injection for CCR +- CrewAI + LlamaIndex integrations +- Demo: "This agent workflow is impossible without Headroom" + +## Phase 3: Moat (Month 9 - Month 18) + +| Goal | Action | Metric | +|------|--------|--------| +| Solution Fit 10/10 | Compression classifier v2 | Retrieval rate < 5% | +| Technical Moat 10/10 | Data flywheel operational | 100M+ data points | +| Overall 10/10 | Category leader | #1 in benchmarks | + +**Key deliverables:** +- Compression classifier v2 (trained on 100M+ samples) +- Headroom Dashboard (analytics product) +- Enterprise partnerships +- Community tool profile contributions +- Category ownership: "Context Optimization" + +--- + +# The 10/10 Vision + +## From Today's Headroom + +``` +"A smart compression layer that saves you tokens" +``` + +## To Tomorrow's Headroom + +``` +"The Context Intelligence Platform for AI Applications" + +We don't just compress - we UNDERSTAND context. +- What's in your context? +- What does your agent need? +- What's the optimal representation? +- How do we learn and improve? + +Every agent needs context intelligence. +Headroom is context intelligence. +``` + +## The End State + +| Dimension | Score | How | +|-----------|-------|-----| +| Problem validity | 10/10 | "Enables capabilities impossible without us" | +| Solution fit | 10/10 | Task-aware + learned profiles + seamless CCR | +| Technical moat | 10/10 | Compression model trained on 100M+ samples | +| Market timing | 10/10 | Category leader, framework default | +| **Overall** | **10/10** | **The context layer for AI** | + +--- + +# Summary: The Three Big Moves + +## Move 1: From Cost Savings to Capability Enablement + +**Before**: "Save 50-90% on tokens" +**After**: "Enable agent capabilities that are impossible without context optimization" + +## Move 2: From Heuristics to Learned Intelligence + +**Before**: Statistical heuristics that work 70% of the time +**After**: Task-aware, confidence-gated, profile-guided compression that learns from every interaction + +## Move 3: From Tool to Platform + +**Before**: A compression library you can use +**After**: The context intelligence layer that every serious AI application needs + +--- + +**The bottom line**: 10/10 isn't about perfecting what we have. It's about building a data flywheel that makes the product better with every user, creating capabilities that are impossible without us, and owning the "Context Intelligence" category before anyone else does. diff --git a/examples/anthropic_example.py b/examples/anthropic_example.py index 081a4b372..8bd77ee95 100644 --- a/examples/anthropic_example.py +++ b/examples/anthropic_example.py @@ -80,7 +80,9 @@ def example_optimize_mode(): { "type": "tool_result", "tool_use_id": "call_1", - "content": '{"results": [' + ",".join([f'{{"id": {i}}}' for i in range(50)]) + "]}", + "content": '{"results": [' + + ",".join([f'{{"id": {i}}}' for i in range(50)]) + + "]}", } ], }, @@ -125,7 +127,9 @@ def example_simulate_mode(): { "type": "tool_result", "tool_use_id": "call_1", - "content": '{"results": [' + ",".join([f'{{"id": {i}}}' for i in range(100)]) + "]}", + "content": '{"results": [' + + ",".join([f'{{"id": {i}}}' for i in range(100)]) + + "]}", } ], }, diff --git a/examples/langchain_before_after.py b/examples/langchain_before_after.py index c0a8044a1..c25189da2 100644 --- a/examples/langchain_before_after.py +++ b/examples/langchain_before_after.py @@ -21,18 +21,19 @@ import os import tempfile import time from dataclasses import dataclass -from datetime import datetime # Check dependencies try: from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolMessage + LANGCHAIN_AVAILABLE = True except ImportError: LANGCHAIN_AVAILABLE = False print("LangChain not installed. Install with: pip install langchain-core") try: - from langchain_openai import ChatOpenAI + from langchain_openai import ChatOpenAI # noqa: F401 + OPENAI_AVAILABLE = True except ImportError: OPENAI_AVAILABLE = False @@ -40,12 +41,13 @@ except ImportError: # Import Headroom try: - from headroom import ( + from headroom import ( # noqa: F401 HeadroomClient, HeadroomConfig, HeadroomMode, OpenAIProvider, ) + HEADROOM_AVAILABLE = True except ImportError: HEADROOM_AVAILABLE = False @@ -55,6 +57,7 @@ except ImportError: @dataclass class ComparisonResult: """Result of before/after comparison.""" + scenario: str tokens_before: int tokens_after: int @@ -82,18 +85,18 @@ def print_comparison(result: ComparisonResult) -> None: print(f"\n{'=' * 60}") print(f"Scenario: {result.scenario}") print(f"{'=' * 60}") - print(f"\n[Token Comparison]") + print("\n[Token Comparison]") print(f" Before: {result.tokens_before:,} tokens") print(f" After: {result.tokens_after:,} tokens") print(f" Saved: {result.tokens_saved:,} tokens ({result.savings_percent:.1f}%)") - print(f"\n[Cost Impact] (GPT-4o pricing)") + print("\n[Cost Impact] (GPT-4o pricing)") print(f" Before: ${result.cost_before_usd:.4f}") print(f" After: ${result.cost_after_usd:.4f}") print(f" Saved: ${result.cost_saved_usd:.4f}") if result.latency_before_ms and result.latency_after_ms: - print(f"\n[Latency]") + print("\n[Latency]") print(f" Before: {result.latency_before_ms:.0f}ms") print(f" After: {result.latency_after_ms:.0f}ms") @@ -122,11 +125,13 @@ def langchain_to_openai_messages(messages: list) -> list[dict]: ] openai_messages.append(msg_dict) elif isinstance(msg, ToolMessage): - openai_messages.append({ - "role": "tool", - "tool_call_id": msg.tool_call_id, - "content": msg.content, - }) + openai_messages.append( + { + "role": "tool", + "tool_call_id": msg.tool_call_id, + "content": msg.content, + } + ) return openai_messages @@ -134,6 +139,7 @@ def langchain_to_openai_messages(messages: list) -> list[dict]: # SCENARIO 1: Agentic Workflow with Large Tool Outputs # ============================================================================ + def scenario_agentic_workflow() -> ComparisonResult: """ Scenario: AI agent that searches a database and processes results. @@ -158,20 +164,24 @@ def scenario_agentic_workflow() -> ComparisonResult: "metadata": { "preferences": {"theme": "dark", "notifications": True}, "tags": ["premium", "verified"] if i % 5 == 0 else [], - } + }, } for i in range(100) ] # The conversation in LangChain format lc_messages = [ - SystemMessage(content="""You are a helpful database assistant. + SystemMessage( + content="""You are a helpful database assistant. When searching for users, analyze the results and provide a summary. - Focus on active users in the Engineering department."""), + Focus on active users in the Engineering department.""" + ), HumanMessage(content="Find users in the Engineering department"), AIMessage( content="I'll search the database for Engineering users.", - tool_calls=[{"id": "call_1", "name": "search_users", "args": {"department": "Engineering"}}], + tool_calls=[ + {"id": "call_1", "name": "search_users", "args": {"department": "Engineering"}} + ], ), ToolMessage( content=json.dumps(search_results), # 100 records! @@ -207,13 +217,13 @@ def scenario_agentic_workflow() -> ComparisonResult: tokens_saved = plan.tokens_saved savings_percent = (tokens_saved / tokens_before * 100) if tokens_before > 0 else 0 - print(f"\n[Before Optimization]") - print(f" - System prompt + conversation") + print("\n[Before Optimization]") + print(" - System prompt + conversation") print(f" - Tool output: 100 user records ({len(json.dumps(search_results))} chars)") - print(f"\n[After Optimization]") - print(f" - SmartCrusher kept: first 3, last 2, + relevance matches") - print(f" - Estimated ~15 items preserved (Engineering dept matches)") + print("\n[After Optimization]") + print(" - SmartCrusher kept: first 3, last 2, + relevance matches") + print(" - Estimated ~15 items preserved (Engineering dept matches)") print(f" - Transforms: {plan.transforms}") client.close() @@ -236,6 +246,7 @@ def scenario_agentic_workflow() -> ComparisonResult: # SCENARIO 2: Long Conversation with Context Window Pressure # ============================================================================ + def scenario_long_conversation() -> ComparisonResult: """ Scenario: Multi-turn conversation approaching context window limit. @@ -249,7 +260,8 @@ def scenario_long_conversation() -> ComparisonResult: # Simulate 50-turn conversation in LangChain format lc_messages = [ - SystemMessage(content="""You are a customer support agent for TechCorp. + SystemMessage( + content="""You are a customer support agent for TechCorp. You have access to customer data and can help with: - Account issues - Billing questions @@ -258,7 +270,8 @@ def scenario_long_conversation() -> ComparisonResult: Current date: 2024-12-15 Agent ID: support-agent-42 - """), + """ + ), ] # Add 50 turns of conversation @@ -273,10 +286,12 @@ def scenario_long_conversation() -> ComparisonResult: for i in range(50): topic = topics[i % len(topics)] lc_messages.append(HumanMessage(content=f"Turn {i}: {topic}")) - lc_messages.append(AIMessage( - content=f"Response to turn {i}: Thank you for reaching out about '{topic}'. " - f"I can help you with that. Here's what I found... " * 3 - )) + lc_messages.append( + AIMessage( + content=f"Response to turn {i}: Thank you for reaching out about '{topic}'. " + f"I can help you with that. Here's what I found... " * 3 + ) + ) # Convert to OpenAI format messages = langchain_to_openai_messages(lc_messages) @@ -306,13 +321,13 @@ def scenario_long_conversation() -> ComparisonResult: tokens_saved = plan.tokens_saved savings_percent = (tokens_saved / tokens_before * 100) if tokens_before > 0 else 0 - print(f"\n[Before Optimization]") - print(f" - 50-turn conversation") + print("\n[Before Optimization]") + print(" - 50-turn conversation") print(f" - ~{tokens_before:,} tokens total") - print(f"\n[After Optimization]") - print(f" - RollingWindow kept system + last N turns") - print(f" - CacheAligner moved date to dynamic tail") + print("\n[After Optimization]") + print(" - RollingWindow kept system + last N turns") + print(" - CacheAligner moved date to dynamic tail") print(f" - Transforms: {plan.transforms}") client.close() @@ -335,6 +350,7 @@ def scenario_long_conversation() -> ComparisonResult: # SCENARIO 3: RAG with Retrieved Documents # ============================================================================ + def scenario_rag_pipeline() -> ComparisonResult: """ Scenario: RAG pipeline that retrieves multiple documents. @@ -358,23 +374,24 @@ def scenario_rag_pipeline() -> ComparisonResult: "author": f"Author {i}", "date": "2024-01-15", "category": "Technical", - } + }, } chunks.append(chunk) - context = "\n\n".join([ - f"[Source: {c['source']}, Page {c['page']}]\n{c['content']}" - for c in chunks - ]) + context = "\n\n".join( + [f"[Source: {c['source']}, Page {c['page']}]\n{c['content']}" for c in chunks] + ) # LangChain format lc_messages = [ SystemMessage(content="You are a helpful assistant. Answer based on the provided context."), - HumanMessage(content=f"""Based on the following retrieved documents: + HumanMessage( + content=f"""Based on the following retrieved documents: {context} -Question: What are the key technical requirements?"""), +Question: What are the key technical requirements?""" + ), ] # Convert to OpenAI format @@ -405,12 +422,12 @@ Question: What are the key technical requirements?"""), tokens_saved = plan.tokens_saved savings_percent = (tokens_saved / tokens_before * 100) if tokens_before > 0 else 0 - print(f"\n[Before Optimization]") - print(f" - 10 retrieved document chunks") + print("\n[Before Optimization]") + print(" - 10 retrieved document chunks") print(f" - ~{tokens_before:,} tokens total") - print(f"\n[After Optimization]") - print(f" - CacheAligner normalized whitespace") + print("\n[After Optimization]") + print(" - CacheAligner normalized whitespace") print(f" - Transforms: {plan.transforms}") client.close() @@ -433,6 +450,7 @@ Question: What are the key technical requirements?"""), # SCENARIO 4: Real API Comparison (if API key available) # ============================================================================ + def scenario_live_api() -> ComparisonResult | None: """ Scenario: Live API comparison with actual timing. @@ -497,7 +515,7 @@ def scenario_live_api() -> ComparisonResult | None: print(f"\n[Latency] {latency_before:.0f}ms -> {latency_after:.0f}ms") # Get metrics - summary = headroom_client.get_summary() + headroom_client.get_summary() headroom_client.close() @@ -524,6 +542,7 @@ def scenario_live_api() -> ComparisonResult | None: # MAIN: Run All Scenarios # ============================================================================ + def main(): """Run all comparison scenarios.""" print("\n" + "=" * 70) @@ -585,7 +604,7 @@ def main(): print(f" Total tokens saved: {total_saved:,}") print(f" Average savings: {avg_savings:.1f}%") print(f" Total cost saved: ${total_cost_saved:.4f}") - print(f"\n[Projection] At scale (1M requests/month):") + print("\n[Projection] At scale (1M requests/month):") print(f" Estimated monthly savings: ${total_cost_saved * 1_000_000 / len(results):,.2f}") diff --git a/examples/langchain_demo/mock_tools.py b/examples/langchain_demo/mock_tools.py index 4b9064842..a220eff5c 100644 --- a/examples/langchain_demo/mock_tools.py +++ b/examples/langchain_demo/mock_tools.py @@ -10,7 +10,6 @@ These simulate real-world API responses that benefit from Headroom compression: import json import random from datetime import datetime, timedelta -from typing import Any def generate_user_database_results(query: str, count: int = 100) -> str: @@ -39,9 +38,11 @@ def generate_user_database_results(query: str, count: int = 100) -> str: "notifications": random.choice([True, False]), "timezone": random.choice(["UTC", "PST", "EST", "CST"]), }, - "tags": random.sample(["premium", "verified", "beta", "enterprise"], k=random.randint(0, 3)), + "tags": random.sample( + ["premium", "verified", "beta", "enterprise"], k=random.randint(0, 3) + ), "login_count": random.randint(1, 500), - } + }, } users.append(user) @@ -61,8 +62,8 @@ def generate_search_results(query: str, count: int = 50) -> str: result = { "id": f"doc_{random.randint(10000, 99999)}", "title": f"Document {i}: {query.title()} Guide", - "snippet": f"This document covers {query}. " * random.randint(2, 5) + - f"Learn more about implementing {query} in your application...", + "snippet": f"This document covers {query}. " * random.randint(2, 5) + + f"Learn more about implementing {query} in your application...", "url": f"https://docs.example.com/{query.replace(' ', '-')}/{i}", "category": random.choice(categories), "relevance_score": round(random.uniform(0.5, 1.0), 3), @@ -88,23 +89,27 @@ def generate_log_entries(service: str, count: int = 200) -> str: entries = [] levels = ["DEBUG", "INFO", "INFO", "INFO", "WARN", "ERROR"] # Most are INFO - for i in range(count): + for _i in range(count): timestamp = datetime.now() - timedelta(minutes=random.randint(1, 1440)) level = random.choice(levels) if level == "ERROR": - message = random.choice([ - f"Connection refused to {service}-db: timeout after 30s", - f"Failed to process request: NullPointerException at line 42", - f"Authentication failed for user: invalid token", - f"Rate limit exceeded: 429 Too Many Requests", - ]) + message = random.choice( + [ + f"Connection refused to {service}-db: timeout after 30s", + "Failed to process request: NullPointerException at line 42", + "Authentication failed for user: invalid token", + "Rate limit exceeded: 429 Too Many Requests", + ] + ) elif level == "WARN": - message = random.choice([ - f"Slow query detected: took 2.5s", - f"Memory usage high: 85% of heap", - f"Retrying request after transient failure", - ]) + message = random.choice( + [ + "Slow query detected: took 2.5s", + "Memory usage high: 85% of heap", + "Retrying request after transient failure", + ] + ) else: message = f"Processing request {random.randint(1000, 9999)} for {service}" @@ -120,7 +125,7 @@ def generate_log_entries(service: str, count: int = 200) -> str: "request_id": f"req_{random.randint(100000, 999999)}", "user_agent": "Mozilla/5.0" if random.random() > 0.5 else "API-Client/1.0", "duration_ms": random.randint(1, 5000), - } + }, } entries.append(entry) @@ -154,7 +159,9 @@ def generate_metrics_data(service: str, count: int = 100) -> str: "error_rate": random.uniform(5, 15) if is_anomaly else random.uniform(0, 1), "latency_p50_ms": random.randint(200, 500) if is_anomaly else random.randint(10, 50), "latency_p99_ms": random.randint(1000, 3000) if is_anomaly else random.randint(50, 200), - "active_connections": random.randint(500, 1000) if is_anomaly else random.randint(50, 150), + "active_connections": random.randint(500, 1000) + if is_anomaly + else random.randint(50, 150), } metrics.append(metric) @@ -184,24 +191,29 @@ def generate_api_response(endpoint: str, count: int = 75) -> str: "name": f"Owner {random.randint(1, 100)}", "email": f"owner{random.randint(1, 100)}@example.com", }, - "tags": random.sample(["urgent", "review", "approved", "blocked", "in-progress"], k=random.randint(1, 3)), + "tags": random.sample( + ["urgent", "review", "approved", "blocked", "in-progress"], k=random.randint(1, 3) + ), "metadata": { "source": random.choice(["web", "api", "mobile", "import"]), "version": f"v{random.randint(1, 5)}.{random.randint(0, 9)}", - } + }, } items.append(item) - return json.dumps({ - "data": items, - "pagination": { - "page": 1, - "per_page": count, - "total": count * 10, # Simulate more pages available - "total_pages": 10, + return json.dumps( + { + "data": items, + "pagination": { + "page": 1, + "per_page": count, + "total": count * 10, # Simulate more pages available + "total_pages": 10, + }, + "endpoint": endpoint, }, - "endpoint": endpoint, - }, indent=2) + indent=2, + ) # Tool definitions for LangChain @@ -217,6 +229,7 @@ TOOL_FUNCTIONS = { if __name__ == "__main__": # Test output sizes import tiktoken + enc = tiktoken.get_encoding("cl100k_base") print("Tool Output Token Counts:") diff --git a/examples/langchain_demo/run_comparison.py b/examples/langchain_demo/run_comparison.py index 121f0f927..c771cf6bf 100644 --- a/examples/langchain_demo/run_comparison.py +++ b/examples/langchain_demo/run_comparison.py @@ -20,7 +20,6 @@ import os import sys import time from dataclasses import dataclass -from typing import Any # Check for required dependencies try: @@ -30,9 +29,14 @@ except ImportError: sys.exit(1) try: - from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolMessage - from langchain_core.tools import tool - from langchain_openai import ChatOpenAI + from langchain_core.messages import ( # noqa: F401 + AIMessage, + HumanMessage, + SystemMessage, + ToolMessage, + ) + from langchain_core.tools import tool # noqa: F401 + from langchain_openai import ChatOpenAI # noqa: F401 except ImportError: print("ERROR: LangChain required. Run: pip install langchain langchain-openai langchain-core") sys.exit(1) @@ -40,7 +44,6 @@ except ImportError: # Import our mock tools from .mock_tools import TOOL_FUNCTIONS - # Token counter ENCODER = tiktoken.get_encoding("cl100k_base") @@ -71,6 +74,7 @@ def count_message_tokens(messages: list[dict]) -> int: @dataclass class AgentRun: """Results from a single agent run.""" + scenario: str mode: str # "baseline" or "headroom" total_input_tokens: int @@ -185,7 +189,7 @@ def run_agent_baseline(scenario: dict, api_key: str) -> AgentRun: # Count output tokens output_tokens = count_tokens(response.content) if response.content else 0 if response.tool_calls: - output_tokens += count_tokens(json.dumps([tc for tc in response.tool_calls])) + output_tokens += count_tokens(json.dumps(list(response.tool_calls))) total_output_tokens += output_tokens # Check if done @@ -212,10 +216,12 @@ def run_agent_baseline(scenario: dict, api_key: str) -> AgentRun: tool_output_tokens += tool_tokens # Add tool result - messages.append(ToolMessage( - content=result, - tool_call_id=tool_call["id"], - )) + messages.append( + ToolMessage( + content=result, + tool_call_id=tool_call["id"], + ) + ) duration_ms = (time.time() - start_time) * 1000 @@ -251,7 +257,7 @@ def run_agent_headroom(scenario: dict, api_key: str) -> AgentRun: # Wrap with Headroom config = HeadroomConfig( smart_crusher_threshold=500, # Compress tool outputs > 500 tokens - smart_crusher_max_items=20, # Keep max 20 items + smart_crusher_max_items=20, # Keep max 20 items cache_alignment=True, rolling_window=True, ) @@ -287,7 +293,7 @@ def run_agent_headroom(scenario: dict, api_key: str) -> AgentRun: # Count output tokens output_tokens = count_tokens(response.content) if response.content else 0 if response.tool_calls: - output_tokens += count_tokens(json.dumps([tc for tc in response.tool_calls])) + output_tokens += count_tokens(json.dumps(list(response.tool_calls))) total_output_tokens += output_tokens # Check if done @@ -311,10 +317,12 @@ def run_agent_headroom(scenario: dict, api_key: str) -> AgentRun: tool_tokens = count_tokens(result) tool_output_tokens += tool_tokens - messages.append(ToolMessage( - content=result, - tool_call_id=tool_call["id"], - )) + messages.append( + ToolMessage( + content=result, + tool_call_id=tool_call["id"], + ) + ) duration_ms = (time.time() - start_time) * 1000 @@ -337,41 +345,59 @@ def run_agent_headroom(scenario: dict, api_key: str) -> AgentRun: def print_comparison(baseline: AgentRun, headroom: AgentRun): """Print comparison between baseline and headroom runs.""" - print(f"\n{'='*70}") + print(f"\n{'=' * 70}") print(f"SCENARIO: {baseline.scenario}") - print(f"{'='*70}") + print(f"{'=' * 70}") # Token comparison input_saved = baseline.total_input_tokens - headroom.total_input_tokens - input_pct = (input_saved / baseline.total_input_tokens * 100) if baseline.total_input_tokens > 0 else 0 + input_pct = ( + (input_saved / baseline.total_input_tokens * 100) if baseline.total_input_tokens > 0 else 0 + ) print(f"\n{'METRIC':<30} {'BASELINE':>15} {'HEADROOM':>15} {'SAVINGS':>15}") print("-" * 75) - print(f"{'Input Tokens':<30} {baseline.total_input_tokens:>15,} {headroom.total_input_tokens:>15,} {input_saved:>14,} ({input_pct:.1f}%)") - print(f"{'Output Tokens':<30} {baseline.total_output_tokens:>15,} {headroom.total_output_tokens:>15,} {'N/A':>15}") - print(f"{'Tool Output Tokens':<30} {baseline.tool_output_tokens:>15,} {headroom.tool_output_tokens:>15,} {'(raw)':>15}") + print( + f"{'Input Tokens':<30} {baseline.total_input_tokens:>15,} {headroom.total_input_tokens:>15,} {input_saved:>14,} ({input_pct:.1f}%)" + ) + print( + f"{'Output Tokens':<30} {baseline.total_output_tokens:>15,} {headroom.total_output_tokens:>15,} {'N/A':>15}" + ) + print( + f"{'Tool Output Tokens':<30} {baseline.tool_output_tokens:>15,} {headroom.tool_output_tokens:>15,} {'(raw)':>15}" + ) print(f"{'Tool Calls':<30} {baseline.tool_calls:>15} {headroom.tool_calls:>15} {'':>15}") print(f"{'Messages':<30} {baseline.messages_count:>15} {headroom.messages_count:>15} {'':>15}") - print(f"{'Duration (ms)':<30} {baseline.duration_ms:>15.0f} {headroom.duration_ms:>15.0f} {'':>15}") + print( + f"{'Duration (ms)':<30} {baseline.duration_ms:>15.0f} {headroom.duration_ms:>15.0f} {'':>15}" + ) # Cost estimation (gpt-4o-mini pricing) input_cost_per_1m = 0.15 output_cost_per_1m = 0.60 - baseline_cost = (baseline.total_input_tokens * input_cost_per_1m + baseline.total_output_tokens * output_cost_per_1m) / 1_000_000 - headroom_cost = (headroom.total_input_tokens * input_cost_per_1m + headroom.total_output_tokens * output_cost_per_1m) / 1_000_000 + baseline_cost = ( + baseline.total_input_tokens * input_cost_per_1m + + baseline.total_output_tokens * output_cost_per_1m + ) / 1_000_000 + headroom_cost = ( + headroom.total_input_tokens * input_cost_per_1m + + headroom.total_output_tokens * output_cost_per_1m + ) / 1_000_000 cost_saved = baseline_cost - headroom_cost cost_pct = (cost_saved / baseline_cost * 100) if baseline_cost > 0 else 0 - print(f"\n{'Estimated Cost (USD)':<30} ${baseline_cost:>14.6f} ${headroom_cost:>14.6f} ${cost_saved:>13.6f} ({cost_pct:.1f}%)") + print( + f"\n{'Estimated Cost (USD)':<30} ${baseline_cost:>14.6f} ${headroom_cost:>14.6f} ${cost_saved:>13.6f} ({cost_pct:.1f}%)" + ) def main(): """Run the before/after comparison.""" - print("\n" + "="*70) + print("\n" + "=" * 70) print("LANGCHAIN AGENT: BEFORE/AFTER HEADROOM COMPARISON") - print("="*70) + print("=" * 70) # Check for API key api_key = os.environ.get("OPENAI_API_KEY") @@ -431,15 +457,17 @@ def run_simulation(): print(f"\n Total tool output: {total_tool_tokens:,} tokens") print(f" With 3 iterations, baseline input would be: ~{total_tool_tokens * 2:,} tokens") print(f" With Headroom (20 items max), estimated: ~{total_tool_tokens // 5:,} tokens") - print(f" Estimated savings: ~{total_tool_tokens * 2 - total_tool_tokens // 5:,} tokens (~80%)") + print( + f" Estimated savings: ~{total_tool_tokens * 2 - total_tool_tokens // 5:,} tokens (~80%)" + ) def print_summary(baseline_runs: list[AgentRun], headroom_runs: list[AgentRun]): """Print overall summary.""" - print("\n" + "="*70) + print("\n" + "=" * 70) print("OVERALL SUMMARY") - print("="*70) + print("=" * 70) total_baseline_input = sum(r.total_input_tokens for r in baseline_runs) total_headroom_input = sum(r.total_input_tokens for r in headroom_runs) @@ -448,7 +476,9 @@ def print_summary(baseline_runs: list[AgentRun], headroom_runs: list[AgentRun]): print(f"\n{'Metric':<30} {'Baseline':>15} {'Headroom':>15} {'Savings':>15}") print("-" * 75) - print(f"{'Total Input Tokens':<30} {total_baseline_input:>15,} {total_headroom_input:>15,} {total_saved:>14,}") + print( + f"{'Total Input Tokens':<30} {total_baseline_input:>15,} {total_headroom_input:>15,} {total_saved:>14,}" + ) print(f"{'Percentage Saved':<30} {'':>15} {'':>15} {pct_saved:>14.1f}%") # Cost @@ -457,11 +487,13 @@ def print_summary(baseline_runs: list[AgentRun], headroom_runs: list[AgentRun]): headroom_cost = total_headroom_input * input_cost cost_saved = baseline_cost - headroom_cost - print(f"\n{'Est. Input Cost (USD)':<30} ${baseline_cost:>14.4f} ${headroom_cost:>14.4f} ${cost_saved:>13.4f}") + print( + f"\n{'Est. Input Cost (USD)':<30} ${baseline_cost:>14.4f} ${headroom_cost:>14.4f} ${cost_saved:>13.4f}" + ) - print("\n" + "="*70) + print("\n" + "=" * 70) print("CONCLUSION") - print("="*70) + print("=" * 70) print(f""" Headroom reduced input tokens by {pct_saved:.1f}% across all scenarios. diff --git a/examples/langchain_demo/show_compression.py b/examples/langchain_demo/show_compression.py index 5973476e6..1043b0902 100644 --- a/examples/langchain_demo/show_compression.py +++ b/examples/langchain_demo/show_compression.py @@ -19,13 +19,11 @@ except ImportError: print("ERROR: tiktoken required. Run: uv pip install tiktoken") sys.exit(1) -from headroom import HeadroomConfig -from headroom.transforms import SmartCrusher from headroom.providers import OpenAIProvider +from headroom.transforms import SmartCrusher from .mock_tools import TOOL_FUNCTIONS - ENCODER = tiktoken.get_encoding("cl100k_base") @@ -37,10 +35,10 @@ def count_tokens(text: str) -> int: def demonstrate_compression(tool_name: str, tool_arg: str, context: str): """Show before/after compression for a tool output.""" - print(f"\n{'='*70}") + print(f"\n{'=' * 70}") print(f"TOOL: {tool_name}({tool_arg!r})") print(f"CONTEXT: {context!r}") - print(f"{'='*70}") + print(f"{'=' * 70}") # Generate tool output raw_output = TOOL_FUNCTIONS[tool_name](tool_arg) @@ -59,11 +57,11 @@ def demonstrate_compression(tool_name: str, tool_arg: str, context: str): else: item_count = "?" - print(f"\n--- BEFORE COMPRESSION ---") + print("\n--- BEFORE COMPRESSION ---") print(f"Items: {item_count}") print(f"Tokens: {raw_tokens:,}") print(f"Chars: {len(raw_output):,}") - print(f"\nFirst 500 chars:") + print("\nFirst 500 chars:") print(raw_output[:500] + "...") # Create SmartCrusher with context @@ -84,7 +82,19 @@ def demonstrate_compression(tool_name: str, tool_arg: str, context: str): messages = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": context}, - {"role": "assistant", "content": None, "tool_calls": [{"id": "call_1", "function": {"name": tool_name, "arguments": json.dumps({tool_name.split("_")[-1]: tool_arg})}}]}, + { + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call_1", + "function": { + "name": tool_name, + "arguments": json.dumps({tool_name.split("_")[-1]: tool_arg}), + }, + } + ], + }, {"role": "tool", "content": raw_output, "tool_call_id": "call_1"}, ] @@ -112,18 +122,18 @@ def demonstrate_compression(tool_name: str, tool_arg: str, context: str): except json.JSONDecodeError: compressed_items = "N/A" - print(f"\n--- AFTER COMPRESSION ---") + print("\n--- AFTER COMPRESSION ---") print(f"Items: {compressed_items}") print(f"Tokens: {compressed_tokens:,}") print(f"Chars: {len(compressed_output):,}") - print(f"\nFirst 500 chars:") + print("\nFirst 500 chars:") print(compressed_output[:500] + "...") # Calculate savings tokens_saved = raw_tokens - compressed_tokens pct_saved = (tokens_saved / raw_tokens * 100) if raw_tokens > 0 else 0 - print(f"\n--- SAVINGS ---") + print("\n--- SAVINGS ---") print(f"Tokens saved: {tokens_saved:,} ({pct_saved:.1f}%)") print(f"Items reduced: {item_count} -> {compressed_items}") @@ -139,9 +149,9 @@ def demonstrate_compression(tool_name: str, tool_arg: str, context: str): def main(): """Run compression demonstrations.""" - print("\n" + "="*70) + print("\n" + "=" * 70) print("HEADROOM SMARTCRUSHER: BEFORE/AFTER COMPRESSION") - print("="*70) + print("=" * 70) print(""" This demonstrates how Headroom's SmartCrusher compresses large tool outputs. @@ -156,44 +166,54 @@ Key techniques: results = [] # Demo 1: User database search - results.append(demonstrate_compression( - tool_name="search_users", - tool_arg="Engineering users", - context="Find all users in the Engineering department who are currently active", - )) + results.append( + demonstrate_compression( + tool_name="search_users", + tool_arg="Engineering users", + context="Find all users in the Engineering department who are currently active", + ) + ) # Demo 2: Log search with errors - results.append(demonstrate_compression( - tool_name="search_logs", - tool_arg="payment-service", - context="Check the payment-service logs for any ERROR entries", - )) + results.append( + demonstrate_compression( + tool_name="search_logs", + tool_arg="payment-service", + context="Check the payment-service logs for any ERROR entries", + ) + ) # Demo 3: Metrics with anomalies - results.append(demonstrate_compression( - tool_name="get_metrics", - tool_arg="api-gateway", - context="Look for any CPU spikes or high error rates in the api-gateway metrics", - )) + results.append( + demonstrate_compression( + tool_name="get_metrics", + tool_arg="api-gateway", + context="Look for any CPU spikes or high error rates in the api-gateway metrics", + ) + ) # Demo 4: Documentation search - results.append(demonstrate_compression( - tool_name="search_docs", - tool_arg="authentication", - context="Find documentation about authentication troubleshooting", - )) + results.append( + demonstrate_compression( + tool_name="search_docs", + tool_arg="authentication", + context="Find documentation about authentication troubleshooting", + ) + ) # Demo 5: API data - results.append(demonstrate_compression( - tool_name="fetch_api_data", - tool_arg="orders", - context="Get recent orders with status 'pending'", - )) + results.append( + demonstrate_compression( + tool_name="fetch_api_data", + tool_arg="orders", + context="Get recent orders with status 'pending'", + ) + ) # Summary - print("\n" + "="*70) + print("\n" + "=" * 70) print("SUMMARY: TOKEN SAVINGS ACROSS ALL TOOLS") - print("="*70) + print("=" * 70) print(f"\n{'Tool':<20} {'Before':>12} {'After':>12} {'Saved':>12} {'%':>8}") print("-" * 66) @@ -202,7 +222,9 @@ Key techniques: total_after = 0 for r in results: - print(f"{r['tool']:<20} {r['before_tokens']:>12,} {r['after_tokens']:>12,} {r['saved_tokens']:>12,} {r['saved_pct']:>7.1f}%") + print( + f"{r['tool']:<20} {r['before_tokens']:>12,} {r['after_tokens']:>12,} {r['saved_tokens']:>12,} {r['saved_pct']:>7.1f}%" + ) total_before += r["before_tokens"] total_after += r["after_tokens"] @@ -210,7 +232,9 @@ Key techniques: total_pct = (total_saved / total_before * 100) if total_before > 0 else 0 print("-" * 66) - print(f"{'TOTAL':<20} {total_before:>12,} {total_after:>12,} {total_saved:>12,} {total_pct:>7.1f}%") + print( + f"{'TOTAL':<20} {total_before:>12,} {total_after:>12,} {total_saved:>12,} {total_pct:>7.1f}%" + ) # Cost savings input_cost_per_1m = 2.50 # gpt-4o pricing @@ -218,11 +242,13 @@ Key techniques: cost_after = total_after * input_cost_per_1m / 1_000_000 cost_saved = cost_before - cost_after - print(f"\n--- COST IMPACT (at gpt-4o $2.50/1M input tokens) ---") + print("\n--- COST IMPACT (at gpt-4o $2.50/1M input tokens) ---") print(f"Before: ${cost_before:.4f}") print(f"After: ${cost_after:.4f}") print(f"Saved: ${cost_saved:.4f} per request") - print(f"\nAt 1000 requests/day: ${cost_saved * 1000:.2f}/day = ${cost_saved * 1000 * 30:.2f}/month") + print( + f"\nAt 1000 requests/day: ${cost_saved * 1000:.2f}/day = ${cost_saved * 1000 * 30:.2f}/month" + ) if __name__ == "__main__": diff --git a/examples/langchain_demo/verify_errors_kept.py b/examples/langchain_demo/verify_errors_kept.py index 729099a9e..216893b4d 100644 --- a/examples/langchain_demo/verify_errors_kept.py +++ b/examples/langchain_demo/verify_errors_kept.py @@ -6,16 +6,16 @@ This is critical - errors should NEVER be dropped during compression. import json from headroom.config import SmartCrusherConfig -from headroom.transforms import SmartCrusher from headroom.providers import OpenAIProvider +from headroom.transforms import SmartCrusher from .mock_tools import generate_log_entries def main(): - print("\n" + "="*70) + print("\n" + "=" * 70) print("VERIFYING ERROR PRESERVATION IN SMARTCRUSHER") - print("="*70) + print("=" * 70) # Generate logs with some ERROR entries raw_output = generate_log_entries("test-service", count=200) @@ -43,7 +43,13 @@ def main(): messages = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Find ERROR entries in the logs"}, - {"role": "assistant", "content": None, "tool_calls": [{"id": "call_1", "function": {"name": "search_logs", "arguments": "{}"}}]}, + { + "role": "assistant", + "content": None, + "tool_calls": [ + {"id": "call_1", "function": {"name": "search_logs", "arguments": "{}"}} + ], + }, {"role": "tool", "content": raw_output, "tool_call_id": "call_1"}, ] @@ -57,11 +63,12 @@ def main(): except json.JSONDecodeError: # Try to extract just the JSON object import re - json_match = re.search(r'(\{.*\})', compressed_output, re.DOTALL) + + json_match = re.search(r"(\{.*\})", compressed_output, re.DOTALL) if json_match: compressed_data = json.loads(json_match.group(1)) else: - print(f"Could not parse compressed output:") + print("Could not parse compressed output:") print(compressed_output[:500]) return @@ -74,20 +81,22 @@ def main(): print(f" - {err['message'][:60]}...") # Verification - print("\n" + "="*70) + print("\n" + "=" * 70) if len(compressed_errors) >= len(original_errors): print("SUCCESS: All ERROR entries were preserved!") elif len(compressed_errors) > 0: print(f"PARTIAL: {len(compressed_errors)}/{len(original_errors)} ERROR entries preserved") else: print("FAILURE: ERROR entries were dropped!") - print("="*70) + print("=" * 70) # Show compression ratio original_count = len(data["entries"]) compressed_count = len(compressed_data["entries"]) reduction = (original_count - compressed_count) / original_count * 100 - print(f"\nCompression: {original_count} → {compressed_count} entries ({reduction:.1f}% reduction)") + print( + f"\nCompression: {original_count} → {compressed_count} entries ({reduction:.1f}% reduction)" + ) print(f"But kept: {len(compressed_errors)} of {len(original_errors)} ERROR entries") diff --git a/examples/mcp_demo/mock_mcp_servers.py b/examples/mcp_demo/mock_mcp_servers.py index 4059d9e94..f9ab15fea 100644 --- a/examples/mcp_demo/mock_mcp_servers.py +++ b/examples/mcp_demo/mock_mcp_servers.py @@ -22,41 +22,50 @@ def generate_slack_search_results(query: str, count: int = 150) -> str: # 15% chance of error-related message is_error = random.random() < 0.15 if is_error: - text = random.choice([ - "ERROR: Database connection pool exhausted at 3:45am", - "CRITICAL: Memory usage at 95% on prod-api-01", - "Exception in PaymentService.processTransaction()", - "FAILED: Deploy pipeline broke - rolling back", - "ALERT: Latency spike detected on /api/users endpoint", - ]) + text = random.choice( + [ + "ERROR: Database connection pool exhausted at 3:45am", + "CRITICAL: Memory usage at 95% on prod-api-01", + "Exception in PaymentService.processTransaction()", + "FAILED: Deploy pipeline broke - rolling back", + "ALERT: Latency spike detected on /api/users endpoint", + ] + ) else: - text = random.choice([ - f"Reviewed the PR for {query}, looks good to merge", - f"Updated the docs with new {query} endpoints", - "Meeting notes from standup attached", - "Can someone review my changes to the auth module?", - "Deployed v2.3.1 to staging environment", - "Thanks for the feedback on the design doc!", - "Working on the feature request from yesterday", - ]) + text = random.choice( + [ + f"Reviewed the PR for {query}, looks good to merge", + f"Updated the docs with new {query} endpoints", + "Meeting notes from standup attached", + "Can someone review my changes to the auth module?", + "Deployed v2.3.1 to staging environment", + "Thanks for the feedback on the design doc!", + "Working on the feature request from yesterday", + ] + ) - messages.append({ - "id": f"msg_{i}", - "channel": random.choice(channels), - "user": random.choice(users), - "text": text, - "timestamp": (datetime.now() - timedelta(hours=i)).isoformat(), - "reactions": random.randint(0, 15), - "thread_replies": random.randint(0, 10), - "permalink": f"https://slack.com/archives/C123/p{i}", - }) + messages.append( + { + "id": f"msg_{i}", + "channel": random.choice(channels), + "user": random.choice(users), + "text": text, + "timestamp": (datetime.now() - timedelta(hours=i)).isoformat(), + "reactions": random.randint(0, 15), + "thread_replies": random.randint(0, 10), + "permalink": f"https://slack.com/archives/C123/p{i}", + } + ) - return json.dumps({ - "query": query, - "messages": messages, - "total": count, - "has_more": count > 100, - }, indent=2) + return json.dumps( + { + "query": query, + "messages": messages, + "total": count, + "has_more": count > 100, + }, + indent=2, + ) def generate_database_query_results(query: str, count: int = 200) -> str: @@ -72,7 +81,9 @@ def generate_database_query_results(query: str, count: int = 200) -> str: "user_id": f"usr_{random.randint(10000, 99999)}", "email": f"user{i}@example.com", "full_name": f"User {i}", - "status": "ERROR: validation_failed" if has_error else random.choice(["active", "inactive", "pending"]), + "status": "ERROR: validation_failed" + if has_error + else random.choice(["active", "inactive", "pending"]), "created_at": (datetime.now() - timedelta(days=random.randint(1, 365))).isoformat(), "last_login": (datetime.now() - timedelta(days=random.randint(0, 30))).isoformat(), "balance": None if has_null else round(random.uniform(0, 10000), 2), @@ -81,17 +92,19 @@ def generate_database_query_results(query: str, count: int = 200) -> str: } rows.append(row) - return json.dumps({ - "query": query, - "rows": rows, - "count": count, - "execution_time_ms": random.randint(50, 500), - }, indent=2) + return json.dumps( + { + "query": query, + "rows": rows, + "count": count, + "execution_time_ms": random.randint(50, 500), + }, + indent=2, + ) def generate_log_search_results(service: str, count: int = 300) -> str: """Simulate log analysis MCP server results.""" - levels = ["DEBUG", "INFO", "WARN", "ERROR", "FATAL"] services = [service, f"{service}-worker", f"{service}-scheduler", "auth-service"] entries = [] @@ -99,34 +112,40 @@ def generate_log_search_results(service: str, count: int = 300) -> str: # 20% error rate (ERROR or FATAL) if random.random() < 0.20: level = random.choice(["ERROR", "FATAL"]) - message = random.choice([ - "Connection timeout to primary database", - "Failed to process message from queue", - "Authentication failed: invalid token", - "Out of memory error in request handler", - "Unhandled exception: NullPointerException", - "Circuit breaker open for external-api", - ]) + message = random.choice( + [ + "Connection timeout to primary database", + "Failed to process message from queue", + "Authentication failed: invalid token", + "Out of memory error in request handler", + "Unhandled exception: NullPointerException", + "Circuit breaker open for external-api", + ] + ) else: level = random.choice(["DEBUG", "INFO", "INFO", "INFO", "WARN"]) - message = random.choice([ - "Request processed successfully", - "Cache hit for user session", - "Starting scheduled job: cleanup", - "Connection pool stats: 10/20 active", - "Metrics exported to datadog", - "Health check passed", - ]) + message = random.choice( + [ + "Request processed successfully", + "Cache hit for user session", + "Starting scheduled job: cleanup", + "Connection pool stats: 10/20 active", + "Metrics exported to datadog", + "Health check passed", + ] + ) - entries.append({ - "timestamp": (datetime.now() - timedelta(minutes=i)).isoformat(), - "level": level, - "service": random.choice(services), - "message": message, - "trace_id": f"trace_{random.randint(100000, 999999)}", - "span_id": f"span_{random.randint(1000, 9999)}", - "host": f"prod-{random.choice(['api', 'worker', 'web'])}-{random.randint(1, 10):02d}", - }) + entries.append( + { + "timestamp": (datetime.now() - timedelta(minutes=i)).isoformat(), + "level": level, + "service": random.choice(services), + "message": message, + "trace_id": f"trace_{random.randint(100000, 999999)}", + "span_id": f"span_{random.randint(1000, 9999)}", + "host": f"prod-{random.choice(['api', 'worker', 'web'])}-{random.randint(1, 10):02d}", + } + ) return json.dumps({"entries": entries, "service": service}, indent=2) @@ -140,25 +159,36 @@ def generate_github_issues_results(repo: str, count: int = 100) -> str: for i in range(count): # 25% bug rate is_bug = random.random() < 0.25 - labels = random.sample(bug_labels, k=random.randint(1, 2)) if is_bug else random.sample(labels_pool, k=random.randint(0, 2)) + labels = ( + random.sample(bug_labels, k=random.randint(1, 2)) + if is_bug + else random.sample(labels_pool, k=random.randint(0, 2)) + ) - issues.append({ - "number": i + 1, - "title": f"{'[BUG] ' if is_bug else ''}{random.choice(['Fix auth flow', 'Add dark mode', 'Update docs', 'Improve perf'])}", - "state": random.choice(["open", "open", "closed"]), - "labels": labels, - "author": f"contributor{random.randint(1, 50)}", - "assignee": f"maintainer{random.randint(1, 5)}" if random.random() > 0.3 else None, - "created_at": (datetime.now() - timedelta(days=random.randint(1, 90))).isoformat(), - "updated_at": (datetime.now() - timedelta(days=random.randint(0, 30))).isoformat(), - "comments": random.randint(0, 30), - "body": "Lorem ipsum dolor sit amet..." if random.random() > 0.5 else "", - "milestone": f"v{random.randint(1, 3)}.{random.randint(0, 9)}" if random.random() > 0.7 else None, - }) + issues.append( + { + "number": i + 1, + "title": f"{'[BUG] ' if is_bug else ''}{random.choice(['Fix auth flow', 'Add dark mode', 'Update docs', 'Improve perf'])}", + "state": random.choice(["open", "open", "closed"]), + "labels": labels, + "author": f"contributor{random.randint(1, 50)}", + "assignee": f"maintainer{random.randint(1, 5)}" if random.random() > 0.3 else None, + "created_at": (datetime.now() - timedelta(days=random.randint(1, 90))).isoformat(), + "updated_at": (datetime.now() - timedelta(days=random.randint(0, 30))).isoformat(), + "comments": random.randint(0, 30), + "body": "Lorem ipsum dolor sit amet..." if random.random() > 0.5 else "", + "milestone": f"v{random.randint(1, 3)}.{random.randint(0, 9)}" + if random.random() > 0.7 + else None, + } + ) - return json.dumps({ - "repository": repo, - "issues": issues, - "total_count": count, - "open_count": sum(1 for i in issues if i["state"] == "open"), - }, indent=2) + return json.dumps( + { + "repository": repo, + "issues": issues, + "total_count": count, + "open_count": sum(1 for i in issues if i["state"] == "open"), + }, + indent=2, + ) diff --git a/examples/mcp_demo/run_agent_eval.py b/examples/mcp_demo/run_agent_eval.py index f015e4dbd..e8a0b5b8b 100644 --- a/examples/mcp_demo/run_agent_eval.py +++ b/examples/mcp_demo/run_agent_eval.py @@ -24,21 +24,34 @@ from headroom.providers import OpenAIProvider # Test Data Generators (Deterministic for eval reproducibility) # ============================================================================ + def generate_slack_with_specific_errors(seed: int = 42) -> tuple[str, list[dict]]: """Generate Slack messages with SPECIFIC errors we'll query for.""" random.seed(seed) # These are the "needle" errors we'll ask the agent to find critical_errors = [ - {"id": "msg_17", "channel": "#incidents", "user": "alice", - "text": "CRITICAL: Payment service is DOWN - customers cannot checkout. Error: ConnectionRefused to payment-db-01", - "timestamp": "2025-01-06T03:45:00Z"}, - {"id": "msg_42", "channel": "#alerts", "user": "bob", - "text": "ERROR: Auth service returning 500s. Stack trace shows NullPointerException in TokenValidator.java:127", - "timestamp": "2025-01-06T02:30:00Z"}, - {"id": "msg_89", "channel": "#engineering", "user": "charlie", - "text": "FAILED: Deploy to prod-us-east failed. Reason: Health check timeout after 300s on api-gateway-03", - "timestamp": "2025-01-05T23:15:00Z"}, + { + "id": "msg_17", + "channel": "#incidents", + "user": "alice", + "text": "CRITICAL: Payment service is DOWN - customers cannot checkout. Error: ConnectionRefused to payment-db-01", + "timestamp": "2025-01-06T03:45:00Z", + }, + { + "id": "msg_42", + "channel": "#alerts", + "user": "bob", + "text": "ERROR: Auth service returning 500s. Stack trace shows NullPointerException in TokenValidator.java:127", + "timestamp": "2025-01-06T02:30:00Z", + }, + { + "id": "msg_89", + "channel": "#engineering", + "user": "charlie", + "text": "FAILED: Deploy to prod-us-east failed. Reason: Health check timeout after 300s on api-gateway-03", + "timestamp": "2025-01-05T23:15:00Z", + }, ] # Generate noise messages @@ -62,13 +75,15 @@ def generate_slack_with_specific_errors(seed: int = 42) -> tuple[str, list[dict] messages.append(critical_errors[error_idx]) error_idx += 1 else: - messages.append({ - "id": f"msg_{i}", - "channel": random.choice(channels), - "user": random.choice(users), - "text": random.choice(noise_messages), - "timestamp": (datetime.now() - timedelta(hours=i)).isoformat(), - }) + messages.append( + { + "id": f"msg_{i}", + "channel": random.choice(channels), + "user": random.choice(users), + "text": random.choice(noise_messages), + "timestamp": (datetime.now() - timedelta(hours=i)).isoformat(), + } + ) return json.dumps({"messages": messages, "total": 150}), critical_errors @@ -79,17 +94,43 @@ def generate_logs_with_specific_errors(seed: int = 43) -> tuple[str, list[dict]] # These are the "needle" errors critical_logs = [ - {"timestamp": "2025-01-06T03:44:58Z", "level": "FATAL", "service": "payment-service", - "message": "Cannot connect to payment-db-01: Connection refused", "trace_id": "trace_payment_001"}, - {"timestamp": "2025-01-06T02:29:55Z", "level": "ERROR", "service": "auth-service", - "message": "NullPointerException in TokenValidator.validate() at line 127", "trace_id": "trace_auth_001"}, - {"timestamp": "2025-01-05T23:14:30Z", "level": "ERROR", "service": "api-gateway", - "message": "Health check failed: timeout after 300000ms", "trace_id": "trace_gateway_001"}, - {"timestamp": "2025-01-06T01:00:00Z", "level": "ERROR", "service": "user-service", - "message": "Database query timeout: SELECT * FROM users WHERE last_login > ?", "trace_id": "trace_user_001"}, + { + "timestamp": "2025-01-06T03:44:58Z", + "level": "FATAL", + "service": "payment-service", + "message": "Cannot connect to payment-db-01: Connection refused", + "trace_id": "trace_payment_001", + }, + { + "timestamp": "2025-01-06T02:29:55Z", + "level": "ERROR", + "service": "auth-service", + "message": "NullPointerException in TokenValidator.validate() at line 127", + "trace_id": "trace_auth_001", + }, + { + "timestamp": "2025-01-05T23:14:30Z", + "level": "ERROR", + "service": "api-gateway", + "message": "Health check failed: timeout after 300000ms", + "trace_id": "trace_gateway_001", + }, + { + "timestamp": "2025-01-06T01:00:00Z", + "level": "ERROR", + "service": "user-service", + "message": "Database query timeout: SELECT * FROM users WHERE last_login > ?", + "trace_id": "trace_user_001", + }, ] - services = ["api-gateway", "auth-service", "payment-service", "user-service", "notification-service"] + services = [ + "api-gateway", + "auth-service", + "payment-service", + "user-service", + "notification-service", + ] info_messages = [ "Request processed successfully", "Cache hit for user session", @@ -105,13 +146,15 @@ def generate_logs_with_specific_errors(seed: int = 43) -> tuple[str, list[dict]] entries.append(critical_logs[error_idx]) error_idx += 1 else: - entries.append({ - "timestamp": (datetime.now() - timedelta(minutes=i)).isoformat(), - "level": random.choice(["DEBUG", "INFO", "INFO", "INFO", "WARN"]), - "service": random.choice(services), - "message": random.choice(info_messages), - "trace_id": f"trace_{random.randint(100000, 999999)}", - }) + entries.append( + { + "timestamp": (datetime.now() - timedelta(minutes=i)).isoformat(), + "level": random.choice(["DEBUG", "INFO", "INFO", "INFO", "WARN"]), + "service": random.choice(services), + "message": random.choice(info_messages), + "trace_id": f"trace_{random.randint(100000, 999999)}", + } + ) return json.dumps({"entries": entries}), critical_logs @@ -122,10 +165,24 @@ def generate_database_with_anomalies(seed: int = 44) -> tuple[str, list[dict]]: # Anomalous records we'll ask about anomalies = [ - {"id": 23, "user_id": "usr_99999", "email": "admin@internal.com", "status": "ERROR: account_locked", - "balance": 999999.99, "login_attempts": 47, "last_login": "2025-01-06T04:00:00Z"}, - {"id": 156, "user_id": "usr_00001", "email": "test@test.com", "status": "ERROR: validation_failed", - "balance": -500.00, "login_attempts": 0, "last_login": None}, + { + "id": 23, + "user_id": "usr_99999", + "email": "admin@internal.com", + "status": "ERROR: account_locked", + "balance": 999999.99, + "login_attempts": 47, + "last_login": "2025-01-06T04:00:00Z", + }, + { + "id": 156, + "user_id": "usr_00001", + "email": "test@test.com", + "status": "ERROR: validation_failed", + "balance": -500.00, + "login_attempts": 0, + "last_login": None, + }, ] rows = [] @@ -135,15 +192,19 @@ def generate_database_with_anomalies(seed: int = 44) -> tuple[str, list[dict]]: rows.append(anomalies[anomaly_idx]) anomaly_idx += 1 else: - rows.append({ - "id": i, - "user_id": f"usr_{random.randint(10000, 99999)}", - "email": f"user{i}@example.com", - "status": random.choice(["active", "active", "active", "inactive", "pending"]), - "balance": round(random.uniform(0, 5000), 2), - "login_attempts": random.randint(0, 5), - "last_login": (datetime.now() - timedelta(days=random.randint(0, 30))).isoformat(), - }) + rows.append( + { + "id": i, + "user_id": f"usr_{random.randint(10000, 99999)}", + "email": f"user{i}@example.com", + "status": random.choice(["active", "active", "active", "inactive", "pending"]), + "balance": round(random.uniform(0, 5000), 2), + "login_attempts": random.randint(0, 5), + "last_login": ( + datetime.now() - timedelta(days=random.randint(0, 30)) + ).isoformat(), + } + ) return json.dumps({"rows": rows, "count": 200}), anomalies @@ -152,9 +213,11 @@ def generate_database_with_anomalies(seed: int = 44) -> tuple[str, list[dict]]: # Eval Test Cases # ============================================================================ + @dataclass class EvalCase: """A single evaluation case.""" + name: str tool_name: str tool_output: str @@ -192,7 +255,13 @@ def create_eval_cases() -> list[EvalCase]: tool_name="mcp__logs__search", tool_output=logs_output, user_query="List all ERROR and FATAL log entries with their services and messages.", - expected_findings=["payment-service", "auth-service", "api-gateway", "Connection refused", "NullPointerException"], + expected_findings=[ + "payment-service", + "auth-service", + "api-gateway", + "Connection refused", + "NullPointerException", + ], critical_data=log_errors, ), EvalCase( @@ -218,6 +287,7 @@ def create_eval_cases() -> list[EvalCase]: # Agent Simulation # ============================================================================ + def run_agent_with_tool_output( client: OpenAI, user_query: str, @@ -230,11 +300,22 @@ def run_agent_with_tool_output( Returns: (answer, tokens_used) """ messages = [ - {"role": "system", "content": "You are a helpful assistant analyzing tool outputs. Be specific and cite exact details from the data."}, + { + "role": "system", + "content": "You are a helpful assistant analyzing tool outputs. Be specific and cite exact details from the data.", + }, {"role": "user", "content": user_query}, - {"role": "assistant", "content": None, "tool_calls": [ - {"id": "call_1", "type": "function", "function": {"name": tool_name, "arguments": "{}"}} - ]}, + { + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call_1", + "type": "function", + "function": {"name": tool_name, "arguments": "{}"}, + } + ], + }, {"role": "tool", "content": tool_output, "tool_call_id": "call_1"}, ] @@ -269,6 +350,7 @@ def evaluate_answer(answer: str, expected_findings: list[str]) -> tuple[int, int # Main Eval Runner # ============================================================================ + def main(): # Check for API key if not os.environ.get("OPENAI_API_KEY"): @@ -299,7 +381,7 @@ def main(): for case in eval_cases: print(f"\n{'─' * 70}") print(f"EVAL: {case.name}") - print(f"Query: \"{case.user_query}\"") + print(f'Query: "{case.user_query}"') print(f"{'─' * 70}") # Measure original tokens @@ -312,26 +394,32 @@ def main(): user_query=case.user_query, ) - print(f"\n Tool Output:") + print("\n Tool Output:") print(f" Original: {original_tokens:,} tokens") print(f" Compressed: {compression.compressed_tokens:,} tokens") print(f" Saved: {compression.tokens_saved:,} ({compression.compression_ratio:.1%})") # Run agent BEFORE (with original output) - print(f"\n Running agent with ORIGINAL output...") + print("\n Running agent with ORIGINAL output...") try: answer_before, tokens_before = run_agent_with_tool_output( client, case.user_query, case.tool_name, case.tool_output ) - found_before, total, missing_before = evaluate_answer(answer_before, case.expected_findings) + found_before, total, missing_before = evaluate_answer( + answer_before, case.expected_findings + ) except Exception as e: print(f" ERROR: {e}") answer_before = "" - found_before, total, missing_before = 0, len(case.expected_findings), case.expected_findings + found_before, total, missing_before = ( + 0, + len(case.expected_findings), + case.expected_findings, + ) tokens_before = 0 # Run agent AFTER (with compressed output) - print(f" Running agent with COMPRESSED output...") + print(" Running agent with COMPRESSED output...") try: answer_after, tokens_after = run_agent_with_tool_output( client, case.user_query, case.tool_name, compression.compressed_content @@ -344,7 +432,7 @@ def main(): tokens_after = 0 # Results - print(f"\n Results:") + print("\n Results:") print(f" BEFORE: Found {found_before}/{total} expected findings") if missing_before: print(f" Missing: {missing_before}") @@ -353,30 +441,34 @@ def main(): print(f" Missing: {missing_after}") # Token usage comparison - print(f"\n API Token Usage:") + print("\n API Token Usage:") print(f" BEFORE: {tokens_before:,} tokens") print(f" AFTER: {tokens_after:,} tokens") if tokens_before > 0: - print(f" Saved: {tokens_before - tokens_after:,} ({(tokens_before - tokens_after) / tokens_before:.1%})") + print( + f" Saved: {tokens_before - tokens_after:,} ({(tokens_before - tokens_after) / tokens_before:.1%})" + ) # Pass/Fail passed = found_after >= found_before status = "PASS" if passed else "FAIL" print(f"\n Status: {status}") if not passed: - print(f" Reason: Compressed output lost information") + print(" Reason: Compressed output lost information") print(f" Lost findings: {set(missing_after) - set(missing_before)}") - results.append({ - "name": case.name, - "passed": passed, - "found_before": found_before, - "found_after": found_after, - "total": total, - "tokens_before": tokens_before, - "tokens_after": tokens_after, - "compression_ratio": compression.compression_ratio, - }) + results.append( + { + "name": case.name, + "passed": passed, + "found_before": found_before, + "found_after": found_after, + "total": total, + "tokens_before": tokens_before, + "tokens_after": tokens_after, + "compression_ratio": compression.compression_ratio, + } + ) # Summary print("\n" + "=" * 70) @@ -387,27 +479,31 @@ def main(): total_cases = len(results) print(f"\n Tests Passed: {passed}/{total_cases}") - print(f"\n Detailed Results:") + print("\n Detailed Results:") print(f" {'Test Name':<35} {'Before':<10} {'After':<10} {'Compress':<10} {'Status':<8}") - print(f" {'-'*35} {'-'*10} {'-'*10} {'-'*10} {'-'*8}") + print(f" {'-' * 35} {'-' * 10} {'-' * 10} {'-' * 10} {'-' * 8}") for r in results: status = "PASS" if r["passed"] else "FAIL" - print(f" {r['name']:<35} {r['found_before']}/{r['total']:<8} {r['found_after']}/{r['total']:<8} {r['compression_ratio']:.0%}{'':>6} {status:<8}") + print( + f" {r['name']:<35} {r['found_before']}/{r['total']:<8} {r['found_after']}/{r['total']:<8} {r['compression_ratio']:.0%}{'':>6} {status:<8}" + ) # Token savings total_tokens_before = sum(r["tokens_before"] for r in results) total_tokens_after = sum(r["tokens_after"] for r in results) - print(f"\n Total API Tokens:") + print("\n Total API Tokens:") print(f" Before: {total_tokens_before:,}") print(f" After: {total_tokens_after:,}") - print(f" Saved: {total_tokens_before - total_tokens_after:,} ({(total_tokens_before - total_tokens_after) / total_tokens_before:.1%})") + print( + f" Saved: {total_tokens_before - total_tokens_after:,} ({(total_tokens_before - total_tokens_after) / total_tokens_before:.1%})" + ) # Cost estimate cost_before = total_tokens_before * 0.15 / 1_000_000 # gpt-4o-mini input cost_after = total_tokens_after * 0.15 / 1_000_000 - print(f"\n Cost (gpt-4o-mini):") + print("\n Cost (gpt-4o-mini):") print(f" Before: ${cost_before:.4f}") print(f" After: ${cost_after:.4f}") print(f" Saved: ${cost_before - cost_after:.4f}") diff --git a/examples/mcp_demo/show_before_after.py b/examples/mcp_demo/show_before_after.py index c853ecb88..8989d4071 100644 --- a/examples/mcp_demo/show_before_after.py +++ b/examples/mcp_demo/show_before_after.py @@ -22,16 +22,16 @@ def main(): print("\nBEFORE (in your MCP host application):") print("-" * 40) - before_standalone = ''' + before_standalone = """ # Your MCP host application result = await mcp_client.call_tool("search_logs", {"service": "api"}) messages.append({"role": "tool", "content": result}) -''' +""" print(before_standalone) print("\nAFTER (with Headroom compression):") print("-" * 40) - after_standalone = ''' + after_standalone = """ from headroom.integrations.mcp import compress_tool_result # ADD THIS # Your MCP host application @@ -42,7 +42,7 @@ compressed = compress_tool_result( # ADD THIS user_query="find errors in api", # ADD THIS ) # ADD THIS messages.append({"role": "tool", "content": compressed}) -''' +""" print(after_standalone) # ========================================================================= @@ -54,7 +54,7 @@ messages.append({"role": "tool", "content": compressed}) print("\nBEFORE:") print("-" * 40) - before_wrapper = ''' + before_wrapper = """ from mcp import Client # Create MCP client @@ -62,12 +62,12 @@ client = Client(transport) # Use client normally result = await client.call_tool("search_logs", {"service": "api"}) -''' +""" print(before_wrapper) print("\nAFTER:") print("-" * 40) - after_wrapper = ''' + after_wrapper = """ from mcp import Client from headroom.integrations.mcp import HeadroomMCPClientWrapper # ADD THIS @@ -77,7 +77,7 @@ client = HeadroomMCPClientWrapper(base_client) # WRAP IT (1 line) # Use client normally - compression is automatic! result = await client.call_tool("search_logs", {"service": "api"}) -''' +""" print(after_wrapper) # ========================================================================= @@ -89,7 +89,7 @@ result = await client.call_tool("search_logs", {"service": "api"}) print("\nCode with metrics tracking:") print("-" * 40) - with_metrics = ''' + with_metrics = """ from headroom.integrations.mcp import compress_tool_result_with_metrics result = await mcp_client.call_tool("search_logs", {"service": "api"}) @@ -104,7 +104,7 @@ print(f"Compression: {compression.compression_ratio:.1%}") print(f"Errors preserved: {compression.errors_preserved}") messages.append({"role": "tool", "content": compression.compressed_content}) -''' +""" print(with_metrics) # ========================================================================= diff --git a/examples/mcp_demo/show_compression.py b/examples/mcp_demo/show_compression.py index 961f3b77a..e97d51f9c 100644 --- a/examples/mcp_demo/show_compression.py +++ b/examples/mcp_demo/show_compression.py @@ -4,20 +4,18 @@ Run with: PYTHONPATH=. python -m examples.mcp_demo.show_compression """ -import json import random from headroom.integrations.mcp import ( compress_tool_result_with_metrics, - HeadroomMCPCompressor, ) from headroom.providers import OpenAIProvider from .mock_mcp_servers import ( - generate_slack_search_results, generate_database_query_results, - generate_log_search_results, generate_github_issues_results, + generate_log_search_results, + generate_slack_search_results, ) @@ -30,7 +28,7 @@ def main(): # Get token counter provider = OpenAIProvider() - tokenizer = provider.get_token_counter("gpt-4o") + provider.get_token_counter("gpt-4o") # Test scenarios scenarios = [ @@ -71,7 +69,7 @@ def main(): print(f"\n{'─' * 70}") print(f"Tool: {scenario['name']}") print(f"MCP Server: {scenario['tool_name']}") - print(f"User Query: \"{scenario['user_query']}\"") + print(f'User Query: "{scenario["user_query"]}"') print(f"{'─' * 70}") result = compress_tool_result_with_metrics( diff --git a/examples/real_world_eval.py b/examples/real_world_eval.py index 9cecb7da6..5206e081a 100644 --- a/examples/real_world_eval.py +++ b/examples/real_world_eval.py @@ -37,10 +37,10 @@ provider = AnthropicProvider() # AGGRESSIVE optimization config aggressive_tool_crusher = ToolCrusherConfig( enabled=True, - min_tokens_to_crush=100, # Crush smaller outputs - max_array_items=3, # Only keep first 3 items (was 10) - max_string_length=200, # Truncate strings > 200 chars (was 1000) - max_depth=3, # Limit nesting to 3 levels (was 5) + min_tokens_to_crush=100, # Crush smaller outputs + max_array_items=3, # Only keep first 3 items (was 10) + max_string_length=200, # Truncate strings > 200 chars (was 1000) + max_depth=3, # Limit nesting to 3 levels (was 5) ) db_path = os.path.join(tempfile.gettempdir(), "headroom_eval.db") @@ -54,7 +54,6 @@ headroom_client = HeadroomClient( ) # Aggressive optimization client -from headroom.config import HeadroomConfig aggressive_config = HeadroomConfig() aggressive_config.tool_crusher = aggressive_tool_crusher @@ -67,52 +66,56 @@ aggressive_client = HeadroomClient( ) # Manually set aggressive config on pipeline aggressive_client._config = aggressive_config -aggressive_client._pipeline = __import__('headroom.transforms', fromlist=['TransformPipeline']).TransformPipeline( - aggressive_config, provider=provider -) +aggressive_client._pipeline = __import__( + "headroom.transforms", fromlist=["TransformPipeline"] +).TransformPipeline(aggressive_config, provider=provider) # ============================================================================= # REALISTIC AGENTIC SCENARIO: Research Assistant # ============================================================================= + def generate_search_results(query: str, count: int = 25) -> str: """Generate realistic search results JSON.""" results = [] for i in range(count): - results.append({ - "id": f"doc_{i:04d}", - "title": f"Research Paper: {query.title()} - Study {i+1}", - "url": f"https://research.example.com/papers/{query.replace(' ', '-')}/{i}", - "snippet": f"This comprehensive study examines {query} through multiple methodologies. " - f"Key findings include significant correlations between variables A and B, " - f"with p-values < 0.05. The sample size of {1000 + i*100} participants " - f"provides robust statistical power. Methods included: surveys, interviews, " - f"longitudinal tracking, and meta-analysis of {50 + i*10} prior studies.", - "citations": 150 + i * 23, - "year": 2020 + (i % 5), - "authors": [ - {"name": f"Dr. Smith{i}", "affiliation": "MIT"}, - {"name": f"Prof. Jones{i}", "affiliation": "Stanford"}, - {"name": f"Dr. Williams{i}", "affiliation": "Harvard"}, - ], - "keywords": ["machine learning", "data science", query, "research", "analysis"], - "abstract": f"Abstract for paper {i}: " + "Lorem ipsum dolor sit amet. " * 20, - "methodology": { - "type": "mixed-methods", - "sample_size": 1000 + i * 100, - "duration_months": 12 + i, - "instruments": ["survey", "interview", "observation"], - }, - }) + results.append( + { + "id": f"doc_{i:04d}", + "title": f"Research Paper: {query.title()} - Study {i + 1}", + "url": f"https://research.example.com/papers/{query.replace(' ', '-')}/{i}", + "snippet": f"This comprehensive study examines {query} through multiple methodologies. " + f"Key findings include significant correlations between variables A and B, " + f"with p-values < 0.05. The sample size of {1000 + i * 100} participants " + f"provides robust statistical power. Methods included: surveys, interviews, " + f"longitudinal tracking, and meta-analysis of {50 + i * 10} prior studies.", + "citations": 150 + i * 23, + "year": 2020 + (i % 5), + "authors": [ + {"name": f"Dr. Smith{i}", "affiliation": "MIT"}, + {"name": f"Prof. Jones{i}", "affiliation": "Stanford"}, + {"name": f"Dr. Williams{i}", "affiliation": "Harvard"}, + ], + "keywords": ["machine learning", "data science", query, "research", "analysis"], + "abstract": f"Abstract for paper {i}: " + "Lorem ipsum dolor sit amet. " * 20, + "methodology": { + "type": "mixed-methods", + "sample_size": 1000 + i * 100, + "duration_months": 12 + i, + "instruments": ["survey", "interview", "observation"], + }, + } + ) return json.dumps({"results": results, "total_count": count, "query": query}) def generate_document_content(doc_id: str) -> str: """Generate realistic document content.""" - return json.dumps({ - "id": doc_id, - "full_text": """ + return json.dumps( + { + "id": doc_id, + "full_text": """ Introduction: This research investigates the complex interplay between artificial intelligence and human decision-making processes. Our longitudinal study spanning 36 months @@ -136,68 +139,99 @@ def generate_document_content(doc_id: str) -> str: Conclusion: The integration of AI in decision-making processes offers substantial benefits but requires careful implementation to avoid potential negative outcomes. - """ * 3, # Make it longer - "metadata": { - "word_count": 15000, - "pages": 45, - "figures": 12, - "tables": 8, - "references": 150, - }, - "sections": [ - {"title": "Introduction", "page": 1, "word_count": 2000}, - {"title": "Literature Review", "page": 5, "word_count": 4000}, - {"title": "Methodology", "page": 15, "word_count": 3000}, - {"title": "Results", "page": 22, "word_count": 3500}, - {"title": "Discussion", "page": 32, "word_count": 2000}, - {"title": "Conclusion", "page": 40, "word_count": 500}, - ], - }) + """ + * 3, # Make it longer + "metadata": { + "word_count": 15000, + "pages": 45, + "figures": 12, + "tables": 8, + "references": 150, + }, + "sections": [ + {"title": "Introduction", "page": 1, "word_count": 2000}, + {"title": "Literature Review", "page": 5, "word_count": 4000}, + {"title": "Methodology", "page": 15, "word_count": 3000}, + {"title": "Results", "page": 22, "word_count": 3500}, + {"title": "Discussion", "page": 32, "word_count": 2000}, + {"title": "Conclusion", "page": 40, "word_count": 500}, + ], + } + ) def generate_analytics_data() -> str: """Generate realistic analytics/metrics data.""" - return json.dumps({ - "summary_statistics": { - "total_papers_analyzed": 500, - "date_range": {"start": "2020-01-01", "end": "2024-12-31"}, - "avg_citations": 45.7, - "median_citations": 32, - "std_dev": 28.3, - }, - "trend_analysis": [ - {"year": 2020, "papers": 80, "avg_citations": 52.3, "top_keywords": ["covid", "remote", "digital"]}, - {"year": 2021, "papers": 95, "avg_citations": 48.1, "top_keywords": ["hybrid", "adaptation", "resilience"]}, - {"year": 2022, "papers": 110, "avg_citations": 44.2, "top_keywords": ["AI", "automation", "efficiency"]}, - {"year": 2023, "papers": 120, "avg_citations": 38.5, "top_keywords": ["LLM", "generative", "ethics"]}, - {"year": 2024, "papers": 95, "avg_citations": 25.1, "top_keywords": ["agents", "multimodal", "safety"]}, - ], - "citation_distribution": { - "0-10": 150, - "11-25": 120, - "26-50": 100, - "51-100": 80, - "101-200": 35, - "200+": 15, - }, - "top_authors": [ - {"name": "Dr. Smith", "papers": 25, "total_citations": 1250, "h_index": 18}, - {"name": "Prof. Jones", "papers": 22, "total_citations": 980, "h_index": 15}, - {"name": "Dr. Williams", "papers": 20, "total_citations": 890, "h_index": 14}, - ] * 5, # More authors - "collaboration_network": { - "nodes": 150, - "edges": 450, - "avg_degree": 6.0, - "clustering_coefficient": 0.45, - }, - }) + return json.dumps( + { + "summary_statistics": { + "total_papers_analyzed": 500, + "date_range": {"start": "2020-01-01", "end": "2024-12-31"}, + "avg_citations": 45.7, + "median_citations": 32, + "std_dev": 28.3, + }, + "trend_analysis": [ + { + "year": 2020, + "papers": 80, + "avg_citations": 52.3, + "top_keywords": ["covid", "remote", "digital"], + }, + { + "year": 2021, + "papers": 95, + "avg_citations": 48.1, + "top_keywords": ["hybrid", "adaptation", "resilience"], + }, + { + "year": 2022, + "papers": 110, + "avg_citations": 44.2, + "top_keywords": ["AI", "automation", "efficiency"], + }, + { + "year": 2023, + "papers": 120, + "avg_citations": 38.5, + "top_keywords": ["LLM", "generative", "ethics"], + }, + { + "year": 2024, + "papers": 95, + "avg_citations": 25.1, + "top_keywords": ["agents", "multimodal", "safety"], + }, + ], + "citation_distribution": { + "0-10": 150, + "11-25": 120, + "26-50": 100, + "51-100": 80, + "101-200": 35, + "200+": 15, + }, + "top_authors": [ + {"name": "Dr. Smith", "papers": 25, "total_citations": 1250, "h_index": 18}, + {"name": "Prof. Jones", "papers": 22, "total_citations": 980, "h_index": 15}, + {"name": "Dr. Williams", "papers": 20, "total_citations": 890, "h_index": 14}, + ] + * 5, # More authors + "collaboration_network": { + "nodes": 150, + "edges": 450, + "avg_degree": 6.0, + "clustering_coefficient": 0.45, + }, + } + ) # ============================================================================= # BUILD COMPLEX AGENTIC CONVERSATION # ============================================================================= + def build_agentic_conversation() -> list[dict]: """Build a realistic multi-turn agentic conversation.""" @@ -210,26 +244,24 @@ def build_agentic_conversation() -> list[dict]: { "role": "user", "content": "Current Date: 2024-12-15. I need you to research the impact of AI on workplace productivity. " - "Search for recent papers, analyze the top results, and give me a summary." + "Search for recent papers, analyze the top results, and give me a summary.", }, - # Turn 2: Assistant decides to search { "role": "assistant", "content": [ { "type": "text", - "text": "I'll help you research AI's impact on workplace productivity. Let me search for recent academic papers on this topic." + "text": "I'll help you research AI's impact on workplace productivity. Let me search for recent academic papers on this topic.", }, { "type": "tool_use", "id": "search_1", "name": "academic_search", - "input": {"query": "AI impact workplace productivity", "limit": 25} - } - ] + "input": {"query": "AI impact workplace productivity", "limit": 25}, + }, + ], }, - # Turn 3: Tool result - large search results { "role": "user", @@ -237,40 +269,38 @@ def build_agentic_conversation() -> list[dict]: { "type": "tool_result", "tool_use_id": "search_1", - "content": generate_search_results("AI impact workplace productivity", 25) + "content": generate_search_results("AI impact workplace productivity", 25), } - ] + ], }, - # Turn 4: Assistant analyzes and fetches more { "role": "assistant", "content": [ { "type": "text", - "text": "I found 25 relevant papers. Let me fetch the full content of the top 3 most cited papers and get analytics on the overall trends." + "text": "I found 25 relevant papers. Let me fetch the full content of the top 3 most cited papers and get analytics on the overall trends.", }, { "type": "tool_use", "id": "fetch_1", "name": "fetch_document", - "input": {"doc_id": "doc_0001"} + "input": {"doc_id": "doc_0001"}, }, { "type": "tool_use", "id": "fetch_2", "name": "fetch_document", - "input": {"doc_id": "doc_0002"} + "input": {"doc_id": "doc_0002"}, }, { "type": "tool_use", "id": "analytics_1", "name": "get_analytics", - "input": {"topic": "AI workplace productivity"} - } - ] + "input": {"topic": "AI workplace productivity"}, + }, + ], }, - # Turn 5: Multiple tool results { "role": "user", @@ -278,56 +308,55 @@ def build_agentic_conversation() -> list[dict]: { "type": "tool_result", "tool_use_id": "fetch_1", - "content": generate_document_content("doc_0001") + "content": generate_document_content("doc_0001"), }, { "type": "tool_result", "tool_use_id": "fetch_2", - "content": generate_document_content("doc_0002") + "content": generate_document_content("doc_0002"), }, { "type": "tool_result", "tool_use_id": "analytics_1", - "content": generate_analytics_data() - } - ] + "content": generate_analytics_data(), + }, + ], }, - # Turn 6: Assistant provides initial summary { "role": "assistant", "content": "Based on my analysis of 25 papers and detailed review of the top cited works, here's what the research shows:\n\n" - "**Key Findings:**\n" - "1. AI tools improve productivity by 25-40% on average\n" - "2. Biggest gains in data analysis and routine tasks\n" - "3. Mixed results for creative work\n" - "4. Training and adoption are critical success factors\n\n" - "Would you like me to dive deeper into any specific aspect?" + "**Key Findings:**\n" + "1. AI tools improve productivity by 25-40% on average\n" + "2. Biggest gains in data analysis and routine tasks\n" + "3. Mixed results for creative work\n" + "4. Training and adoption are critical success factors\n\n" + "Would you like me to dive deeper into any specific aspect?", }, - # Turn 7: User asks follow-up { "role": "user", - "content": "Yes, tell me more about the training and adoption factors. What makes AI adoption successful?" + "content": "Yes, tell me more about the training and adoption factors. What makes AI adoption successful?", }, - # Turn 8: Assistant searches for more specific info { "role": "assistant", "content": [ { "type": "text", - "text": "Great question. Let me search for papers specifically about AI adoption success factors." + "text": "Great question. Let me search for papers specifically about AI adoption success factors.", }, { "type": "tool_use", "id": "search_2", "name": "academic_search", - "input": {"query": "AI adoption success factors workplace training", "limit": 20} - } - ] + "input": { + "query": "AI adoption success factors workplace training", + "limit": 20, + }, + }, + ], }, - # Turn 9: More search results { "role": "user", @@ -335,28 +364,28 @@ def build_agentic_conversation() -> list[dict]: { "type": "tool_result", "tool_use_id": "search_2", - "content": generate_search_results("AI adoption success factors workplace training", 20) + "content": generate_search_results( + "AI adoption success factors workplace training", 20 + ), } - ] + ], }, - # Turn 10: Assistant synthesizes { "role": "assistant", "content": "Based on the additional research on AI adoption success factors:\n\n" - "**Critical Success Factors:**\n\n" - "1. **Leadership Support** - Executive buy-in correlates with 3x higher adoption rates\n" - "2. **Gradual Rollout** - Phased implementation outperforms big-bang approaches\n" - "3. **Training Investment** - Organizations spending >40 hours on training see 50% better outcomes\n" - "4. **Change Management** - Addressing employee concerns reduces resistance by 60%\n" - "5. **Clear Use Cases** - Specific, measurable goals improve success rates\n\n" - "The research consistently shows that technical capabilities matter less than organizational readiness." + "**Critical Success Factors:**\n\n" + "1. **Leadership Support** - Executive buy-in correlates with 3x higher adoption rates\n" + "2. **Gradual Rollout** - Phased implementation outperforms big-bang approaches\n" + "3. **Training Investment** - Organizations spending >40 hours on training see 50% better outcomes\n" + "4. **Change Management** - Addressing employee concerns reduces resistance by 60%\n" + "5. **Clear Use Cases** - Specific, measurable goals improve success rates\n\n" + "The research consistently shows that technical capabilities matter less than organizational readiness.", }, - # Turn 11: User asks for final deliverable { "role": "user", - "content": "Perfect. Now synthesize everything into a brief executive summary I can share with my team." + "content": "Perfect. Now synthesize everything into a brief executive summary I can share with my team.", }, ] @@ -367,9 +396,11 @@ def build_agentic_conversation() -> list[dict]: # EVALUATION FRAMEWORK # ============================================================================= + @dataclass class EvalResult: """Results from a single evaluation run.""" + mode: str tokens_input: int tokens_output: int @@ -443,15 +474,15 @@ Provide scores in this exact JSON format: response = base_client.messages.create( model="claude-3-5-haiku-latest", max_tokens=500, - messages=[{"role": "user", "content": eval_prompt}] + messages=[{"role": "user", "content": eval_prompt}], ) try: # Extract JSON from response text = response.content[0].text # Find JSON in response - start = text.find('{') - end = text.rfind('}') + 1 + start = text.find("{") + end = text.rfind("}") + 1 if start >= 0 and end > start: return json.loads(text[start:end]) except (json.JSONDecodeError, IndexError): @@ -464,6 +495,7 @@ Provide scores in this exact JSON format: # MAIN EVALUATION # ============================================================================= + def run_aggressive_evaluation(messages: list[dict], mode: str) -> EvalResult: """Run evaluation with aggressive client.""" tokenizer = provider.get_token_counter("claude-3-5-haiku-latest") @@ -505,8 +537,8 @@ def main(): messages = build_agentic_conversation() print(f"Scenario: Research Assistant with {len(messages)} turns") - print(f"Tool calls: 4 (search x2, fetch x2, analytics x1)") - print(f"Tool outputs: Large JSON payloads (~50KB total)") + print("Tool calls: 4 (search x2, fetch x2, analytics x1)") + print("Tool outputs: Large JSON payloads (~50KB total)") print() # ========================================================================= @@ -530,8 +562,12 @@ def main(): print(f"\n{'Mode':<20} {'Before':>10} {'After':>10} {'Saved':>10} {'%':>8}") print("-" * 60) - print(f"{'Conservative':<20} {sim_default.tokens_before:>10,} {sim_default.tokens_after:>10,} {sim_default.tokens_saved:>10,} {sim_default.tokens_saved/sim_default.tokens_before*100:>7.1f}%") - print(f"{'Aggressive':<20} {sim_aggressive.tokens_before:>10,} {sim_aggressive.tokens_after:>10,} {sim_aggressive.tokens_saved:>10,} {sim_aggressive.tokens_saved/sim_aggressive.tokens_before*100:>7.1f}%") + print( + f"{'Conservative':<20} {sim_default.tokens_before:>10,} {sim_default.tokens_after:>10,} {sim_default.tokens_saved:>10,} {sim_default.tokens_saved / sim_default.tokens_before * 100:>7.1f}%" + ) + print( + f"{'Aggressive':<20} {sim_aggressive.tokens_before:>10,} {sim_aggressive.tokens_after:>10,} {sim_aggressive.tokens_saved:>10,} {sim_aggressive.tokens_saved / sim_aggressive.tokens_before * 100:>7.1f}%" + ) print() print(f"Conservative transforms: {sim_default.transforms}") print(f"Aggressive transforms: {sim_aggressive.transforms}") @@ -545,7 +581,9 @@ def main(): print("1. BASELINE (No Optimization)") print("-" * 70) baseline = run_evaluation(messages, "audit") - print(f"Input: {baseline.tokens_input:,} tokens | Cost: ${baseline.cost_estimate:.4f} | Latency: {baseline.latency_ms:.0f}ms") + print( + f"Input: {baseline.tokens_input:,} tokens | Cost: ${baseline.cost_estimate:.4f} | Latency: {baseline.latency_ms:.0f}ms" + ) print(f"Response: {baseline.response[:300]}...") print() @@ -553,7 +591,9 @@ def main(): print("2. CONSERVATIVE OPTIMIZATION (Default Settings)") print("-" * 70) conservative = run_evaluation(messages, "optimize") - print(f"Input: {conservative.tokens_input:,} tokens | Cost: ${conservative.cost_estimate:.4f} | Latency: {conservative.latency_ms:.0f}ms") + print( + f"Input: {conservative.tokens_input:,} tokens | Cost: ${conservative.cost_estimate:.4f} | Latency: {conservative.latency_ms:.0f}ms" + ) print(f"Response: {conservative.response[:300]}...") print() @@ -561,7 +601,9 @@ def main(): print("3. AGGRESSIVE OPTIMIZATION (max_array=3, max_string=200, max_depth=3)") print("-" * 70) aggressive = run_aggressive_evaluation(messages, "optimize") - print(f"Input: {aggressive.tokens_input:,} tokens | Cost: ${aggressive.cost_estimate:.4f} | Latency: {aggressive.latency_ms:.0f}ms") + print( + f"Input: {aggressive.tokens_input:,} tokens | Cost: ${aggressive.cost_estimate:.4f} | Latency: {aggressive.latency_ms:.0f}ms" + ) print(f"Response: {aggressive.response[:300]}...") print() @@ -574,10 +616,18 @@ def main(): print(f"\n{'Metric':<25} {'Baseline':>12} {'Conservative':>12} {'Aggressive':>12}") print("-" * 65) - print(f"{'Input Tokens':<25} {baseline.tokens_input:>12,} {conservative.tokens_input:>12,} {aggressive.tokens_input:>12,}") - print(f"{'Output Tokens':<25} {baseline.tokens_output:>12,} {conservative.tokens_output:>12,} {aggressive.tokens_output:>12,}") - print(f"{'Cost':<25} ${baseline.cost_estimate:>11.4f} ${conservative.cost_estimate:>11.4f} ${aggressive.cost_estimate:>11.4f}") - print(f"{'Latency (ms)':<25} {baseline.latency_ms:>12.0f} {conservative.latency_ms:>12.0f} {aggressive.latency_ms:>12.0f}") + print( + f"{'Input Tokens':<25} {baseline.tokens_input:>12,} {conservative.tokens_input:>12,} {aggressive.tokens_input:>12,}" + ) + print( + f"{'Output Tokens':<25} {baseline.tokens_output:>12,} {conservative.tokens_output:>12,} {aggressive.tokens_output:>12,}" + ) + print( + f"{'Cost':<25} ${baseline.cost_estimate:>11.4f} ${conservative.cost_estimate:>11.4f} ${aggressive.cost_estimate:>11.4f}" + ) + print( + f"{'Latency (ms)':<25} {baseline.latency_ms:>12.0f} {conservative.latency_ms:>12.0f} {aggressive.latency_ms:>12.0f}" + ) # Savings vs baseline cons_savings = baseline.tokens_input - conservative.tokens_input @@ -586,12 +636,16 @@ def main(): aggr_pct = (aggr_savings / baseline.tokens_input) * 100 if baseline.tokens_input > 0 else 0 print() - print(f"{'Token Savings vs Baseline':<25} {'-':>12} {cons_savings:>10,} ({cons_pct:.0f}%) {aggr_savings:>10,} ({aggr_pct:.0f}%)") + print( + f"{'Token Savings vs Baseline':<25} {'-':>12} {cons_savings:>10,} ({cons_pct:.0f}%) {aggr_savings:>10,} ({aggr_pct:.0f}%)" + ) cons_cost_save = baseline.cost_estimate - conservative.cost_estimate aggr_cost_save = baseline.cost_estimate - aggressive.cost_estimate - print(f"{'Cost Savings vs Baseline':<25} {'-':>12} ${cons_cost_save:>10.4f} ${aggr_cost_save:>10.4f}") + print( + f"{'Cost Savings vs Baseline':<25} {'-':>12} ${cons_cost_save:>10.4f} ${aggr_cost_save:>10.4f}" + ) print() # ========================================================================= @@ -610,9 +664,9 @@ def main(): print(f"\n{'Criterion':<20} {'Baseline':>10} {'Conservative':>12} {'Aggressive':>12}") print("-" * 55) for criterion in ["completeness", "accuracy", "clarity", "actionability"]: - b_score = qual_cons['baseline'].get(criterion, 'N/A') - c_score = qual_cons['optimized'].get(criterion, 'N/A') - a_score = qual_aggr['optimized'].get(criterion, 'N/A') + b_score = qual_cons["baseline"].get(criterion, "N/A") + c_score = qual_cons["optimized"].get(criterion, "N/A") + a_score = qual_aggr["optimized"].get(criterion, "N/A") print(f"{criterion.title():<20} {b_score:>10} {c_score:>12} {a_score:>12}") print() diff --git a/examples/real_world_openai_eval.py b/examples/real_world_openai_eval.py index d6314a582..295dcd1dd 100644 --- a/examples/real_world_openai_eval.py +++ b/examples/real_world_openai_eval.py @@ -27,6 +27,7 @@ from openai import OpenAI from headroom import HeadroomClient, OpenAIProvider, ToolCrusherConfig from headroom.config import HeadroomConfig +from headroom.transforms import TransformPipeline load_dotenv(".env.local") @@ -59,7 +60,6 @@ aggressive_client = HeadroomClient( default_mode="audit", ) aggressive_client._config = aggressive_config -from headroom.transforms import TransformPipeline aggressive_client._pipeline = TransformPipeline(aggressive_config, provider=provider) @@ -67,6 +67,7 @@ aggressive_client._pipeline = TransformPipeline(aggressive_config, provider=prov # REALISTIC TOOL OUTPUTS - Based on actual production systems # ============================================================================= + def generate_metrics_response() -> str: """ Realistic Prometheus/Datadog metrics query response. @@ -80,58 +81,71 @@ def generate_metrics_response() -> str: ts = base_time + timedelta(minutes=i) # Simulate spike around minute 45 value = 45 + (i * 0.5) if i < 45 else 85 + (i - 45) * 2 - cpu_data.append({ - "timestamp": ts.isoformat(), - "value": min(value, 98), - "labels": {"instance": "prod-api-1", "job": "api-server"} - }) + cpu_data.append( + { + "timestamp": ts.isoformat(), + "value": min(value, 98), + "labels": {"instance": "prod-api-1", "job": "api-server"}, + } + ) # Memory metrics memory_data = [] for i in range(60): ts = base_time + timedelta(minutes=i) value = 62 + (i * 0.3) - memory_data.append({ - "timestamp": ts.isoformat(), - "value": min(value, 89), - "labels": {"instance": "prod-api-1", "job": "api-server"} - }) + memory_data.append( + { + "timestamp": ts.isoformat(), + "value": min(value, 89), + "labels": {"instance": "prod-api-1", "job": "api-server"}, + } + ) # Request latency (p99) latency_data = [] for i in range(60): ts = base_time + timedelta(minutes=i) value = 120 if i < 45 else 450 + (i - 45) * 50 - latency_data.append({ - "timestamp": ts.isoformat(), - "value": min(value, 2500), - "labels": {"instance": "prod-api-1", "endpoint": "/api/v1/users"} - }) + latency_data.append( + { + "timestamp": ts.isoformat(), + "value": min(value, 2500), + "labels": {"instance": "prod-api-1", "endpoint": "/api/v1/users"}, + } + ) # Error rate error_data = [] for i in range(60): ts = base_time + timedelta(minutes=i) value = 0.1 if i < 45 else 2.5 + (i - 45) * 0.5 - error_data.append({ - "timestamp": ts.isoformat(), - "value": min(value, 15), - "labels": {"instance": "prod-api-1", "status_code": "5xx"} - }) + error_data.append( + { + "timestamp": ts.isoformat(), + "value": min(value, 15), + "labels": {"instance": "prod-api-1", "status_code": "5xx"}, + } + ) - return json.dumps({ - "status": "success", - "data": { - "resultType": "matrix", - "result": [ - {"metric": {"__name__": "cpu_usage_percent"}, "values": cpu_data}, - {"metric": {"__name__": "memory_usage_percent"}, "values": memory_data}, - {"metric": {"__name__": "http_request_duration_p99_ms"}, "values": latency_data}, - {"metric": {"__name__": "http_errors_rate_percent"}, "values": error_data}, - ] - }, - "query_time_ms": 127 - }) + return json.dumps( + { + "status": "success", + "data": { + "resultType": "matrix", + "result": [ + {"metric": {"__name__": "cpu_usage_percent"}, "values": cpu_data}, + {"metric": {"__name__": "memory_usage_percent"}, "values": memory_data}, + { + "metric": {"__name__": "http_request_duration_p99_ms"}, + "values": latency_data, + }, + {"metric": {"__name__": "http_errors_rate_percent"}, "values": error_data}, + ], + }, + "query_time_ms": 127, + } + ) def generate_logs_response() -> str: @@ -144,7 +158,11 @@ def generate_logs_response() -> str: logs = [] log_templates = [ ("ERROR", "Connection pool exhausted, waiting for available connection", "api-server"), - ("WARN", "Slow query detected: SELECT * FROM users WHERE status = 'active' took 2.3s", "api-server"), + ( + "WARN", + "Slow query detected: SELECT * FROM users WHERE status = 'active' took 2.3s", + "api-server", + ), ("ERROR", "Database connection timeout after 30000ms", "api-server"), ("INFO", "Retry attempt 1/3 for database connection", "api-server"), ("ERROR", "Max retries exceeded for database operation", "api-server"), @@ -164,103 +182,161 @@ def generate_logs_response() -> str: ts = base_time + timedelta(seconds=i * 45) level, msg, source = log_templates[i % len(log_templates)] - logs.append({ - "@timestamp": ts.isoformat(), - "level": level, - "message": msg, - "service": source, - "trace_id": f"trace-{1000+i:04d}-abcd-{i:04d}", - "span_id": f"span-{i:04d}", - "host": f"prod-{source}-{i % 3 + 1}", - "environment": "production", - "version": "2.4.1", - "kubernetes": { - "namespace": "production", - "pod": f"{source}-{i % 5 + 1}-abc123", - "container": source, - "node": f"node-{i % 3 + 1}.prod.internal" - }, - "request": { - "method": "GET" if i % 2 == 0 else "POST", - "path": "/api/v1/users" if i % 3 == 0 else "/api/v1/orders", - "status_code": 500 if level == "ERROR" else 200, - "duration_ms": 150 + (i * 100) if level != "ERROR" else 30000 + logs.append( + { + "@timestamp": ts.isoformat(), + "level": level, + "message": msg, + "service": source, + "trace_id": f"trace-{1000 + i:04d}-abcd-{i:04d}", + "span_id": f"span-{i:04d}", + "host": f"prod-{source}-{i % 3 + 1}", + "environment": "production", + "version": "2.4.1", + "kubernetes": { + "namespace": "production", + "pod": f"{source}-{i % 5 + 1}-abc123", + "container": source, + "node": f"node-{i % 3 + 1}.prod.internal", + }, + "request": { + "method": "GET" if i % 2 == 0 else "POST", + "path": "/api/v1/users" if i % 3 == 0 else "/api/v1/orders", + "status_code": 500 if level == "ERROR" else 200, + "duration_ms": 150 + (i * 100) if level != "ERROR" else 30000, + }, } - }) + ) - return json.dumps({ - "took": 234, - "timed_out": False, - "hits": { - "total": {"value": len(logs), "relation": "eq"}, - "max_score": 1.0, - "hits": logs + return json.dumps( + { + "took": 234, + "timed_out": False, + "hits": { + "total": {"value": len(logs), "relation": "eq"}, + "max_score": 1.0, + "hits": logs, + }, } - }) + ) def generate_service_status() -> str: """ Realistic health check / service status response. """ - return json.dumps({ - "services": [ - { - "name": "api-server", - "status": "degraded", - "instances": [ - {"id": "api-1", "status": "unhealthy", "cpu": 94, "memory": 87, "connections": 500}, - {"id": "api-2", "status": "healthy", "cpu": 45, "memory": 62, "connections": 150}, - {"id": "api-3", "status": "unhealthy", "cpu": 91, "memory": 85, "connections": 480}, - ], - "last_check": datetime.now().isoformat(), - "error_rate": 12.5, - "p99_latency_ms": 2100 - }, - { - "name": "database-primary", - "status": "critical", - "instances": [ - {"id": "db-primary", "status": "unhealthy", "connections": 500, "max_connections": 500, - "replication_lag_ms": 0, "disk_usage_percent": 78} - ], - "last_check": datetime.now().isoformat(), - "active_queries": 487, - "blocked_queries": 52 - }, - { - "name": "database-replica", - "status": "healthy", - "instances": [ - {"id": "db-replica-1", "status": "healthy", "connections": 120, "max_connections": 500, - "replication_lag_ms": 150, "disk_usage_percent": 76}, - {"id": "db-replica-2", "status": "healthy", "connections": 115, "max_connections": 500, - "replication_lag_ms": 180, "disk_usage_percent": 77} - ], - "last_check": datetime.now().isoformat() - }, - { - "name": "redis-cache", - "status": "healthy", - "instances": [ - {"id": "redis-1", "status": "healthy", "memory_used_mb": 2048, "memory_max_mb": 4096, - "connected_clients": 45, "hit_rate": 0.94} - ], - "last_check": datetime.now().isoformat() - }, - { - "name": "nginx-ingress", - "status": "healthy", - "instances": [ - {"id": "nginx-1", "status": "healthy", "active_connections": 1250, "requests_per_sec": 450}, - {"id": "nginx-2", "status": "healthy", "active_connections": 1180, "requests_per_sec": 420} - ], - "last_check": datetime.now().isoformat() - } - ], - "overall_status": "critical", - "timestamp": datetime.now().isoformat() - }) + return json.dumps( + { + "services": [ + { + "name": "api-server", + "status": "degraded", + "instances": [ + { + "id": "api-1", + "status": "unhealthy", + "cpu": 94, + "memory": 87, + "connections": 500, + }, + { + "id": "api-2", + "status": "healthy", + "cpu": 45, + "memory": 62, + "connections": 150, + }, + { + "id": "api-3", + "status": "unhealthy", + "cpu": 91, + "memory": 85, + "connections": 480, + }, + ], + "last_check": datetime.now().isoformat(), + "error_rate": 12.5, + "p99_latency_ms": 2100, + }, + { + "name": "database-primary", + "status": "critical", + "instances": [ + { + "id": "db-primary", + "status": "unhealthy", + "connections": 500, + "max_connections": 500, + "replication_lag_ms": 0, + "disk_usage_percent": 78, + } + ], + "last_check": datetime.now().isoformat(), + "active_queries": 487, + "blocked_queries": 52, + }, + { + "name": "database-replica", + "status": "healthy", + "instances": [ + { + "id": "db-replica-1", + "status": "healthy", + "connections": 120, + "max_connections": 500, + "replication_lag_ms": 150, + "disk_usage_percent": 76, + }, + { + "id": "db-replica-2", + "status": "healthy", + "connections": 115, + "max_connections": 500, + "replication_lag_ms": 180, + "disk_usage_percent": 77, + }, + ], + "last_check": datetime.now().isoformat(), + }, + { + "name": "redis-cache", + "status": "healthy", + "instances": [ + { + "id": "redis-1", + "status": "healthy", + "memory_used_mb": 2048, + "memory_max_mb": 4096, + "connected_clients": 45, + "hit_rate": 0.94, + } + ], + "last_check": datetime.now().isoformat(), + }, + { + "name": "nginx-ingress", + "status": "healthy", + "instances": [ + { + "id": "nginx-1", + "status": "healthy", + "active_connections": 1250, + "requests_per_sec": 450, + }, + { + "id": "nginx-2", + "status": "healthy", + "active_connections": 1180, + "requests_per_sec": 420, + }, + ], + "last_check": datetime.now().isoformat(), + }, + ], + "overall_status": "critical", + "timestamp": datetime.now().isoformat(), + } + ) def generate_deployments_response() -> str: @@ -272,84 +348,86 @@ def generate_deployments_response() -> str: deployments = [] for i in range(15): ts = base_time - timedelta(hours=i * 4) - deployments.append({ - "id": f"deploy-{1000-i}", - "service": "api-server" if i % 3 != 2 else "database-migration", - "version": f"2.4.{15-i}", - "status": "success" if i != 1 else "success", # Recent deploy - "timestamp": ts.isoformat(), - "commit": f"abc{i:04d}def", - "author": f"dev{i % 5 + 1}@company.com", - "message": [ - "feat: Add new user endpoint", - "fix: Connection pool sizing", - "chore: Update dependencies", - "feat: Implement caching layer", - "fix: Memory leak in request handler" - ][i % 5], - "changes": { - "files_changed": 5 + i, - "insertions": 100 + i * 20, - "deletions": 30 + i * 5 - }, - "rollback_available": True, - "canary_status": "completed" if i > 0 else "in_progress" - }) + deployments.append( + { + "id": f"deploy-{1000 - i}", + "service": "api-server" if i % 3 != 2 else "database-migration", + "version": f"2.4.{15 - i}", + "status": "success" if i != 1 else "success", # Recent deploy + "timestamp": ts.isoformat(), + "commit": f"abc{i:04d}def", + "author": f"dev{i % 5 + 1}@company.com", + "message": [ + "feat: Add new user endpoint", + "fix: Connection pool sizing", + "chore: Update dependencies", + "feat: Implement caching layer", + "fix: Memory leak in request handler", + ][i % 5], + "changes": { + "files_changed": 5 + i, + "insertions": 100 + i * 20, + "deletions": 30 + i * 5, + }, + "rollback_available": True, + "canary_status": "completed" if i > 0 else "in_progress", + } + ) - return json.dumps({ - "deployments": deployments, - "total_count": len(deployments), - "page": 1, - "per_page": 20 - }) + return json.dumps( + {"deployments": deployments, "total_count": len(deployments), "page": 1, "per_page": 20} + ) def generate_runbook_response() -> str: """ Realistic runbook/documentation lookup. """ - return json.dumps({ - "runbook": { - "title": "Database Connection Pool Exhaustion", - "id": "RUN-DB-001", - "severity": "P1", - "last_updated": "2024-11-15", - "owner": "platform-team", - "symptoms": [ - "High error rate on API endpoints", - "Connection timeout errors in logs", - "Database showing max connections reached", - "Increased latency across all services" - ], - "diagnosis_steps": [ - "1. Check current connection count: SELECT count(*) FROM pg_stat_activity", - "2. Identify connection holders: SELECT * FROM pg_stat_activity WHERE state != 'idle'", - "3. Check for long-running queries: SELECT * FROM pg_stat_activity WHERE state = 'active' AND query_start < now() - interval '1 minute'", - "4. Verify connection pool settings in application config", - "5. Check for connection leaks in recent deployments" - ], - "remediation_steps": [ - "1. IMMEDIATE: Kill idle connections older than 10 minutes", - "2. IMMEDIATE: Scale up API server replicas to distribute load", - "3. SHORT-TERM: Increase max_connections on database (requires restart)", - "4. SHORT-TERM: Review and optimize connection pool settings", - "5. LONG-TERM: Implement connection pooler (PgBouncer)" - ], - "commands": { - "kill_idle_connections": "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE state = 'idle' AND query_start < now() - interval '10 minutes'", - "check_pool_settings": "kubectl get configmap api-server-config -o yaml | grep -A5 'database'", - "scale_replicas": "kubectl scale deployment api-server --replicas=5" - }, - "related_incidents": ["INC-2024-089", "INC-2024-056", "INC-2024-023"], - "escalation_path": ["on-call-sre", "database-team", "platform-lead"] + return json.dumps( + { + "runbook": { + "title": "Database Connection Pool Exhaustion", + "id": "RUN-DB-001", + "severity": "P1", + "last_updated": "2024-11-15", + "owner": "platform-team", + "symptoms": [ + "High error rate on API endpoints", + "Connection timeout errors in logs", + "Database showing max connections reached", + "Increased latency across all services", + ], + "diagnosis_steps": [ + "1. Check current connection count: SELECT count(*) FROM pg_stat_activity", + "2. Identify connection holders: SELECT * FROM pg_stat_activity WHERE state != 'idle'", + "3. Check for long-running queries: SELECT * FROM pg_stat_activity WHERE state = 'active' AND query_start < now() - interval '1 minute'", + "4. Verify connection pool settings in application config", + "5. Check for connection leaks in recent deployments", + ], + "remediation_steps": [ + "1. IMMEDIATE: Kill idle connections older than 10 minutes", + "2. IMMEDIATE: Scale up API server replicas to distribute load", + "3. SHORT-TERM: Increase max_connections on database (requires restart)", + "4. SHORT-TERM: Review and optimize connection pool settings", + "5. LONG-TERM: Implement connection pooler (PgBouncer)", + ], + "commands": { + "kill_idle_connections": "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE state = 'idle' AND query_start < now() - interval '10 minutes'", + "check_pool_settings": "kubectl get configmap api-server-config -o yaml | grep -A5 'database'", + "scale_replicas": "kubectl scale deployment api-server --replicas=5", + }, + "related_incidents": ["INC-2024-089", "INC-2024-056", "INC-2024-023"], + "escalation_path": ["on-call-sre", "database-team", "platform-lead"], + } } - }) + ) # ============================================================================= # BUILD REALISTIC INCIDENT RESPONSE CONVERSATION # ============================================================================= + def build_incident_conversation() -> list[dict]: """ Build a realistic incident response agentic conversation. @@ -373,15 +451,13 @@ You have access to the following tools: - query_deployments: Get recent deployment history - get_runbook: Lookup runbook documentation -Always be concise and focus on actionable insights.""" +Always be concise and focus on actionable insights.""", }, - # User reports incident { "role": "user", - "content": "We're seeing elevated error rates on the API. Users reporting timeouts. Can you investigate?" + "content": "We're seeing elevated error rates on the API. Users reporting timeouts. Can you investigate?", }, - # Agent starts investigation - queries metrics { "role": "assistant", @@ -392,22 +468,18 @@ Always be concise and focus on actionable insights.""" "type": "function", "function": { "name": "query_metrics", - "arguments": json.dumps({ - "query": "api_error_rate, cpu_usage, memory_usage, latency_p99", - "time_range": "1h" - }) - } + "arguments": json.dumps( + { + "query": "api_error_rate, cpu_usage, memory_usage, latency_p99", + "time_range": "1h", + } + ), + }, } - ] + ], }, - # Metrics response - { - "role": "tool", - "tool_call_id": "call_metrics_1", - "content": generate_metrics_response() - }, - + {"role": "tool", "tool_call_id": "call_metrics_1", "content": generate_metrics_response()}, # Agent analyzes and queries logs { "role": "assistant", @@ -418,23 +490,19 @@ Always be concise and focus on actionable insights.""" "type": "function", "function": { "name": "search_logs", - "arguments": json.dumps({ - "query": "level:ERROR OR level:WARN", - "time_range": "30m", - "services": ["api-server", "database"] - }) - } + "arguments": json.dumps( + { + "query": "level:ERROR OR level:WARN", + "time_range": "30m", + "services": ["api-server", "database"], + } + ), + }, } - ] + ], }, - # Logs response - { - "role": "tool", - "tool_call_id": "call_logs_1", - "content": generate_logs_response() - }, - + {"role": "tool", "tool_call_id": "call_logs_1", "content": generate_logs_response()}, # Agent identifies root cause, checks service status { "role": "assistant", @@ -443,21 +511,12 @@ Always be concise and focus on actionable insights.""" { "id": "call_status_1", "type": "function", - "function": { - "name": "get_service_status", - "arguments": json.dumps({}) - } + "function": {"name": "get_service_status", "arguments": json.dumps({})}, } - ] + ], }, - # Service status response - { - "role": "tool", - "tool_call_id": "call_status_1", - "content": generate_service_status() - }, - + {"role": "tool", "tool_call_id": "call_status_1", "content": generate_service_status()}, # Agent checks for recent deployments { "role": "assistant", @@ -468,19 +527,17 @@ Always be concise and focus on actionable insights.""" "type": "function", "function": { "name": "query_deployments", - "arguments": json.dumps({"service": "api-server", "limit": 15}) - } + "arguments": json.dumps({"service": "api-server", "limit": 15}), + }, } - ] + ], }, - # Deployments response { "role": "tool", "tool_call_id": "call_deploys_1", - "content": generate_deployments_response() + "content": generate_deployments_response(), }, - # Agent gets runbook { "role": "assistant", @@ -491,19 +548,13 @@ Always be concise and focus on actionable insights.""" "type": "function", "function": { "name": "get_runbook", - "arguments": json.dumps({"search": "database connection pool exhaustion"}) - } + "arguments": json.dumps({"search": "database connection pool exhaustion"}), + }, } - ] + ], }, - # Runbook response - { - "role": "tool", - "tool_call_id": "call_runbook_1", - "content": generate_runbook_response() - }, - + {"role": "tool", "tool_call_id": "call_runbook_1", "content": generate_runbook_response()}, # Agent provides final analysis { "role": "assistant", @@ -521,13 +572,12 @@ Always be concise and focus on actionable insights.""" 2. Scale API servers to 5 replicas to distribute load 3. Consider rolling back deploy-999 if connection pool change is confirmed -Shall I proceed with the remediation steps?""" +Shall I proceed with the remediation steps?""", }, - # User asks for action { "role": "user", - "content": "Yes, give me the exact commands to run and summarize the incident for the post-mortem." + "content": "Yes, give me the exact commands to run and summarize the incident for the post-mortem.", }, ] @@ -538,6 +588,7 @@ Shall I proceed with the remediation steps?""" # EVALUATION # ============================================================================= + @dataclass class EvalResult: mode: str @@ -548,7 +599,9 @@ class EvalResult: cost_estimate: float -def evaluate_response_quality(baseline_response: str, optimized_response: str, optimization_level: str) -> dict: +def evaluate_response_quality( + baseline_response: str, optimized_response: str, optimization_level: str +) -> dict: """ Use GPT-4o as judge to evaluate if the optimized response maintains quality. """ @@ -595,10 +648,14 @@ PASS means overall_score >= 4.0, FAIL means < 4.0.""" ) import json as json_module + try: return json_module.loads(response.choices[0].message.content) - except: - return {"error": "Failed to parse judge response", "raw": response.choices[0].message.content} + except Exception: + return { + "error": "Failed to parse judge response", + "raw": response.choices[0].message.content, + } def run_eval(messages: list[dict], mode: str, use_aggressive: bool = False) -> EvalResult: @@ -652,12 +709,18 @@ def main(): print("-" * 70) sim_default = client.chat.completions.simulate(model="gpt-4o-mini", messages=messages) - sim_aggressive = aggressive_client.chat.completions.simulate(model="gpt-4o-mini", messages=messages) + sim_aggressive = aggressive_client.chat.completions.simulate( + model="gpt-4o-mini", messages=messages + ) print(f"\n{'Mode':<15} {'Before':>10} {'After':>10} {'Saved':>10} {'%':>8}") print("-" * 55) - print(f"{'Default':<15} {sim_default.tokens_before:>10,} {sim_default.tokens_after:>10,} {sim_default.tokens_saved:>10,} {sim_default.tokens_saved/sim_default.tokens_before*100:>7.1f}%") - print(f"{'Aggressive':<15} {sim_aggressive.tokens_before:>10,} {sim_aggressive.tokens_after:>10,} {sim_aggressive.tokens_saved:>10,} {sim_aggressive.tokens_saved/sim_aggressive.tokens_before*100:>7.1f}%") + print( + f"{'Default':<15} {sim_default.tokens_before:>10,} {sim_default.tokens_after:>10,} {sim_default.tokens_saved:>10,} {sim_default.tokens_saved / sim_default.tokens_before * 100:>7.1f}%" + ) + print( + f"{'Aggressive':<15} {sim_aggressive.tokens_before:>10,} {sim_aggressive.tokens_after:>10,} {sim_aggressive.tokens_saved:>10,} {sim_aggressive.tokens_saved / sim_aggressive.tokens_before * 100:>7.1f}%" + ) print(f"\nTransforms: {sim_default.transforms}") print() @@ -679,7 +742,9 @@ def main(): print("\n3. AGGRESSIVE OPTIMIZATION...") aggressive_opt = run_eval(messages, "optimize", use_aggressive=True) print(f" Tokens: {aggressive_opt.tokens_input:,} in / {aggressive_opt.tokens_output:,} out") - print(f" Cost: ${aggressive_opt.cost_estimate:.6f} | Latency: {aggressive_opt.latency_ms:.0f}ms") + print( + f" Cost: ${aggressive_opt.cost_estimate:.6f} | Latency: {aggressive_opt.latency_ms:.0f}ms" + ) # Results table print() @@ -694,12 +759,22 @@ def main(): print(f"\n{'Metric':<20} {'Baseline':>12} {'Default Opt':>12} {'Aggressive':>12}") print("-" * 60) - print(f"{'Input Tokens':<20} {baseline.tokens_input:>12,} {default_opt.tokens_input:>12,} {aggressive_opt.tokens_input:>12,}") - print(f"{'Output Tokens':<20} {baseline.tokens_output:>12,} {default_opt.tokens_output:>12,} {aggressive_opt.tokens_output:>12,}") - print(f"{'Cost':<20} ${baseline.cost_estimate:>11.6f} ${default_opt.cost_estimate:>11.6f} ${aggressive_opt.cost_estimate:>11.6f}") - print(f"{'Latency (ms)':<20} {baseline.latency_ms:>12.0f} {default_opt.latency_ms:>12.0f} {aggressive_opt.latency_ms:>12.0f}") + print( + f"{'Input Tokens':<20} {baseline.tokens_input:>12,} {default_opt.tokens_input:>12,} {aggressive_opt.tokens_input:>12,}" + ) + print( + f"{'Output Tokens':<20} {baseline.tokens_output:>12,} {default_opt.tokens_output:>12,} {aggressive_opt.tokens_output:>12,}" + ) + print( + f"{'Cost':<20} ${baseline.cost_estimate:>11.6f} ${default_opt.cost_estimate:>11.6f} ${aggressive_opt.cost_estimate:>11.6f}" + ) + print( + f"{'Latency (ms)':<20} {baseline.latency_ms:>12.0f} {default_opt.latency_ms:>12.0f} {aggressive_opt.latency_ms:>12.0f}" + ) print() - print(f"{'Token Savings':<20} {'-':>12} {def_savings:>10,} ({def_pct:.0f}%) {agg_savings:>10,} ({agg_pct:.0f}%)") + print( + f"{'Token Savings':<20} {'-':>12} {def_savings:>10,} ({def_pct:.0f}%) {agg_savings:>10,} ({agg_pct:.0f}%)" + ) # Show responses print() @@ -725,7 +800,9 @@ def main(): default_eval = evaluate_response_quality(baseline.response, default_opt.response, "default") print("\nEvaluating AGGRESSIVE optimization vs Baseline...") - aggressive_eval = evaluate_response_quality(baseline.response, aggressive_opt.response, "aggressive") + aggressive_eval = evaluate_response_quality( + baseline.response, aggressive_opt.response, "aggressive" + ) print(f"\n{'Criterion':<20} {'Default':>12} {'Aggressive':>12}") print("-" * 46) @@ -796,8 +873,8 @@ Cost Impact @ 1K requests/day: - Monthly savings: ${cost_save_monthly:.2f} CONCLUSION: - {'✓ Headroom achieves ' + f'{agg_pct:.0f}% token reduction with PASSING quality scores.' if a_verdict == 'PASS' else '⚠ Aggressive optimization may degrade response quality - use conservative settings.'} - {' The compressed context maintains semantic equivalence for model reasoning.' if a_verdict == 'PASS' else ''} + {"✓ Headroom achieves " + f"{agg_pct:.0f}% token reduction with PASSING quality scores." if a_verdict == "PASS" else "⚠ Aggressive optimization may degrade response quality - use conservative settings."} + {" The compressed context maintains semantic equivalence for model reasoning." if a_verdict == "PASS" else ""} """) diff --git a/examples/smart_vs_naive_eval.py b/examples/smart_vs_naive_eval.py index 4e2ded656..22d3e54a3 100644 --- a/examples/smart_vs_naive_eval.py +++ b/examples/smart_vs_naive_eval.py @@ -20,7 +20,7 @@ from datetime import datetime, timedelta from dotenv import load_dotenv from openai import OpenAI -from headroom import HeadroomClient, OpenAIProvider, ToolCrusherConfig, SmartCrusherConfig +from headroom import HeadroomClient, OpenAIProvider, SmartCrusherConfig, ToolCrusherConfig from headroom.config import HeadroomConfig from headroom.transforms import TransformPipeline @@ -91,6 +91,7 @@ baseline_client = HeadroomClient( # GENERATE TEST DATA WITH CLEAR PATTERNS # ============================================================================= + def generate_metrics_with_spike() -> str: """ Generate metrics data with a CLEAR spike pattern. @@ -111,22 +112,20 @@ def generate_metrics_with_spike() -> str: cpu = 85 + (i - 45) * 2 # Spike: 85 -> 115 error_rate = 5 + (i - 45) # Error spike too - data_points.append({ - "timestamp": ts.isoformat(), - "host": "prod-api-1", # CONSTANT - should be factored out - "region": "us-east-1", # CONSTANT - should be factored out - "datacenter": "dc-01", # CONSTANT - should be factored out - "cpu_percent": min(cpu, 99), - "memory_percent": 62, # CONSTANT - "error_rate": round(error_rate, 2), - "request_count": 1500 + (i * 10), - }) + data_points.append( + { + "timestamp": ts.isoformat(), + "host": "prod-api-1", # CONSTANT - should be factored out + "region": "us-east-1", # CONSTANT - should be factored out + "datacenter": "dc-01", # CONSTANT - should be factored out + "cpu_percent": min(cpu, 99), + "memory_percent": 62, # CONSTANT + "error_rate": round(error_rate, 2), + "request_count": 1500 + (i * 10), + } + ) - return json.dumps({ - "status": "success", - "metrics": data_points, - "query_time_ms": 127 - }) + return json.dumps({"status": "success", "metrics": data_points, "query_time_ms": 127}) def generate_clusterable_logs() -> str: @@ -161,21 +160,20 @@ def generate_clusterable_logs() -> str: ts = base_time + timedelta(seconds=i * 36) level, msg = message_templates[i % len(message_templates)] - logs.append({ - "@timestamp": ts.isoformat(), - "level": level, - "message": msg, - "service": "api-server", # CONSTANT - "environment": "production", # CONSTANT - "version": "2.4.1", # CONSTANT - "host": f"prod-api-{i % 3 + 1}", - "trace_id": f"trace-{1000+i:04d}", - }) + logs.append( + { + "@timestamp": ts.isoformat(), + "level": level, + "message": msg, + "service": "api-server", # CONSTANT + "environment": "production", # CONSTANT + "version": "2.4.1", # CONSTANT + "host": f"prod-api-{i % 3 + 1}", + "trace_id": f"trace-{1000 + i:04d}", + } + ) - return json.dumps({ - "took": 234, - "hits": {"total": len(logs), "hits": logs} - }) + return json.dumps({"took": 234, "hits": {"total": len(logs), "hits": logs}}) def generate_search_results() -> str: @@ -185,14 +183,16 @@ def generate_search_results() -> str: """ results = [] for i in range(30): - results.append({ - "id": f"doc-{i+1}", - "title": f"Result document {i+1}", - "snippet": f"This is the snippet for document {i+1} with relevant content...", - "score": 0.95 - (i * 0.02), # Decreasing relevance - "source": "knowledge_base", # CONSTANT - "category": "technical", # CONSTANT - }) + results.append( + { + "id": f"doc-{i + 1}", + "title": f"Result document {i + 1}", + "snippet": f"This is the snippet for document {i + 1} with relevant content...", + "score": 0.95 - (i * 0.02), # Decreasing relevance + "source": "knowledge_base", # CONSTANT + "category": "technical", # CONSTANT + } + ) return json.dumps({"results": results, "total": 30}) @@ -201,6 +201,7 @@ def generate_search_results() -> str: # BUILD TEST CONVERSATION # ============================================================================= + def build_test_conversation() -> list[dict]: """Build a conversation that exercises all SmartCrusher strategies.""" @@ -208,53 +209,47 @@ def build_test_conversation() -> list[dict]: { "role": "system", "content": """You are an SRE assistant. Analyze the data and provide insights. -Current Date: 2024-12-15T14:30:00Z""" +Current Date: 2024-12-15T14:30:00Z""", }, {"role": "user", "content": "Check the metrics for the last hour."}, { "role": "assistant", "content": None, - "tool_calls": [{ - "id": "call_1", - "type": "function", - "function": {"name": "query_metrics", "arguments": "{}"} - }] - }, - { - "role": "tool", - "tool_call_id": "call_1", - "content": generate_metrics_with_spike() + "tool_calls": [ + { + "id": "call_1", + "type": "function", + "function": {"name": "query_metrics", "arguments": "{}"}, + } + ], }, + {"role": "tool", "tool_call_id": "call_1", "content": generate_metrics_with_spike()}, {"role": "assistant", "content": "I see CPU metrics. Let me check the logs."}, { "role": "assistant", "content": None, - "tool_calls": [{ - "id": "call_2", - "type": "function", - "function": {"name": "search_logs", "arguments": "{}"} - }] - }, - { - "role": "tool", - "tool_call_id": "call_2", - "content": generate_clusterable_logs() + "tool_calls": [ + { + "id": "call_2", + "type": "function", + "function": {"name": "search_logs", "arguments": "{}"}, + } + ], }, + {"role": "tool", "tool_call_id": "call_2", "content": generate_clusterable_logs()}, {"role": "assistant", "content": "Found error patterns. Let me search docs."}, { "role": "assistant", "content": None, - "tool_calls": [{ - "id": "call_3", - "type": "function", - "function": {"name": "search_docs", "arguments": "{}"} - }] - }, - { - "role": "tool", - "tool_call_id": "call_3", - "content": generate_search_results() + "tool_calls": [ + { + "id": "call_3", + "type": "function", + "function": {"name": "search_docs", "arguments": "{}"}, + } + ], }, + {"role": "tool", "tool_call_id": "call_3", "content": generate_search_results()}, {"role": "user", "content": "What's the root cause and what should we do?"}, ] @@ -265,6 +260,7 @@ Current Date: 2024-12-15T14:30:00Z""" # EVALUATION # ============================================================================= + @dataclass class EvalResult: name: str @@ -299,7 +295,9 @@ def evaluate(client, messages: list[dict], name: str, mode: str) -> EvalResult: tokens_before=sim.tokens_before, tokens_after=tokens_in, tokens_saved=sim.tokens_before - tokens_in, - reduction_pct=(sim.tokens_before - tokens_in) / sim.tokens_before * 100 if sim.tokens_before else 0, + reduction_pct=(sim.tokens_before - tokens_in) / sim.tokens_before * 100 + if sim.tokens_before + else 0, transforms=sim.transforms, response=response.choices[0].message.content or "", latency_ms=latency, @@ -341,7 +339,7 @@ PASS = overall >= 4.0""" try: return json.loads(response.choices[0].message.content) - except: + except Exception: return {"error": "Parse failed"} @@ -380,12 +378,16 @@ def main(): print("\n2. NAIVE CRUSHER (fixed rules: keep first 10)...") naive = evaluate(naive_client, messages, "Naive", "optimize") - print(f" Tokens: {naive.tokens_after:,} (saved {naive.tokens_saved:,}, {naive.reduction_pct:.1f}%)") + print( + f" Tokens: {naive.tokens_after:,} (saved {naive.tokens_saved:,}, {naive.reduction_pct:.1f}%)" + ) print(f" Transforms: {naive.transforms}") print("\n3. SMART CRUSHER (statistical analysis)...") smart = evaluate(smart_client, messages, "Smart", "optimize") - print(f" Tokens: {smart.tokens_after:,} (saved {smart.tokens_saved:,}, {smart.reduction_pct:.1f}%)") + print( + f" Tokens: {smart.tokens_after:,} (saved {smart.tokens_saved:,}, {smart.reduction_pct:.1f}%)" + ) print(f" Transforms: {smart.transforms}") # Results comparison @@ -397,17 +399,25 @@ def main(): print(f"\n{'Method':<20} {'Tokens':>10} {'Saved':>10} {'Reduction':>10}") print("-" * 52) print(f"{'Baseline':<20} {baseline.tokens_after:>10,} {'-':>10} {'-':>10}") - print(f"{'Naive Crusher':<20} {naive.tokens_after:>10,} {naive.tokens_saved:>10,} {naive.reduction_pct:>9.1f}%") - print(f"{'Smart Crusher':<20} {smart.tokens_after:>10,} {smart.tokens_saved:>10,} {smart.reduction_pct:>9.1f}%") + print( + f"{'Naive Crusher':<20} {naive.tokens_after:>10,} {naive.tokens_saved:>10,} {naive.reduction_pct:>9.1f}%" + ) + print( + f"{'Smart Crusher':<20} {smart.tokens_after:>10,} {smart.tokens_saved:>10,} {smart.reduction_pct:>9.1f}%" + ) # Show the difference diff = naive.tokens_after - smart.tokens_after if diff > 0: - print(f"\n→ Smart Crusher saves {diff:,} MORE tokens than Naive ({diff/naive.tokens_after*100:.1f}% better)") + print( + f"\n→ Smart Crusher saves {diff:,} MORE tokens than Naive ({diff / naive.tokens_after * 100:.1f}% better)" + ) elif diff < 0: - print(f"\n→ Naive Crusher saves {-diff:,} MORE tokens than Smart ({-diff/smart.tokens_after*100:.1f}% better)") + print( + f"\n→ Naive Crusher saves {-diff:,} MORE tokens than Smart ({-diff / smart.tokens_after * 100:.1f}% better)" + ) else: - print(f"\n→ Both methods produce same token count") + print("\n→ Both methods produce same token count") # Quality evaluation print() @@ -429,8 +439,12 @@ def main(): s_score = smart_quality.get(criterion, {}).get("score", "?") print(f"{criterion.replace('_', ' ').title():<20} {n_score:>10}/5 {s_score:>10}/5") print("-" * 42) - print(f"{'OVERALL':<20} {naive_quality.get('overall', '?'):>10}/5 {smart_quality.get('overall', '?'):>10}/5") - print(f"{'VERDICT':<20} {naive_quality.get('verdict', '?'):>10} {smart_quality.get('verdict', '?'):>10}") + print( + f"{'OVERALL':<20} {naive_quality.get('overall', '?'):>10}/5 {smart_quality.get('overall', '?'):>10}/5" + ) + print( + f"{'VERDICT':<20} {naive_quality.get('verdict', '?'):>10} {smart_quality.get('verdict', '?'):>10}" + ) print("\n[Quality Analysis]") print(f" Naive: {naive_quality.get('data_awareness', {}).get('reason', 'N/A')}") @@ -453,12 +467,12 @@ SmartCrusher vs NaiveCrusher on SRE incident data: Token Efficiency: - Naive: {naive.reduction_pct:.1f}% reduction - Smart: {smart.reduction_pct:.1f}% reduction - - Winner: {'SMART' if smart.reduction_pct > naive.reduction_pct else 'NAIVE' if naive.reduction_pct > smart.reduction_pct else 'TIE'} (+{abs(smart.reduction_pct - naive.reduction_pct):.1f}% {'more' if smart.reduction_pct > naive.reduction_pct else 'less'} reduction) + - Winner: {"SMART" if smart.reduction_pct > naive.reduction_pct else "NAIVE" if naive.reduction_pct > smart.reduction_pct else "TIE"} (+{abs(smart.reduction_pct - naive.reduction_pct):.1f}% {"more" if smart.reduction_pct > naive.reduction_pct else "less"} reduction) Response Quality: - Naive: {n_overall}/5 ({n_verdict}) - Smart: {s_overall}/5 ({s_verdict}) - - Winner: {'SMART' if s_overall > n_overall else 'NAIVE' if n_overall > s_overall else 'TIE'} + - Winner: {"SMART" if s_overall > n_overall else "NAIVE" if n_overall > s_overall else "TIE"} Key Insight: SmartCrusher uses statistical analysis to preserve important data: diff --git a/headroom/cache/__init__.py b/headroom/cache/__init__.py index 88f5998c9..78400a539 100644 --- a/headroom/cache/__init__.py +++ b/headroom/cache/__init__.py @@ -25,6 +25,7 @@ Usage: CacheOptimizerRegistry.register("my-provider", MyOptimizer) """ +from .anthropic import AnthropicCacheOptimizer from .base import ( BaseCacheOptimizer, CacheBreakpoint, @@ -42,11 +43,10 @@ from .dynamic_detector import ( DynamicSpan, detect_dynamic_content, ) -from .registry import CacheOptimizerRegistry -from .anthropic import AnthropicCacheOptimizer -from .openai import OpenAICacheOptimizer from .google import GoogleCacheOptimizer -from .semantic import SemanticCacheLayer, SemanticCache +from .openai import OpenAICacheOptimizer +from .registry import CacheOptimizerRegistry +from .semantic import SemanticCache, SemanticCacheLayer __all__ = [ # Base types diff --git a/headroom/cache/anthropic.py b/headroom/cache/anthropic.py index 670ffa5b3..11559d7b5 100644 --- a/headroom/cache/anthropic.py +++ b/headroom/cache/anthropic.py @@ -39,7 +39,6 @@ from .base import ( OptimizationContext, ) - # Anthropic-specific constants ANTHROPIC_MIN_CACHEABLE_TOKENS = 1024 ANTHROPIC_MAX_BREAKPOINTS = 4 @@ -147,13 +146,9 @@ class AnthropicCacheOptimizer(BaseCacheOptimizer): warnings.extend(plan.warnings) # Step 4: Insert cache_control blocks - optimized_messages = self._insert_breakpoints( - optimized_messages, plan.breakpoints - ) + optimized_messages = self._insert_breakpoints(optimized_messages, plan.breakpoints) if plan.breakpoints: - transforms_applied.append( - f"inserted_{len(plan.breakpoints)}_cache_breakpoints" - ) + transforms_applied.append(f"inserted_{len(plan.breakpoints)}_cache_breakpoints") # Step 5: Compute metrics prefix_content = self._extract_cacheable_content(optimized_messages) @@ -194,9 +189,7 @@ class AnthropicCacheOptimizer(BaseCacheOptimizer): warnings=warnings, ) - def _analyze_sections( - self, messages: list[dict[str, Any]] - ) -> list[ContentSection]: + def _analyze_sections(self, messages: list[dict[str, Any]]) -> list[ContentSection]: """Analyze messages to identify distinct content sections.""" sections: list[ContentSection] = [] @@ -207,9 +200,13 @@ class AnthropicCacheOptimizer(BaseCacheOptimizer): if role == "system": section_type = "system" elif role == "user": - section_type = "examples" if self._looks_like_example(message, messages, idx) else "user" + section_type = ( + "examples" if self._looks_like_example(message, messages, idx) else "user" + ) elif role == "assistant": - section_type = "examples" if self._looks_like_example(message, messages, idx) else "assistant" + section_type = ( + "examples" if self._looks_like_example(message, messages, idx) else "assistant" + ) else: section_type = role @@ -227,17 +224,17 @@ class AnthropicCacheOptimizer(BaseCacheOptimizer): if isinstance(content, str): token_count = self._count_tokens_estimate(content) - is_cacheable, reason = self._assess_cacheability( - section_type, token_count, content + is_cacheable, reason = self._assess_cacheability(section_type, token_count, content) + sections.append( + ContentSection( + content=content, + section_type=section_type, + message_index=idx, + token_count=token_count, + is_cacheable=is_cacheable, + reason=reason, + ) ) - sections.append(ContentSection( - content=content, - section_type=section_type, - message_index=idx, - token_count=token_count, - is_cacheable=is_cacheable, - reason=reason, - )) elif isinstance(content, list): for block_idx, block in enumerate(content): @@ -247,15 +244,17 @@ class AnthropicCacheOptimizer(BaseCacheOptimizer): is_cacheable, reason = self._assess_cacheability( section_type, token_count, text ) - sections.append(ContentSection( - content=block, - section_type=section_type, - message_index=idx, - content_index=block_idx, - token_count=token_count, - is_cacheable=is_cacheable, - reason=reason, - )) + sections.append( + ContentSection( + content=block, + section_type=section_type, + message_index=idx, + content_index=block_idx, + token_count=token_count, + is_cacheable=is_cacheable, + reason=reason, + ) + ) return sections @@ -264,7 +263,10 @@ class AnthropicCacheOptimizer(BaseCacheOptimizer): ) -> tuple[bool, str]: """Assess whether a section is cacheable.""" if token_count < self.config.min_cacheable_tokens: - return False, f"Below minimum tokens ({token_count} < {self.config.min_cacheable_tokens})" + return ( + False, + f"Below minimum tokens ({token_count} < {self.config.min_cacheable_tokens})", + ) if section_type == "system": return True, "System prompts are highly cacheable" @@ -318,6 +320,7 @@ class AnthropicCacheOptimizer(BaseCacheOptimizer): def _estimate_tools_tokens(self, tools: Any) -> int: """Estimate token count for tool definitions.""" import json + try: return self._count_tokens_estimate(json.dumps(tools)) except (TypeError, ValueError): @@ -353,9 +356,7 @@ class AnthropicCacheOptimizer(BaseCacheOptimizer): return messages, transforms - def _stabilize_text( - self, text: str, config: CacheConfig - ) -> tuple[str, list[str]]: + def _stabilize_text(self, text: str, config: CacheConfig) -> tuple[str, list[str]]: """Stabilize a text string.""" transforms: list[str] = [] result = text @@ -408,9 +409,7 @@ class AnthropicCacheOptimizer(BaseCacheOptimizer): for section in cacheable: if len(selected) >= config.max_breakpoints: - plan.warnings.append( - f"Reached maximum breakpoints ({config.max_breakpoints})" - ) + plan.warnings.append(f"Reached maximum breakpoints ({config.max_breakpoints})") break selected.append(section) diff --git a/headroom/cache/base.py b/headroom/cache/base.py index 617c961fa..ed03be647 100644 --- a/headroom/cache/base.py +++ b/headroom/cache/base.py @@ -82,11 +82,13 @@ class CacheConfig: max_breakpoints: int = 4 # Patterns to extract and move to dynamic section - date_patterns: list[str] = field(default_factory=lambda: [ - r"Today is \w+ \d{1,2},? \d{4}\.?", - r"Current date: \d{4}-\d{2}-\d{2}", - r"The current time is .+\.", - ]) + date_patterns: list[str] = field( + default_factory=lambda: [ + r"Today is \w+ \d{1,2},? \d{4}\.?", + r"Current date: \d{4}-\d{2}-\d{2}", + r"The current time is .+\.", + ] + ) # Whether to normalize whitespace normalize_whitespace: bool = True @@ -317,6 +319,7 @@ class BaseCacheOptimizer(ABC): def _compute_prefix_hash(self, content: str) -> str: """Compute a short hash of content.""" import hashlib + return hashlib.sha256(content.encode()).hexdigest()[:12] def _extract_system_content(self, messages: list[dict[str, Any]]) -> str: diff --git a/headroom/cache/compression_feedback.py b/headroom/cache/compression_feedback.py index dee53947a..20413ade4 100644 --- a/headroom/cache/compression_feedback.py +++ b/headroom/cache/compression_feedback.py @@ -30,7 +30,6 @@ from __future__ import annotations import re import threading import time -from collections import defaultdict from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any @@ -185,7 +184,9 @@ class CompressionFeedback: # Time-based tracking self._last_analysis: float = 0.0 self._analysis_interval: float = analysis_interval - self._last_event_timestamp: float = 0.0 # Track last processed event to avoid double-counting + self._last_event_timestamp: float = ( + 0.0 # Track last processed event to avoid double-counting + ) # Global statistics self._total_compressions: int = 0 @@ -196,6 +197,7 @@ class CompressionFeedback: """Get the compression store (lazy load global if not set).""" if self._store is None: from .compression_store import get_compression_store + self._store = get_compression_store() return self._store @@ -301,9 +303,7 @@ class CompressionFeedback: # Track query patterns if event.query: query_lower = event.query.lower() - pattern.common_queries[query_lower] = ( - pattern.common_queries.get(query_lower, 0) + 1 - ) + pattern.common_queries[query_lower] = pattern.common_queries.get(query_lower, 0) + 1 # HIGH: Limit common_queries dict to prevent unbounded growth if len(pattern.common_queries) > 100: @@ -325,32 +325,32 @@ class CompressionFeedback: from both dicts, then truncate both to the same key set. """ # Get top 40 strategies from each dict (using 40 to allow union to stay under 50) - top_compressions = set( - k for k, _ in sorted( + top_compressions = { + k + for k, _ in sorted( pattern.strategy_compressions.items(), key=lambda x: x[1], reverse=True, )[:40] - ) - top_retrievals = set( - k for k, _ in sorted( + } + top_retrievals = { + k + for k, _ in sorted( pattern.strategy_retrievals.items(), key=lambda x: x[1], reverse=True, )[:40] - ) + } # Keep union of top strategies from both keys_to_keep = top_compressions | top_retrievals # Truncate both dicts to same keys pattern.strategy_compressions = { - k: v for k, v in pattern.strategy_compressions.items() - if k in keys_to_keep + k: v for k, v in pattern.strategy_compressions.items() if k in keys_to_keep } pattern.strategy_retrievals = { - k: v for k, v in pattern.strategy_retrievals.items() - if k in keys_to_keep + k: v for k, v in pattern.strategy_retrievals.items() if k in keys_to_keep } def _extract_field_hints(self, pattern: LocalToolPattern, query: str) -> None: @@ -361,22 +361,30 @@ class CompressionFeedback: - JSON field names like "status", "error", "id" """ # Look for field:value patterns - field_patterns = re.findall(r'(\w+)[=:]', query) - for field in field_patterns: - pattern.queried_fields[field] = ( - pattern.queried_fields.get(field, 0) + 1 - ) + field_patterns = re.findall(r"(\w+)[=:]", query) + for field_name in field_patterns: + pattern.queried_fields[field_name] = pattern.queried_fields.get(field_name, 0) + 1 # Look for common JSON field names common_fields = [ - "id", "name", "status", "error", "message", "type", - "code", "result", "value", "data", "items", "count", + "id", + "name", + "status", + "error", + "message", + "type", + "code", + "result", + "value", + "data", + "items", + "count", ] query_lower = query.lower() - for field in common_fields: - if field in query_lower: - pattern.queried_fields[field] = ( - pattern.queried_fields.get(field, 0) + 1 + for common_field in common_fields: + if common_field in query_lower: + pattern.queried_fields[common_field] = ( + pattern.queried_fields.get(common_field, 0) + 1 ) # HIGH: Limit queried_fields dict to prevent unbounded growth @@ -459,8 +467,7 @@ class CompressionFeedback: hints.suggested_items = 10 hints.aggressiveness = 0.7 hints.reason = ( - f"Low retrieval rate ({retrieval_rate:.0%}), " - f"current compression is effective" + f"Low retrieval rate ({retrieval_rate:.0%}), current compression is effective" ) # Add field preservation hints based on common queries @@ -488,6 +495,7 @@ class CompressionFeedback: HIGH FIX: Returns deep copies to prevent external mutation of internal state. """ import copy as copy_module + with self._lock: # Deep copy to prevent external code from modifying internal state return copy_module.deepcopy(self._tool_patterns) @@ -504,7 +512,8 @@ class CompressionFeedback: "total_retrievals": self._total_retrievals, "global_retrieval_rate": ( self._total_retrievals / self._total_compressions - if self._total_compressions > 0 else 0.0 + if self._total_compressions > 0 + else 0.0 ), "tools_tracked": len(self._tool_patterns), "tool_patterns": { diff --git a/headroom/cache/compression_store.py b/headroom/cache/compression_store.py index e8da28e40..ab767c206 100644 --- a/headroom/cache/compression_store.py +++ b/headroom/cache/compression_store.py @@ -33,7 +33,6 @@ Usage: from __future__ import annotations -import copy import hashlib import heapq import json @@ -231,8 +230,7 @@ class CompressionStore: # True hash collision - different content, same hash # This is extremely rare with SHA256[:24] but should be logged logger.warning( - "Hash collision detected: hash=%s tool=%s " - "(existing_len=%d, new_len=%d)", + "Hash collision detected: hash=%s tool=%s (existing_len=%d, new_len=%d)", hash_key, tool_name, len(existing.original_content), @@ -437,15 +435,9 @@ class CompressionStore: # Clean expired entries self._clean_expired() - total_original_tokens = sum( - e.original_tokens for e in self._store.values() - ) - total_compressed_tokens = sum( - e.compressed_tokens for e in self._store.values() - ) - total_retrievals = sum( - e.retrieval_count for e in self._store.values() - ) + total_original_tokens = sum(e.original_tokens for e in self._store.values()) + total_compressed_tokens = sum(e.compressed_tokens for e in self._store.values()) + total_retrievals = sum(e.retrieval_count for e in self._store.values()) return { "entry_count": len(self._store), @@ -534,10 +526,7 @@ class CompressionStore: CRITICAL FIX: Track stale heap entries when deleting to prevent memory leak. """ - expired_keys = [ - key for key, entry in self._store.items() - if entry.is_expired() - ] + expired_keys = [key for key, entry in self._store.items() if entry.is_expired()] for key in expired_keys: del self._store[key] # CRITICAL FIX: Increment stale counter - the heap still has an entry @@ -552,8 +541,7 @@ class CompressionStore: """ # Build new heap from current store entries only self._eviction_heap = [ - (entry.created_at, hash_key) - for hash_key, entry in self._store.items() + (entry.created_at, hash_key) for hash_key, entry in self._store.items() ] heapq.heapify(self._eviction_heap) # Reset stale counter - heap is now clean @@ -630,7 +618,7 @@ class CompressionStore: # Keep only recent events if len(self._retrieval_events) > self._max_events: - self._retrieval_events = self._retrieval_events[-self._max_events:] + self._retrieval_events = self._retrieval_events[-self._max_events :] # Queue event for feedback processing (will be processed after lock release) # This is safe because process_pending_feedback() uses the lock to atomically @@ -648,9 +636,9 @@ class CompressionStore: This is called automatically on each retrieval to ensure the feedback loop operates in real-time. """ - from .compression_feedback import get_compression_feedback from ..telemetry import get_telemetry_collector from ..telemetry.toin import get_toin + from .compression_feedback import get_compression_feedback # Get pending events and related entry data atomically with self._lock: @@ -664,12 +652,14 @@ class CompressionStore: if entry: # Use the ACTUAL tool_signature_hash stored during compression # This MUST match the hash used by SmartCrusher - event_data.append(( - event, - entry.tool_name, - entry.tool_signature_hash, # The correct hash! - entry.compression_strategy, - )) + event_data.append( + ( + event, + entry.tool_name, + entry.tool_signature_hash, # The correct hash! + entry.compression_strategy, + ) + ) else: event_data.append((event, None, None, None)) @@ -679,7 +669,7 @@ class CompressionStore: telemetry = get_telemetry_collector() toin = get_toin() - for event, tool_name, sig_hash, strategy in event_data: + for event, _tool_name, sig_hash, strategy in event_data: # Notify feedback system (pass strategy for success rate tracking) feedback.record_retrieval(event, strategy=strategy) @@ -687,7 +677,7 @@ class CompressionStore: query_fields = None if event.query: # Extract field:value patterns - query_fields = re.findall(r'(\w+)[=:]', event.query) + query_fields = re.findall(r"(\w+)[=:]", event.query) # Notify telemetry for data flywheel try: diff --git a/headroom/cache/dynamic_detector.py b/headroom/cache/dynamic_detector.py index 01a2d60ab..46c641b37 100644 --- a/headroom/cache/dynamic_detector.py +++ b/headroom/cache/dynamic_detector.py @@ -44,13 +44,15 @@ _SENTENCE_TRANSFORMERS_AVAILABLE = False try: import spacy + _SPACY_AVAILABLE = True except ImportError: spacy = None # type: ignore try: - from sentence_transformers import SentenceTransformer import numpy as np + from sentence_transformers import SentenceTransformer + _SENTENCE_TRANSFORMERS_AVAILABLE = True except ImportError: SentenceTransformer = None # type: ignore @@ -138,35 +140,77 @@ class DetectorConfig: """Configuration for the dynamic content detector.""" # Which tiers to enable (order matters - later tiers can use earlier results) - tiers: list[Literal["regex", "ner", "semantic"]] = field( - default_factory=lambda: ["regex"] - ) + tiers: list[Literal["regex", "ner", "semantic"]] = field(default_factory=lambda: ["regex"]) # Tier 1: Structural labels that indicate dynamic content # These are the KEY names that hint the VALUE is dynamic # Users can add domain-specific labels - dynamic_labels: list[str] = field(default_factory=lambda: [ - # Time-related - "date", "time", "timestamp", "datetime", "created", "updated", - "modified", "expires", "last", "current", "today", "now", - # Identifiers - "id", "uuid", "guid", "session", "request", "trace", "span", - "transaction", "correlation", "token", "key", "secret", - # User-related - "user", "username", "email", "name", "phone", "address", - "customer", "client", "employee", "member", - # System state - "version", "build", "commit", "branch", "revision", - "status", "state", "count", "total", "balance", "remaining", - "load", "queue", "active", "pending", - # Order/ticket related - "order", "ticket", "case", "invoice", "reference", - ]) + dynamic_labels: list[str] = field( + default_factory=lambda: [ + # Time-related + "date", + "time", + "timestamp", + "datetime", + "created", + "updated", + "modified", + "expires", + "last", + "current", + "today", + "now", + # Identifiers + "id", + "uuid", + "guid", + "session", + "request", + "trace", + "span", + "transaction", + "correlation", + "token", + "key", + "secret", + # User-related + "user", + "username", + "email", + "name", + "phone", + "address", + "customer", + "client", + "employee", + "member", + # System state + "version", + "build", + "commit", + "branch", + "revision", + "status", + "state", + "count", + "total", + "balance", + "remaining", + "load", + "queue", + "active", + "pending", + # Order/ticket related + "order", + "ticket", + "case", + "invoice", + "reference", + ] + ) # Tier 1: Custom regex patterns (user-provided) - custom_patterns: list[tuple[str, DynamicCategory]] = field( - default_factory=list - ) + custom_patterns: list[tuple[str, DynamicCategory]] = field(default_factory=list) # Entropy threshold for detecting random strings (0-1 scale normalized) # Higher = more selective (only very random strings) @@ -237,48 +281,47 @@ class RegexDetector: # Universal patterns (these formats are language-agnostic) UNIVERSAL_PATTERNS = [ # UUID - truly universal format - (r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}", - DynamicCategory.UUID, "uuid"), - + ( + r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}", + DynamicCategory.UUID, + "uuid", + ), # ISO 8601 datetime (most universal date format) - (r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?", - DynamicCategory.DATETIME, "iso_datetime"), - + ( + r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?", + DynamicCategory.DATETIME, + "iso_datetime", + ), # ISO 8601 date only - (r"\d{4}-\d{2}-\d{2}(?!\d)", - DynamicCategory.DATE, "iso_date"), - + (r"\d{4}-\d{2}-\d{2}(?!\d)", DynamicCategory.DATE, "iso_date"), # Unix timestamps (10-13 digits, but NOT within longer numbers) - (r"(?(?:{labels_pattern}))(?P\s*[:=]\s*|\s+)(?P[^\n,;]+)", - re.IGNORECASE + re.IGNORECASE, ) # Compile custom patterns @@ -319,15 +362,17 @@ class RegexDetector: if end - start < self.config.min_span_length: continue - spans.append(DynamicSpan( - text=match.group(), - start=start, - end=end, - category=category, - tier="regex", - confidence=1.0, - metadata={"pattern": pattern_name, "method": "universal"}, - )) + spans.append( + DynamicSpan( + text=match.group(), + start=start, + end=end, + category=category, + tier="regex", + confidence=1.0, + metadata={"pattern": pattern_name, "method": "universal"}, + ) + ) seen_ranges.add((start, end)) # 2. Structural detection: "Label: value" patterns @@ -355,15 +400,17 @@ class RegexDetector: if not value.strip(): continue - spans.append(DynamicSpan( - text=value, - start=value_start, - end=value_end, - category=category, - tier="regex", - confidence=0.9, - metadata={"pattern": "structural", "method": "structural", "label": label}, - )) + spans.append( + DynamicSpan( + text=value, + start=value_start, + end=value_end, + category=category, + tier="regex", + confidence=0.9, + metadata={"pattern": "structural", "method": "structural", "label": label}, + ) + ) seen_ranges.add((value_start, value_end)) # 3. Entropy-based detection for remaining potential IDs @@ -378,15 +425,17 @@ class RegexDetector: if end - start < self.config.min_span_length: continue - spans.append(DynamicSpan( - text=match.group(), - start=start, - end=end, - category=category, - tier="regex", - confidence=0.8, - metadata={"pattern": "custom", "method": "custom"}, - )) + spans.append( + DynamicSpan( + text=match.group(), + start=start, + end=end, + category=category, + tier="regex", + confidence=0.8, + metadata={"pattern": "custom", "method": "custom"}, + ) + ) seen_ranges.add((start, end)) return sorted(spans, key=lambda s: s.start) @@ -432,15 +481,17 @@ class RegexDetector: entropy = calculate_entropy(text) if entropy >= self.config.entropy_threshold: - spans.append(DynamicSpan( - text=text, - start=start, - end=end, - category=DynamicCategory.IDENTIFIER, - tier="regex", - confidence=entropy, # Use entropy as confidence - metadata={"pattern": "entropy", "method": "entropy", "entropy": entropy}, - )) + spans.append( + DynamicSpan( + text=text, + start=start, + end=end, + category=DynamicCategory.IDENTIFIER, + tier="regex", + confidence=entropy, # Use entropy as confidence + metadata={"pattern": "entropy", "method": "entropy", "entropy": entropy}, + ) + ) seen_ranges.add((start, end)) return spans @@ -452,10 +503,7 @@ class RegexDetector: seen_ranges: set[tuple[int, int]], ) -> bool: """Check if range overlaps with any existing range.""" - return any( - not (end <= s or start >= e) - for s, e in seen_ranges - ) + return any(not (end <= s or start >= e) for s, e in seen_ranges) def _categorize_label(self, label: str) -> DynamicCategory: """Categorize based on the label name.""" @@ -478,15 +526,35 @@ class RegexDetector: return DynamicCategory.REQUEST_ID # User-related - if label in {"user", "username", "email", "name", "phone", "address", - "customer", "client", "employee", "member"}: + if label in { + "user", + "username", + "email", + "name", + "phone", + "address", + "customer", + "client", + "employee", + "member", + }: return DynamicCategory.USER_DATA # System state if label in {"version", "build", "commit", "branch", "revision"}: return DynamicCategory.VERSION - if label in {"status", "state", "count", "total", "balance", "remaining", - "load", "queue", "active", "pending"}: + if label in { + "status", + "state", + "count", + "total", + "balance", + "remaining", + "load", + "queue", + "active", + "pending", + }: return DynamicCategory.VOLATILE # Order/ticket @@ -570,8 +638,7 @@ class NERDetector: # Check for overlap with existing spans overlaps = any( - not (ent.end_char <= s or ent.start_char >= e) - for s, e in existing_ranges + not (ent.end_char <= s or ent.start_char >= e) for s, e in existing_ranges ) if overlaps: continue @@ -583,15 +650,17 @@ class NERDetector: if category == DynamicCategory.UNKNOWN: continue - spans.append(DynamicSpan( - text=ent.text, - start=ent.start_char, - end=ent.end_char, - category=category, - tier="ner", - confidence=0.9, - metadata={"entity_type": ent.label_}, - )) + spans.append( + DynamicSpan( + text=ent.text, + start=ent.start_char, + end=ent.end_char, + category=category, + tier="ner", + confidence=0.9, + metadata={"entity_type": ent.label_}, + ) + ) existing_ranges.add((ent.start_char, ent.end_char)) return sorted(spans, key=lambda s: s.start), None @@ -611,18 +680,15 @@ class SemanticDetector: "Real-time data", "Live prices", "Current stock price", - # Session-specific "Your session ID", "Your account balance", "Your recent orders", "Your conversation history", - # User-specific "Hello [user]", "Dear customer", "Your name is", - # System state "Server status", "System load", @@ -707,10 +773,7 @@ class SemanticDetector: continue # Check overlap with existing spans - overlaps = any( - not (end <= s or start >= e) - for s, e in existing_ranges - ) + overlaps = any(not (end <= s or start >= e) for s, e in existing_ranges) if overlaps: continue @@ -721,18 +784,20 @@ class SemanticDetector: # Determine category based on exemplar category = self._categorize_exemplar(best_exemplar) - spans.append(DynamicSpan( - text=text, - start=start, - end=end, - category=category, - tier="semantic", - confidence=max_sim, - metadata={ - "matched_exemplar": best_exemplar, - "similarity": max_sim, - }, - )) + spans.append( + DynamicSpan( + text=text, + start=start, + end=end, + category=category, + tier="semantic", + confidence=max_sim, + metadata={ + "matched_exemplar": best_exemplar, + "similarity": max_sim, + }, + ) + ) existing_ranges.add((start, end)) return sorted(spans, key=lambda s: s.start), None @@ -740,7 +805,7 @@ class SemanticDetector: def _split_sentences(self, content: str) -> list[tuple[str, int, int]]: """Split content into sentences with positions.""" sentences: list[tuple[str, int, int]] = [] - pattern = r'[^.!?\n]+[.!?\n]?' + pattern = r"[^.!?\n]+[.!?\n]?" for match in re.finditer(pattern, content): text = match.group().strip() if len(text) > 10: @@ -820,6 +885,7 @@ class DynamicContentDetector: DetectionResult with spans, static/dynamic content split, etc. """ import time + start_time = time.perf_counter() all_spans: list[DynamicSpan] = [] @@ -881,7 +947,7 @@ class DynamicContentDetector: for span in reversed(spans): dynamic_parts.append(span.text) - static = static[:span.start] + static[span.end:] + static = static[: span.start] + static[span.end :] static = self._clean_static_content(static) dynamic_parts.reverse() diff --git a/headroom/cache/google.py b/headroom/cache/google.py index 84e325c0f..360aacc68 100644 --- a/headroom/cache/google.py +++ b/headroom/cache/google.py @@ -274,16 +274,13 @@ class GoogleCacheOptimizer(BaseCacheOptimizer): Returns: CacheResult with analysis and cache information """ - effective_config = config or self.config # Extract cacheable content (system messages + static context) cacheable_content = self._extract_cacheable_content(messages) content_hash = self._compute_prefix_hash(cacheable_content) # Estimate tokens - total_tokens = self._count_tokens_estimate( - self._messages_to_text(messages) - ) + total_tokens = self._count_tokens_estimate(self._messages_to_text(messages)) cacheable_tokens = self._count_tokens_estimate(cacheable_content) # Check for existing cache @@ -371,9 +368,7 @@ class GoogleCacheOptimizer(BaseCacheOptimizer): cacheable_content = self._extract_cacheable_content(messages) content_hash = self._compute_prefix_hash(cacheable_content) - total_tokens = self._count_tokens_estimate( - self._messages_to_text(messages) - ) + total_tokens = self._count_tokens_estimate(self._messages_to_text(messages)) cacheable_tokens = self._count_tokens_estimate(cacheable_content) is_cacheable = cacheable_tokens >= GOOGLE_MIN_CACHE_TOKENS @@ -384,33 +379,30 @@ class GoogleCacheOptimizer(BaseCacheOptimizer): if not is_cacheable: recommendations.append( - f"Add {tokens_below_minimum:,} more tokens to static content " - f"to enable caching" + f"Add {tokens_below_minimum:,} more tokens to static content to enable caching" ) recommendations.append( "Consider adding detailed examples or documentation to system prompt" ) else: recommendations.append( - f"Content is cacheable. Create cache with google-generativeai SDK" + "Content is cacheable. Create cache with google-generativeai SDK" ) # Storage cost estimation (rough - actual pricing varies) # Assuming ~$0.001 per 1000 tokens per hour (simplified) hourly_cost = (cacheable_tokens / 1000) * 0.001 - recommendations.append( - f"Estimated storage cost: ~${hourly_cost:.4f}/hour" - ) + recommendations.append(f"Estimated storage cost: ~${hourly_cost:.4f}/hour") # Break-even analysis if hourly_cost > 0: # Assuming $0.01 per 1000 input tokens base price base_cost_per_request = (cacheable_tokens / 1000) * 0.01 savings_per_request = base_cost_per_request * GOOGLE_CACHE_DISCOUNT - break_even_requests = hourly_cost / savings_per_request if savings_per_request > 0 else float('inf') - recommendations.append( - f"Break-even: ~{int(break_even_requests)} requests/hour" + break_even_requests = ( + hourly_cost / savings_per_request if savings_per_request > 0 else float("inf") ) + recommendations.append(f"Break-even: ~{int(break_even_requests)} requests/hour") return CacheabilityAnalysis( is_cacheable=is_cacheable, @@ -580,9 +572,7 @@ class GoogleCacheOptimizer(BaseCacheOptimizer): old_expires = cache_info.expires_at cache_info.expires_at = new_expires_at - logger.info( - f"Extended cache {cache_id} TTL from {old_expires} to {new_expires_at}" - ) + logger.info(f"Extended cache {cache_id} TTL from {old_expires} to {new_expires_at}") return cache_info @@ -721,8 +711,7 @@ class GoogleCacheOptimizer(BaseCacheOptimizer): if not analysis.is_cacheable: logger.debug( - f"Content not cacheable: {analysis.tokens_below_minimum} " - f"tokens below minimum" + f"Content not cacheable: {analysis.tokens_below_minimum} tokens below minimum" ) return None @@ -764,8 +753,7 @@ class GoogleCacheOptimizer(BaseCacheOptimizer): "cached_content": cache_id, "contents": dynamic_messages, "_headroom_note": ( - "Use cached_content parameter with GenerativeModel " - "to leverage the cache" + "Use cached_content parameter with GenerativeModel to leverage the cache" ), } diff --git a/headroom/cache/openai.py b/headroom/cache/openai.py index 6f23806b2..fa7c9ff4c 100644 --- a/headroom/cache/openai.py +++ b/headroom/cache/openai.py @@ -42,10 +42,9 @@ Usage: from __future__ import annotations -import re from copy import deepcopy from dataclasses import dataclass, field -from typing import Any, Literal +from typing import Any from .base import ( BaseCacheOptimizer, @@ -233,12 +232,8 @@ class OpenAICacheOptimizer(BaseCacheOptimizer): warnings.extend(result.warnings) if result.spans: - transforms_applied.append( - f"extracted_{len(result.spans)}_dynamic_elements" - ) - transforms_applied.extend( - f"tier_{tier}" for tier in result.tiers_used - ) + transforms_applied.append(f"extracted_{len(result.spans)}_dynamic_elements") + transforms_applied.extend(f"tier_{tier}" for tier in result.tiers_used) # Get static content with dynamic parts removed stabilized = result.static_content @@ -382,8 +377,7 @@ class OpenAICacheOptimizer(BaseCacheOptimizer): # Check if prefix is stable current_hash = self._compute_prefix_hash(system_content) likely_hit = ( - self._previous_prefix_hash is not None - and current_hash == self._previous_prefix_hash + self._previous_prefix_hash is not None and current_hash == self._previous_prefix_hash ) if likely_hit: @@ -427,7 +421,9 @@ class OpenAICacheOptimizer(BaseCacheOptimizer): leading = len(line) - len(line.lstrip()) # Collapse multiple spaces in content (not indentation) content_part = " ".join(stripped.split()) - normalized_lines.append(" " * leading + content_part[leading:] if leading else content_part) + normalized_lines.append( + " " * leading + content_part[leading:] if leading else content_part + ) else: normalized_lines.append("") @@ -581,11 +577,8 @@ class OpenAICacheOptimizer(BaseCacheOptimizer): for block in content: if isinstance(block, dict): if block.get("type") == "text": - total += self._count_tokens_estimate( - block.get("text", "") - ) + total += self._count_tokens_estimate(block.get("text", "")) elif block.get("type") == "image_url": # Rough estimate for images total += 85 # Base cost return total - diff --git a/headroom/cache/registry.py b/headroom/cache/registry.py index 26856052e..6dca25add 100644 --- a/headroom/cache/registry.py +++ b/headroom/cache/registry.py @@ -7,9 +7,7 @@ This allows users to swap implementations and register custom optimizers. from __future__ import annotations -from typing import Type - -from .base import CacheOptimizer, BaseCacheOptimizer, CacheConfig +from .base import BaseCacheOptimizer, CacheConfig class CacheOptimizerRegistry: @@ -32,14 +30,14 @@ class CacheOptimizerRegistry: CacheOptimizerRegistry.register("my-provider", MyOptimizer) """ - _optimizers: dict[str, Type[BaseCacheOptimizer]] = {} + _optimizers: dict[str, type[BaseCacheOptimizer]] = {} _instances: dict[str, BaseCacheOptimizer] = {} @classmethod def register( cls, name: str, - optimizer_class: Type[BaseCacheOptimizer], + optimizer_class: type[BaseCacheOptimizer], *, override: bool = False, ) -> None: @@ -56,8 +54,7 @@ class CacheOptimizerRegistry: """ if name in cls._optimizers and not override: raise ValueError( - f"Optimizer '{name}' already registered. " - f"Use override=True to replace." + f"Optimizer '{name}' already registered. Use override=True to replace." ) cls._optimizers[name] = optimizer_class # Clear cached instance if exists @@ -109,10 +106,7 @@ class CacheOptimizerRegistry: if key not in cls._optimizers: available = list(cls._optimizers.keys()) - raise KeyError( - f"No optimizer registered for '{key}'. " - f"Available: {available}" - ) + raise KeyError(f"No optimizer registered for '{key}'. Available: {available}") # Return cached instance if requested cache_key = f"{key}:{id(config)}" if config else key @@ -165,8 +159,8 @@ def _register_defaults() -> None: """Register default optimizers.""" # Import here to avoid circular imports from .anthropic import AnthropicCacheOptimizer - from .openai import OpenAICacheOptimizer from .google import GoogleCacheOptimizer + from .openai import OpenAICacheOptimizer CacheOptimizerRegistry.register("anthropic", AnthropicCacheOptimizer) CacheOptimizerRegistry.register("openai", OpenAICacheOptimizer) diff --git a/headroom/cache/semantic.py b/headroom/cache/semantic.py index 17ecafec1..8ec26a161 100644 --- a/headroom/cache/semantic.py +++ b/headroom/cache/semantic.py @@ -38,8 +38,9 @@ from __future__ import annotations import hashlib import time from collections import OrderedDict -from dataclasses import dataclass, field -from typing import Any, Callable +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any from .base import ( BaseCacheOptimizer, @@ -303,7 +304,8 @@ class SemanticCache: now = time.time() expired = [ - key for key, entry in self._cache.items() + key + for key, entry in self._cache.items() if now - entry.created_at > self.config.ttl_seconds ] @@ -440,6 +442,7 @@ class SemanticCacheLayer: def _compute_messages_hash(self, messages: list[dict[str, Any]]) -> str: """Compute a hash of all messages.""" import json + try: content = json.dumps(messages, sort_keys=True) return hashlib.sha256(content.encode()).hexdigest()[:24] diff --git a/headroom/ccr/__init__.py b/headroom/ccr/__init__.py index 61af10d85..c3ae014a9 100644 --- a/headroom/ccr/__init__.py +++ b/headroom/ccr/__init__.py @@ -21,6 +21,7 @@ from .tool_injection import ( # MCP server is optional (requires mcp package) try: from .mcp_server import CCRMCPServer, create_ccr_mcp_server + MCP_SERVER_AVAILABLE = True except ImportError: CCRMCPServer = None # type: ignore diff --git a/headroom/ccr/mcp_server.py b/headroom/ccr/mcp_server.py index e87807530..e64015a42 100644 --- a/headroom/ccr/mcp_server.py +++ b/headroom/ccr/mcp_server.py @@ -32,7 +32,6 @@ import asyncio import json import logging import os -import sys from typing import Any # Try to import MCP SDK @@ -40,6 +39,7 @@ try: from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import TextContent, Tool + MCP_AVAILABLE = True except ImportError: MCP_AVAILABLE = False @@ -49,6 +49,7 @@ except ImportError: # Try to import httpx for proxy communication try: import httpx + HTTPX_AVAILABLE = True except ImportError: HTTPX_AVAILABLE = False @@ -88,14 +89,11 @@ class CCRMCPServer: self._http_client: httpx.AsyncClient | None = None if not MCP_AVAILABLE: - raise ImportError( - "MCP SDK not installed. Install with: pip install mcp" - ) + raise ImportError("MCP SDK not installed. Install with: pip install mcp") if not direct_mode and not HTTPX_AVAILABLE: raise ImportError( - "httpx not installed (required for HTTP mode). " - "Install with: pip install httpx" + "httpx not installed (required for HTTP mode). Install with: pip install httpx" ) self.server = Server("headroom-ccr") @@ -140,19 +138,23 @@ class CCRMCPServer: async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]: """Handle tool calls.""" if name != CCR_TOOL_NAME: - return [TextContent( - type="text", - text=json.dumps({"error": f"Unknown tool: {name}"}), - )] + return [ + TextContent( + type="text", + text=json.dumps({"error": f"Unknown tool: {name}"}), + ) + ] hash_key = arguments.get("hash") query = arguments.get("query") if not hash_key: - return [TextContent( - type="text", - text=json.dumps({"error": "hash parameter is required"}), - )] + return [ + TextContent( + type="text", + text=json.dumps({"error": "hash parameter is required"}), + ) + ] # Retrieve content try: @@ -161,16 +163,20 @@ class CCRMCPServer: else: result = await self._retrieve_via_proxy(hash_key, query) - return [TextContent( - type="text", - text=json.dumps(result, indent=2), - )] + return [ + TextContent( + type="text", + text=json.dumps(result, indent=2), + ) + ] except Exception as e: logger.error(f"Retrieval failed: {e}") - return [TextContent( - type="text", - text=json.dumps({"error": str(e)}), - )] + return [ + TextContent( + type="text", + text=json.dumps({"error": str(e)}), + ) + ] async def _retrieve_via_proxy( self, diff --git a/headroom/ccr/tool_injection.py b/headroom/ccr/tool_injection.py index 8be5e9cc2..752226c73 100644 --- a/headroom/ccr/tool_injection.py +++ b/headroom/ccr/tool_injection.py @@ -321,10 +321,12 @@ class CCRToolInjector: else: # Append instructions if isinstance(content, str): - updated_messages.append({ - **message, - "content": content + instructions, - }) + updated_messages.append( + { + **message, + "content": content + instructions, + } + ) else: # Handle structured content updated_messages.append(message) @@ -333,10 +335,13 @@ class CCRToolInjector: # If no system message, prepend one if not system_found: - updated_messages.insert(0, { - "role": "system", - "content": instructions.strip(), - }) + updated_messages.insert( + 0, + { + "role": "system", + "content": instructions.strip(), + }, + ) return updated_messages diff --git a/headroom/cli.py b/headroom/cli.py index 65a4db367..f9699dc9b 100644 --- a/headroom/cli.py +++ b/headroom/cli.py @@ -30,6 +30,7 @@ def get_version() -> str: """Get the current version.""" try: from headroom import __version__ + return __version__ except ImportError: return "unknown" @@ -63,9 +64,9 @@ def cmd_proxy(args: argparse.Namespace) -> int: Starting proxy server... URL: http://{config.host}:{config.port} - Optimization: {'ENABLED' if config.optimize else 'DISABLED'} - Caching: {'ENABLED' if config.cache_enabled else 'DISABLED'} - Rate Limit: {'ENABLED' if config.rate_limit_enabled else 'DISABLED'} + Optimization: {"ENABLED" if config.optimize else "DISABLED"} + Caching: {"ENABLED" if config.cache_enabled else "DISABLED"} + Rate Limit: {"ENABLED" if config.rate_limit_enabled else "DISABLED"} Usage with Claude Code: ANTHROPIC_BASE_URL=http://{config.host}:{config.port} claude @@ -119,7 +120,8 @@ Documentation: https://github.com/headroom-sdk/headroom ) parser.add_argument( - "--version", "-V", + "--version", + "-V", action="store_true", help="Show version and exit", ) @@ -138,7 +140,8 @@ Documentation: https://github.com/headroom-sdk/headroom help="Host to bind to (default: 127.0.0.1)", ) proxy_parser.add_argument( - "--port", "-p", + "--port", + "-p", type=int, default=8787, help="Port to bind to (default: 8787)", diff --git a/headroom/client.py b/headroom/client.py index 9581d7e99..1ceac4032 100644 --- a/headroom/client.py +++ b/headroom/client.py @@ -8,8 +8,8 @@ from typing import Any from .cache import ( BaseCacheOptimizer, - CacheOptimizerRegistry, CacheConfig, + CacheOptimizerRegistry, OptimizationContext, SemanticCacheLayer, ) @@ -19,12 +19,6 @@ from .config import ( RequestMetrics, SimulationResult, ) -from .exceptions import ( - ConfigurationError, - ProviderError, - StorageError, - ValidationError, -) from .parser import parse_messages from .providers.base import Provider from .storage import create_storage @@ -386,9 +380,7 @@ class HeadroomClient: tokenizer = self._get_tokenizer(model) # Analyze original messages - blocks, block_breakdown, waste_signals = parse_messages( - messages, tokenizer - ) + blocks, block_breakdown, waste_signals = parse_messages(messages, tokenizer) tokens_before = tokenizer.count_messages(messages) # Compute cache alignment score @@ -410,7 +402,9 @@ class HeadroomClient: # Apply transforms if in optimize mode if mode == HeadroomMode.OPTIMIZE: - output_buffer = headroom_output_buffer_tokens or self._config.rolling_window.output_buffer_tokens + output_buffer = ( + headroom_output_buffer_tokens or self._config.rolling_window.output_buffer_tokens + ) model_limit = self._get_context_limit(model) result = self._pipeline.apply( @@ -443,7 +437,9 @@ class HeadroomClient: cached_response = cache_result.cached_response # Update metrics from cache result - cache_optimizer_used = cache_result.metrics.optimizer_name or self._cache_optimizer.name + cache_optimizer_used = ( + cache_result.metrics.optimizer_name or self._cache_optimizer.name + ) cache_optimizer_strategy = cache_result.metrics.strategy cacheable_tokens = cache_result.metrics.cacheable_tokens breakpoints_inserted = cache_result.metrics.breakpoints_inserted @@ -456,9 +452,7 @@ class HeadroomClient: elif self._cache_optimizer is not None: # Direct cache optimizer (no semantic layer) - cache_result = self._cache_optimizer.optimize( - optimized_messages, cache_context - ) + cache_result = self._cache_optimizer.optimize(optimized_messages, cache_context) cache_optimizer_used = self._cache_optimizer.name cache_optimizer_strategy = self._cache_optimizer.strategy.value cacheable_tokens = cache_result.metrics.cacheable_tokens @@ -625,8 +619,7 @@ class HeadroomClient: ) -> Iterator[Any]: """Wrap stream to pass through chunks and save metrics at end.""" try: - for chunk in stream: - yield chunk + yield from stream finally: # Save metrics when stream completes # Note: output tokens unknown for streams @@ -666,9 +659,7 @@ class HeadroomClient: # Extract response content for caching response_data = self._extract_response_content(response) if response_data: - self._semantic_cache_layer.store_response( - messages, response_data, cache_context - ) + self._semantic_cache_layer.store_response(messages, response_data, cache_context) def _extract_response_content(self, response: Any) -> dict[str, Any] | None: """Extract cacheable content from API response.""" @@ -704,18 +695,18 @@ class HeadroomClient: tokenizer = self._get_tokenizer(model) # Analyze original - blocks, block_breakdown, waste_signals = parse_messages( - messages, tokenizer - ) + blocks, block_breakdown, waste_signals = parse_messages(messages, tokenizer) tokens_before = tokenizer.count_messages(messages) # Compute original cache alignment aligner = CacheAligner(self._config.cache_aligner) cache_alignment_score = aligner.get_alignment_score(messages) - stable_prefix_hash = compute_prefix_hash(messages) + compute_prefix_hash(messages) # Apply transforms - output_buffer = headroom_output_buffer_tokens or self._config.rolling_window.output_buffer_tokens + output_buffer = ( + headroom_output_buffer_tokens or self._config.rolling_window.output_buffer_tokens + ) model_limit = self._get_context_limit(model) result = self._pipeline.simulate( @@ -946,9 +937,7 @@ class HeadroomClient: "config": { "mode": self._default_mode.value, "provider": self._provider.name, - "cache_optimizer": ( - self._cache_optimizer.name if self._cache_optimizer else None - ), + "cache_optimizer": (self._cache_optimizer.name if self._cache_optimizer else None), "semantic_cache": self._semantic_cache_layer is not None, }, "transforms": { diff --git a/headroom/config.py b/headroom/config.py index b6ae6b2c6..3073c9c01 100644 --- a/headroom/config.py +++ b/headroom/config.py @@ -266,7 +266,9 @@ class CCRConfig: # Retrieval marker format # Inserted at end of compressed content to tell LLM how to get more - marker_template: str = "\n[{original_count} items compressed to {compressed_count}. Retrieve more: hash={hash}]" + marker_template: str = ( + "\n[{original_count} items compressed to {compressed_count}. Retrieve more: hash={hash}]" + ) @dataclass diff --git a/headroom/exceptions.py b/headroom/exceptions.py index 844f1627e..223d78ee2 100644 --- a/headroom/exceptions.py +++ b/headroom/exceptions.py @@ -60,6 +60,7 @@ class ConfigurationError(HeadroomError): details={"valid_modes": ["audit", "optimize"]} ) """ + pass @@ -77,6 +78,7 @@ class ProviderError(HeadroomError): details={"provider": "foo", "known_providers": ["openai", "anthropic"]} ) """ + pass @@ -94,6 +96,7 @@ class StorageError(HeadroomError): details={"url": "sqlite:///foo.db", "error": "Permission denied"} ) """ + pass @@ -111,6 +114,7 @@ class CompressionError(HeadroomError): details={"tool_name": "search_api", "content_preview": "..."} ) """ + pass @@ -128,6 +132,7 @@ class TokenizationError(HeadroomError): details={"model": "gpt-99", "fallback_used": True} ) """ + pass @@ -145,6 +150,7 @@ class CacheError(HeadroomError): details={"hash": "abc123", "ttl": 300} ) """ + pass @@ -164,6 +170,7 @@ class ValidationError(HeadroomError): } ) """ + pass @@ -181,4 +188,5 @@ class TransformError(HeadroomError): details={"transform": "smart_crusher", "reason": "..."} ) """ + pass diff --git a/headroom/integrations/langchain.py b/headroom/integrations/langchain.py index 781d1cead..f0691f0b9 100644 --- a/headroom/integrations/langchain.py +++ b/headroom/integrations/langchain.py @@ -142,6 +142,7 @@ class HeadroomChatModel(BaseChatModel): class Config: """Pydantic config for LangChain compatibility.""" + arbitrary_types_allowed = True def __init__( @@ -206,9 +207,7 @@ class HeadroomChatModel(BaseChatModel): """History of optimization metrics.""" return self._metrics_history.copy() - def _convert_messages_to_openai( - self, messages: list[BaseMessage] - ) -> list[dict[str, Any]]: + def _convert_messages_to_openai(self, messages: list[BaseMessage]) -> list[dict[str, Any]]: """Convert LangChain messages to OpenAI format for Headroom.""" result = [] for msg in messages: @@ -232,22 +231,24 @@ class HeadroomChatModel(BaseChatModel): ] result.append(entry) elif isinstance(msg, ToolMessage): - result.append({ - "role": "tool", - "tool_call_id": msg.tool_call_id, - "content": msg.content, - }) + result.append( + { + "role": "tool", + "tool_call_id": msg.tool_call_id, + "content": msg.content, + } + ) else: # Generic fallback - result.append({ - "role": getattr(msg, "type", "user"), - "content": msg.content, - }) + result.append( + { + "role": getattr(msg, "type", "user"), + "content": msg.content, + } + ) return result - def _convert_messages_from_openai( - self, messages: list[dict[str, Any]] - ) -> list[BaseMessage]: + def _convert_messages_from_openai(self, messages: list[dict[str, Any]]) -> list[BaseMessage]: """Convert OpenAI format messages back to LangChain format.""" result = [] for msg in messages: @@ -262,17 +263,21 @@ class HeadroomChatModel(BaseChatModel): tool_calls = [] if "tool_calls" in msg: for tc in msg["tool_calls"]: - tool_calls.append({ - "id": tc["id"], - "name": tc["function"]["name"], - "args": json.loads(tc["function"]["arguments"]), - }) + tool_calls.append( + { + "id": tc["id"], + "name": tc["function"]["name"], + "args": json.loads(tc["function"]["arguments"]), + } + ) result.append(AIMessage(content=content, tool_calls=tool_calls)) elif role == "tool": - result.append(ToolMessage( - content=content, - tool_call_id=msg.get("tool_call_id", ""), - )) + result.append( + ToolMessage( + content=content, + tool_call_id=msg.get("tool_call_id", ""), + ) + ) return result def _optimize_messages( @@ -308,7 +313,8 @@ class HeadroomChatModel(BaseChatModel): tokens_saved=result.tokens_before - result.tokens_after, savings_percent=( (result.tokens_before - result.tokens_after) / result.tokens_before * 100 - if result.tokens_before > 0 else 0 + if result.tokens_before > 0 + else 0 ), transforms_applied=result.transforms_applied, model=model, @@ -400,9 +406,8 @@ class HeadroomChatModel(BaseChatModel): return { "total_requests": len(self._metrics_history), "total_tokens_saved": self._total_tokens_saved, - "average_savings_percent": sum( - m.savings_percent for m in self._metrics_history - ) / len(self._metrics_history), + "average_savings_percent": sum(m.savings_percent for m in self._metrics_history) + / len(self._metrics_history), "total_tokens_before": sum(m.tokens_before for m in self._metrics_history), "total_tokens_after": sum(m.tokens_after for m in self._metrics_history), } @@ -530,7 +535,7 @@ class HeadroomCallbackHandler(BaseCallbackHandler): if self.log_level in ("DEBUG", "INFO"): logger.log( logging.DEBUG if self.log_level == "DEBUG" else logging.INFO, - f"Chat model request: ~{estimated_tokens} input tokens" + f"Chat model request: ~{estimated_tokens} input tokens", ) def on_llm_end(self, response: Any, **kwargs) -> None: @@ -565,7 +570,7 @@ class HeadroomCallbackHandler(BaseCallbackHandler): duration = f"{self._current_request['duration_ms']:.0f}ms" logger.log( logging.DEBUG if self.log_level == "DEBUG" else logging.INFO, - f"LLM request completed: {tokens_info} in {duration}" + f"LLM request completed: {tokens_info} in {duration}", ) self._current_request = None @@ -602,7 +607,8 @@ class HeadroomCallbackHandler(BaseCallbackHandler): "average_tokens": total_tokens / len(successful) if successful else 0, "average_duration_ms": ( sum(r.get("duration_ms", 0) for r in successful) / len(successful) - if successful else 0 + if successful + else 0 ), "errors": len(self._requests) - len(successful), "alerts": len(self._alerts), @@ -670,11 +676,13 @@ class HeadroomRunnable: def __or__(self, other): """Support pipe operator for LCEL composition.""" from langchain_core.runnables import RunnableSequence + return RunnableSequence(first=self.as_runnable(), last=other) def __ror__(self, other): """Support reverse pipe operator.""" from langchain_core.runnables import RunnableSequence + return RunnableSequence(first=other, last=self.as_runnable()) def as_runnable(self): @@ -704,16 +712,20 @@ class HeadroomRunnable: elif isinstance(msg, AIMessage): openai_messages.append({"role": "assistant", "content": msg.content}) elif isinstance(msg, ToolMessage): - openai_messages.append({ - "role": "tool", - "tool_call_id": msg.tool_call_id, - "content": msg.content, - }) + openai_messages.append( + { + "role": "tool", + "tool_call_id": msg.tool_call_id, + "content": msg.content, + } + ) elif hasattr(msg, "type") and hasattr(msg, "content"): - openai_messages.append({ - "role": msg.type, - "content": msg.content, - }) + openai_messages.append( + { + "role": msg.type, + "content": msg.content, + } + ) # Get model context limit model = "gpt-4o" # Default model for estimation @@ -735,7 +747,8 @@ class HeadroomRunnable: tokens_saved=result.tokens_before - result.tokens_after, savings_percent=( (result.tokens_before - result.tokens_after) / result.tokens_before * 100 - if result.tokens_before > 0 else 0 + if result.tokens_before > 0 + else 0 ), transforms_applied=result.transforms_applied, model="gpt-4o", @@ -755,10 +768,12 @@ class HeadroomRunnable: elif role == "assistant": output_messages.append(AIMessage(content=content)) elif role == "tool": - output_messages.append(ToolMessage( - content=content, - tool_call_id=msg.get("tool_call_id", ""), - )) + output_messages.append( + ToolMessage( + content=content, + tool_call_id=msg.get("tool_call_id", ""), + ) + ) return output_messages @@ -823,11 +838,13 @@ def optimize_messages( ] openai_messages.append(entry) elif isinstance(msg, ToolMessage): - openai_messages.append({ - "role": "tool", - "tool_call_id": msg.tool_call_id, - "content": msg.content, - }) + openai_messages.append( + { + "role": "tool", + "tool_call_id": msg.tool_call_id, + "content": msg.content, + } + ) # Get model context limit model_limit = provider.get_context_limit(model) @@ -853,17 +870,21 @@ def optimize_messages( tool_calls = [] if "tool_calls" in msg: for tc in msg["tool_calls"]: - tool_calls.append({ - "id": tc["id"], - "name": tc["function"]["name"], - "args": json.loads(tc["function"]["arguments"]), - }) + tool_calls.append( + { + "id": tc["id"], + "name": tc["function"]["name"], + "args": json.loads(tc["function"]["arguments"]), + } + ) output_messages.append(AIMessage(content=content, tool_calls=tool_calls)) elif role == "tool": - output_messages.append(ToolMessage( - content=content, - tool_call_id=msg.get("tool_call_id", ""), - )) + output_messages.append( + ToolMessage( + content=content, + tool_call_id=msg.get("tool_call_id", ""), + ) + ) metrics = { "tokens_before": result.tokens_before, @@ -871,7 +892,8 @@ def optimize_messages( "tokens_saved": result.tokens_before - result.tokens_after, "savings_percent": ( (result.tokens_before - result.tokens_after) / result.tokens_before * 100 - if result.tokens_before > 0 else 0 + if result.tokens_before > 0 + else 0 ), "transforms_applied": result.transforms_applied, } diff --git a/headroom/integrations/mcp.py b/headroom/integrations/mcp.py index aa102b8a8..07997a9d7 100644 --- a/headroom/integrations/mcp.py +++ b/headroom/integrations/mcp.py @@ -222,7 +222,7 @@ class HeadroomMCPCompressor: # Try to parse as JSON try: - data = json.loads(content) + json.loads(content) except json.JSONDecodeError: # Not JSON, return as-is return MCPCompressionResult( @@ -260,7 +260,12 @@ class HeadroomMCPCompressor: { "role": "assistant", "content": None, - "tool_calls": [{"id": "call_1", "function": {"name": tool_name, "arguments": json.dumps(tool_args or {})}}] + "tool_calls": [ + { + "id": "call_1", + "function": {"name": tool_name, "arguments": json.dumps(tool_args or {})}, + } + ], }, {"role": "tool", "content": content, "tool_call_id": "call_1"}, ] @@ -287,7 +292,7 @@ class HeadroomMCPCompressor: compressed_content = result.messages[-1]["content"] # Remove any Headroom markers for clean output - compressed_content = re.sub(r'\n]+>', '', compressed_content) + compressed_content = re.sub(r"\n]+>", "", compressed_content) # Count items and errors try: @@ -295,13 +300,13 @@ class HeadroomMCPCompressor: compressed_data = json.loads(compressed_content) # Find the array in original - for key, value in original_data.items(): + for _key, value in original_data.items(): if isinstance(value, list): items_before = len(value) break # Find the array in compressed - for key, value in compressed_data.items(): + for _key, value in compressed_data.items(): if isinstance(value, list): items_after = len(value) # Count errors preserved @@ -521,7 +526,7 @@ def create_headroom_mcp_proxy( ``` """ return { - "upstream_servers": {name: server for name, server in upstream_servers}, + "upstream_servers": dict(upstream_servers), "compressor": HeadroomMCPCompressor(config=config), "config": config or HeadroomConfig(), } diff --git a/headroom/models/registry.py b/headroom/models/registry.py index 47340c674..f903de855 100644 --- a/headroom/models/registry.py +++ b/headroom/models/registry.py @@ -649,7 +649,7 @@ class ModelRegistry: Returns: List of provider names. """ - return list(set(info.provider for info in _MODELS.values())) + return list({info.provider for info in _MODELS.values()}) @classmethod def get_context_limit(cls, model: str, default: int = 128000) -> int: diff --git a/headroom/pricing/registry.py b/headroom/pricing/registry.py index e1a0eb940..3f1a92319 100644 --- a/headroom/pricing/registry.py +++ b/headroom/pricing/registry.py @@ -10,6 +10,7 @@ class ModelPricing: All prices are in USD per 1 million tokens. """ + model: str provider: str input_per_1m: float @@ -24,6 +25,7 @@ class ModelPricing: @dataclass class CostEstimate: """Result of a cost estimation calculation.""" + cost_usd: float breakdown: dict = field(default_factory=dict) pricing_date: date | None = None diff --git a/headroom/providers/anthropic.py b/headroom/providers/anthropic.py index 789da2d9f..22a9c9778 100644 --- a/headroom/providers/anthropic.py +++ b/headroom/providers/anthropic.py @@ -87,13 +87,14 @@ class AnthropicTokenCounter(TokenCounter): "For accurate counting, pass an Anthropic client: " "AnthropicProvider(client=Anthropic())", UserWarning, - stacklevel=4 + stacklevel=4, ) _FALLBACK_WARNING_SHOWN = True # Load tiktoken as fallback try: import tiktoken + self._encoding = tiktoken.get_encoding("cl100k_base") except ImportError: if not self._use_api: @@ -101,7 +102,7 @@ class AnthropicTokenCounter(TokenCounter): "tiktoken not installed - token counting will be very approximate. " "Install tiktoken or provide an Anthropic client.", UserWarning, - stacklevel=4 + stacklevel=4, ) def count_text(self, text: str) -> int: @@ -184,11 +185,13 @@ class AnthropicTokenCounter(TokenCounter): # Tool results in OpenAI format return { "role": "user", - "content": [{ - "type": "tool_result", - "tool_use_id": message.get("tool_call_id", ""), - "content": message.get("content", ""), - }] + "content": [ + { + "type": "tool_result", + "tool_use_id": message.get("tool_call_id", ""), + "content": message.get("content", ""), + } + ], } return {"role": role, "content": message.get("content", "")} @@ -232,9 +235,7 @@ class AnthropicTokenCounter(TokenCounter): except Exception as e: # Fall back to estimation on API error warnings.warn( - f"Token Count API failed ({e}), using estimation", - UserWarning, - stacklevel=3 + f"Token Count API failed ({e}), using estimation", UserWarning, stacklevel=3 ) return self._count_messages_estimated(messages) @@ -318,8 +319,7 @@ class AnthropicProvider(Provider): return True # Check prefix matches return any( - model.startswith(prefix) - for prefix in ["claude-3", "claude-2", "claude-instant"] + model.startswith(prefix) for prefix in ["claude-3", "claude-2", "claude-instant"] ) def estimate_cost( diff --git a/headroom/providers/cohere.py b/headroom/providers/cohere.py index ed736a20f..c4950acd6 100644 --- a/headroom/providers/cohere.py +++ b/headroom/providers/cohere.py @@ -108,7 +108,7 @@ class CohereTokenCounter: "For accurate counting, pass a Cohere client: " "CohereProvider(client=cohere.ClientV2())", UserWarning, - stacklevel=4 + stacklevel=4, ) _FALLBACK_WARNING_SHOWN = True diff --git a/headroom/providers/google.py b/headroom/providers/google.py index 1b7b0b9d0..708bcea38 100644 --- a/headroom/providers/google.py +++ b/headroom/providers/google.py @@ -107,7 +107,7 @@ class GeminiTokenCounter: "For accurate counting, pass google.generativeai: " "GoogleProvider(client=genai)", UserWarning, - stacklevel=4 + stacklevel=4, ) _FALLBACK_WARNING_SHOWN = True diff --git a/headroom/providers/litellm.py b/headroom/providers/litellm.py index d1e72814e..9a34acd3d 100644 --- a/headroom/providers/litellm.py +++ b/headroom/providers/litellm.py @@ -70,8 +70,7 @@ class LiteLLMTokenCounter: """ if not LITELLM_AVAILABLE: raise RuntimeError( - "LiteLLM is required for LiteLLMProvider. " - "Install with: pip install litellm" + "LiteLLM is required for LiteLLMProvider. Install with: pip install litellm" ) self.model = model # Fallback estimator for when litellm counting fails @@ -163,8 +162,7 @@ class LiteLLMProvider(Provider): """Initialize LiteLLM provider.""" if not LITELLM_AVAILABLE: raise RuntimeError( - "LiteLLM is required for LiteLLMProvider. " - "Install with: pip install litellm" + "LiteLLM is required for LiteLLMProvider. Install with: pip install litellm" ) @property diff --git a/headroom/providers/openai.py b/headroom/providers/openai.py index 963b8fdc4..1b1f92976 100644 --- a/headroom/providers/openai.py +++ b/headroom/providers/openai.py @@ -100,8 +100,7 @@ def _get_encoding(encoding_name: str) -> Any: """Get tiktoken encoding, cached.""" if not TIKTOKEN_AVAILABLE: raise RuntimeError( - "tiktoken is required for OpenAI provider. " - "Install with: pip install tiktoken" + "tiktoken is required for OpenAI provider. Install with: pip install tiktoken" ) return tiktoken.get_encoding(encoding_name) @@ -118,8 +117,7 @@ def _get_encoding_name_for_model(model: str) -> str: return encoding raise ValueError( - f"Unknown OpenAI model: {model}. " - f"Supported models: {list(_MODEL_ENCODINGS.keys())}" + f"Unknown OpenAI model: {model}. Supported models: {list(_MODEL_ENCODINGS.keys())}" ) diff --git a/headroom/proxy/server.py b/headroom/proxy/server.py index b0137d04e..daf83ce0c 100644 --- a/headroom/proxy/server.py +++ b/headroom/proxy/server.py @@ -41,9 +41,10 @@ import httpx try: import uvicorn - from fastapi import FastAPI, Header, HTTPException, Request, Response + from fastapi import FastAPI, HTTPException, Request, Response from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import PlainTextResponse, StreamingResponse + FASTAPI_AVAILABLE = True except ImportError: FASTAPI_AVAILABLE = False @@ -53,16 +54,15 @@ sys.path.insert(0, str(Path(__file__).parent.parent.parent)) from headroom.cache.compression_feedback import get_compression_feedback from headroom.cache.compression_store import get_compression_store -from headroom.telemetry import get_telemetry_collector -from headroom.ccr import CCRToolInjector, CCR_TOOL_NAME, parse_tool_call -from headroom.config import CacheAlignerConfig, CCRConfig, RollingWindowConfig, SmartCrusherConfig +from headroom.ccr import CCR_TOOL_NAME, CCRToolInjector, parse_tool_call +from headroom.config import CacheAlignerConfig, RollingWindowConfig, SmartCrusherConfig from headroom.providers import AnthropicProvider, OpenAIProvider +from headroom.telemetry import get_telemetry_collector from headroom.tokenizers import get_tokenizer from headroom.transforms import CacheAligner, RollingWindow, SmartCrusher, TransformPipeline logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) logger = logging.getLogger("headroom.proxy") @@ -71,9 +71,11 @@ logger = logging.getLogger("headroom.proxy") # Data Models # ============================================================================= + @dataclass class RequestLog: """Complete log of a single request.""" + request_id: str timestamp: str provider: str @@ -108,6 +110,7 @@ class RequestLog: @dataclass class CacheEntry: """Cached response entry.""" + response_body: bytes response_headers: dict[str, str] created_at: datetime @@ -119,6 +122,7 @@ class CacheEntry: @dataclass class RateLimitState: """Token bucket rate limiter state.""" + tokens: float last_update: float @@ -126,6 +130,7 @@ class RateLimitState: @dataclass class ProxyConfig: """Proxy configuration.""" + # Server host: str = "127.0.0.1" port: int = 8787 @@ -179,6 +184,7 @@ class ProxyConfig: # Caching # ============================================================================= + class SemanticCache: """Simple semantic cache based on message content hash.""" @@ -191,10 +197,13 @@ class SemanticCache: def _compute_key(self, messages: list[dict], model: str) -> str: """Compute cache key from messages and model.""" # Normalize messages for consistent hashing - normalized = json.dumps({ - "model": model, - "messages": messages, - }, sort_keys=True) + normalized = json.dumps( + { + "model": model, + "messages": messages, + }, + sort_keys=True, + ) return hashlib.sha256(normalized.encode()).hexdigest()[:32] def get(self, messages: list[dict], model: str) -> CacheEntry | None: @@ -260,6 +269,7 @@ class SemanticCache: # Rate Limiting # ============================================================================= + class TokenBucketRateLimiter: """Token bucket rate limiter for requests and tokens.""" @@ -325,6 +335,7 @@ class TokenBucketRateLimiter: # Cost Tracking # ============================================================================= + class CostTracker: """Track costs and enforce budgets.""" @@ -378,9 +389,9 @@ class CostTracker: regular_input = input_tokens - cached_tokens cost = ( - (regular_input / 1_000_000) * input_price + - (cached_tokens / 1_000_000) * cached_price + - (output_tokens / 1_000_000) * output_price + (regular_input / 1_000_000) * input_price + + (cached_tokens / 1_000_000) * cached_price + + (output_tokens / 1_000_000) * output_price ) return cost @@ -409,7 +420,7 @@ class CostTracker: def check_budget(self) -> tuple[bool, float]: """Check if within budget. Returns (allowed, remaining).""" if self.budget_limit_usd is None: - return True, float('inf') + return True, float("inf") period_cost = self.get_period_cost() remaining = self.budget_limit_usd - period_cost @@ -423,7 +434,9 @@ class CostTracker: "period_cost_usd": round(self.get_period_cost(), 4), "budget_limit_usd": self.budget_limit_usd, "budget_period": self.budget_period, - "budget_remaining_usd": round(self.check_budget()[1], 4) if self.budget_limit_usd else None, + "budget_remaining_usd": round(self.check_budget()[1], 4) + if self.budget_limit_usd + else None, } @@ -431,6 +444,7 @@ class CostTracker: # Prometheus Metrics # ============================================================================= + class PrometheusMetrics: """Prometheus-compatible metrics.""" @@ -533,20 +547,24 @@ class PrometheusMetrics: ] # Per-provider metrics - lines.extend([ - "", - "# HELP headroom_requests_by_provider Requests by provider", - "# TYPE headroom_requests_by_provider counter", - ]) + lines.extend( + [ + "", + "# HELP headroom_requests_by_provider Requests by provider", + "# TYPE headroom_requests_by_provider counter", + ] + ) for provider, count in self.requests_by_provider.items(): lines.append(f'headroom_requests_by_provider{{provider="{provider}"}} {count}') # Per-model metrics - lines.extend([ - "", - "# HELP headroom_requests_by_model Requests by model", - "# TYPE headroom_requests_by_model counter", - ]) + lines.extend( + [ + "", + "# HELP headroom_requests_by_model Requests by model", + "# TYPE headroom_requests_by_model counter", + ] + ) for model, count in self.requests_by_model.items(): lines.append(f'headroom_requests_by_model{{model="{model}"}} {count}') @@ -557,6 +575,7 @@ class PrometheusMetrics: # Request Logger # ============================================================================= + class RequestLogger: """Log requests to JSONL file.""" @@ -584,8 +603,11 @@ class RequestLogger: """Get recent log entries.""" entries = self._logs[-n:] return [ - {k: v for k, v in asdict(e).items() - if k not in ("request_messages", "response_content")} + { + k: v + for k, v in asdict(e).items() + if k not in ("request_messages", "response_content") + } for e in entries ] @@ -601,6 +623,7 @@ class RequestLogger: # Main Proxy # ============================================================================= + class HeadroomProxy: """Production-ready Headroom optimization proxy.""" @@ -617,16 +640,20 @@ class HeadroomProxy: # Initialize transforms transforms = [ CacheAligner(CacheAlignerConfig(enabled=True)), - SmartCrusher(SmartCrusherConfig( - enabled=True, - min_tokens_to_crush=config.min_tokens_to_crush, - max_items_after_crush=config.max_items_after_crush, - )), - RollingWindow(RollingWindowConfig( - enabled=True, - keep_system=True, - keep_last_turns=config.keep_last_turns, - )), + SmartCrusher( + SmartCrusherConfig( + enabled=True, + min_tokens_to_crush=config.min_tokens_to_crush, + max_items_after_crush=config.max_items_after_crush, + ) + ), + RollingWindow( + RollingWindowConfig( + enabled=True, + keep_system=True, + keep_last_turns=config.keep_last_turns, + ) + ), ] self.anthropic_pipeline = TransformPipeline( @@ -639,27 +666,43 @@ class HeadroomProxy: ) # Initialize components - self.cache = SemanticCache( - max_entries=config.cache_max_entries, - ttl_seconds=config.cache_ttl_seconds, - ) if config.cache_enabled else None + self.cache = ( + SemanticCache( + max_entries=config.cache_max_entries, + ttl_seconds=config.cache_ttl_seconds, + ) + if config.cache_enabled + else None + ) - self.rate_limiter = TokenBucketRateLimiter( - requests_per_minute=config.rate_limit_requests_per_minute, - tokens_per_minute=config.rate_limit_tokens_per_minute, - ) if config.rate_limit_enabled else None + self.rate_limiter = ( + TokenBucketRateLimiter( + requests_per_minute=config.rate_limit_requests_per_minute, + tokens_per_minute=config.rate_limit_tokens_per_minute, + ) + if config.rate_limit_enabled + else None + ) - self.cost_tracker = CostTracker( - budget_limit_usd=config.budget_limit_usd, - budget_period=config.budget_period, - ) if config.cost_tracking_enabled else None + self.cost_tracker = ( + CostTracker( + budget_limit_usd=config.budget_limit_usd, + budget_period=config.budget_period, + ) + if config.cost_tracking_enabled + else None + ) self.metrics = PrometheusMetrics() - self.logger = RequestLogger( - log_file=config.log_file, - log_full_messages=config.log_full_messages, - ) if config.log_requests else None + self.logger = ( + RequestLogger( + log_file=config.log_file, + log_full_messages=config.log_full_messages, + ) + if config.log_requests + else None + ) # HTTP client self.http_client: httpx.AsyncClient | None = None @@ -716,7 +759,9 @@ class HeadroomProxy: logger.info(f"Output tokens: {m.tokens_output_total:,}") logger.info(f"Tokens saved: {m.tokens_saved_total:,}") if m.tokens_input_total > 0: - savings_pct = (m.tokens_saved_total / (m.tokens_input_total + m.tokens_saved_total)) * 100 + savings_pct = ( + m.tokens_saved_total / (m.tokens_input_total + m.tokens_saved_total) + ) * 100 logger.info(f"Token savings: {savings_pct:.1f}%") logger.info(f"Total cost: ${m.cost_total_usd:.4f}") logger.info(f"Total savings: ${m.savings_total_usd:.4f}") @@ -780,7 +825,7 @@ class HeadroomProxy: # Exponential backoff with jitter delay = min( - self.config.retry_base_delay_ms * (2 ** attempt), + self.config.retry_base_delay_ms * (2**attempt), self.config.retry_max_delay_ms, ) delay_with_jitter = delay * (0.5 + random.random()) @@ -807,7 +852,7 @@ class HeadroomProxy: stream = body.get("stream", False) # Extract headers and tags - headers = {k: v for k, v in request.headers.items()} + headers = dict(request.headers.items()) headers.pop("host", None) headers.pop("content-length", None) tags = self._extract_tags(headers) @@ -859,10 +904,7 @@ class HeadroomProxy: # Count original tokens tokenizer = get_tokenizer(model) - original_tokens = sum( - tokenizer.count_text(str(m.get("content", ""))) - for m in messages - ) + original_tokens = sum(tokenizer.count_text(str(m.get("content", ""))) for m in messages) # Apply optimization transforms_applied = [] @@ -882,8 +924,7 @@ class HeadroomProxy: optimized_messages = result.messages transforms_applied = result.transforms_applied optimized_tokens = sum( - tokenizer.count_text(str(m.get("content", ""))) - for m in optimized_messages + tokenizer.count_text(str(m.get("content", ""))) for m in optimized_messages ) except Exception as e: logger.warning(f"Optimization failed: {e}") @@ -900,7 +941,9 @@ class HeadroomProxy: inject_tool=self.config.ccr_inject_tool, inject_system_instructions=self.config.ccr_inject_system_instructions, ) - optimized_messages, tools, was_injected = injector.process_request(optimized_messages, tools) + optimized_messages, tools, was_injected = injector.process_request( + optimized_messages, tools + ) if injector.has_compressed_content: if was_injected: @@ -923,9 +966,18 @@ class HeadroomProxy: try: if stream: return await self._stream_response( - url, headers, body, "anthropic", model, request_id, - original_tokens, optimized_tokens, tokens_saved, - transforms_applied, tags, optimization_latency, + url, + headers, + body, + "anthropic", + model, + request_id, + original_tokens, + optimized_tokens, + tokens_saved, + transforms_applied, + tags, + optimization_latency, ) else: response = await self._retry_request("POST", url, headers, body) @@ -937,7 +989,7 @@ class HeadroomProxy: resp_json = response.json() usage = resp_json.get("usage", {}) output_tokens = usage.get("output_tokens", 0) - except: + except Exception: pass # Calculate cost @@ -958,7 +1010,8 @@ class HeadroomProxy: # Cache response if self.cache and response.status_code == 200: self.cache.set( - messages, model, + messages, + model, response.content, dict(response.headers), tokens_saved=tokens_saved, @@ -978,32 +1031,37 @@ class HeadroomProxy: # Log request if self.logger: - self.logger.log(RequestLog( - request_id=request_id, - timestamp=datetime.now().isoformat(), - provider="anthropic", - model=model, - input_tokens_original=original_tokens, - input_tokens_optimized=optimized_tokens, - output_tokens=output_tokens, - tokens_saved=tokens_saved, - savings_percent=(tokens_saved / original_tokens * 100) if original_tokens > 0 else 0, - estimated_cost_usd=cost_usd, - estimated_savings_usd=savings_usd, - optimization_latency_ms=optimization_latency, - total_latency_ms=total_latency, - tags=tags, - cache_hit=cache_hit, - transforms_applied=transforms_applied, - request_messages=messages if self.config.log_full_messages else None, - )) + self.logger.log( + RequestLog( + request_id=request_id, + timestamp=datetime.now().isoformat(), + provider="anthropic", + model=model, + input_tokens_original=original_tokens, + input_tokens_optimized=optimized_tokens, + output_tokens=output_tokens, + tokens_saved=tokens_saved, + savings_percent=(tokens_saved / original_tokens * 100) + if original_tokens > 0 + else 0, + estimated_cost_usd=cost_usd, + estimated_savings_usd=savings_usd, + optimization_latency_ms=optimization_latency, + total_latency_ms=total_latency, + tags=tags, + cache_hit=cache_hit, + transforms_applied=transforms_applied, + request_messages=messages if self.config.log_full_messages else None, + ) + ) # Log to console if tokens_saved > 0: logger.info( f"[{request_id}] {model}: {original_tokens:,} → {optimized_tokens:,} " - f"(saved {tokens_saved:,} tokens, ${savings_usd:.4f})" if savings_usd else - f"[{request_id}] {model}: {original_tokens:,} → {optimized_tokens:,} " + f"(saved {tokens_saved:,} tokens, ${savings_usd:.4f})" + if savings_usd + else f"[{request_id}] {model}: {original_tokens:,} → {optimized_tokens:,} " f"(saved {tokens_saved:,} tokens)" ) @@ -1023,7 +1081,7 @@ class HeadroomProxy: # Convert to OpenAI format and retry # (simplified - would need message format conversion) - raise HTTPException(status_code=502, detail=str(e)) + raise HTTPException(status_code=502, detail=str(e)) from e async def _stream_response( self, @@ -1046,7 +1104,9 @@ class HeadroomProxy: async def generate(): output_chunks = [] try: - async with self.http_client.stream("POST", url, json=body, headers=headers) as response: + async with self.http_client.stream( + "POST", url, json=body, headers=headers + ) as response: async for chunk in response.aiter_bytes(): output_chunks.append(chunk) yield chunk @@ -1090,7 +1150,7 @@ class HeadroomProxy: messages = body.get("messages", []) stream = body.get("stream", False) - headers = {k: v for k, v in request.headers.items()} + headers = dict(request.headers.items()) headers.pop("host", None) headers.pop("content-length", None) tags = self._extract_tags(headers) @@ -1107,14 +1167,14 @@ class HeadroomProxy: ) # Check cache - cache_hit = False if self.cache and not stream: cached = self.cache.get(messages, model) if cached: - cache_hit = True self.metrics.record_request( - provider="openai", model=model, - input_tokens=0, output_tokens=0, + provider="openai", + model=model, + input_tokens=0, + output_tokens=0, tokens_saved=cached.tokens_saved_per_hit, latency_ms=(time.time() - start_time) * 1000, cached=True, @@ -1123,10 +1183,7 @@ class HeadroomProxy: # Token counting tokenizer = get_tokenizer(model) - original_tokens = sum( - tokenizer.count_text(str(m.get("content", ""))) - for m in messages - ) + original_tokens = sum(tokenizer.count_text(str(m.get("content", ""))) for m in messages) # Optimization transforms_applied = [] @@ -1145,8 +1202,7 @@ class HeadroomProxy: optimized_messages = result.messages transforms_applied = result.transforms_applied optimized_tokens = sum( - tokenizer.count_text(str(m.get("content", ""))) - for m in optimized_messages + tokenizer.count_text(str(m.get("content", ""))) for m in optimized_messages ) except Exception as e: logger.warning(f"Optimization failed: {e}") @@ -1162,7 +1218,9 @@ class HeadroomProxy: inject_tool=self.config.ccr_inject_tool, inject_system_instructions=self.config.ccr_inject_system_instructions, ) - optimized_messages, tools, was_injected = injector.process_request(optimized_messages, tools) + optimized_messages, tools, was_injected = injector.process_request( + optimized_messages, tools + ) if injector.has_compressed_content: if was_injected: @@ -1182,9 +1240,18 @@ class HeadroomProxy: try: if stream: return await self._stream_response( - url, headers, body, "openai", model, request_id, - original_tokens, optimized_tokens, tokens_saved, - transforms_applied, tags, optimization_latency, + url, + headers, + body, + "openai", + model, + request_id, + original_tokens, + optimized_tokens, + tokens_saved, + transforms_applied, + tags, + optimization_latency, ) else: response = await self._retry_request("POST", url, headers, body) @@ -1195,14 +1262,18 @@ class HeadroomProxy: resp_json = response.json() usage = resp_json.get("usage", {}) output_tokens = usage.get("completion_tokens", 0) - except: + except Exception: pass # Cost tracking cost_usd = savings_usd = None if self.cost_tracker: - cost_usd = self.cost_tracker.estimate_cost(model, optimized_tokens, output_tokens) - original_cost = self.cost_tracker.estimate_cost(model, original_tokens, output_tokens) + cost_usd = self.cost_tracker.estimate_cost( + model, optimized_tokens, output_tokens + ) + original_cost = self.cost_tracker.estimate_cost( + model, original_tokens, output_tokens + ) if cost_usd and original_cost: savings_usd = original_cost - cost_usd self.cost_tracker.record_cost(cost_usd) @@ -1210,14 +1281,20 @@ class HeadroomProxy: # Cache if self.cache and response.status_code == 200: - self.cache.set(messages, model, response.content, dict(response.headers), tokens_saved) + self.cache.set( + messages, model, response.content, dict(response.headers), tokens_saved + ) # Metrics self.metrics.record_request( - provider="openai", model=model, - input_tokens=optimized_tokens, output_tokens=output_tokens, - tokens_saved=tokens_saved, latency_ms=total_latency, - cost_usd=cost_usd or 0, savings_usd=savings_usd or 0, + provider="openai", + model=model, + input_tokens=optimized_tokens, + output_tokens=output_tokens, + tokens_saved=tokens_saved, + latency_ms=total_latency, + cost_usd=cost_usd or 0, + savings_usd=savings_usd or 0, ) if tokens_saved > 0: @@ -1233,14 +1310,14 @@ class HeadroomProxy: ) except Exception as e: self.metrics.record_failed() - raise HTTPException(status_code=502, detail=str(e)) + raise HTTPException(status_code=502, detail=str(e)) from e async def handle_passthrough(self, request: Request, base_url: str) -> Response: """Pass through request unchanged.""" path = request.url.path url = f"{base_url}{path}" - headers = {k: v for k, v in request.headers.items()} + headers = dict(request.headers.items()) headers.pop("host", None) body = await request.body() @@ -1263,6 +1340,7 @@ class HeadroomProxy: # FastAPI App # ============================================================================= + def create_app(config: ProxyConfig | None = None) -> FastAPI: """Create FastAPI application.""" if not FASTAPI_AVAILABLE: @@ -1305,7 +1383,7 @@ def create_app(config: ProxyConfig | None = None) -> FastAPI: "optimize": config.optimize, "cache": config.cache_enabled, "rate_limit": config.rate_limit_enabled, - } + }, } @app.get("/stats") @@ -1324,7 +1402,9 @@ def create_app(config: ProxyConfig | None = None) -> FastAPI: "saved": m.tokens_saved_total, "savings_percent": round( (m.tokens_saved_total / (m.tokens_input_total + m.tokens_saved_total) * 100) - if m.tokens_input_total > 0 else 0, 2 + if m.tokens_input_total > 0 + else 0, + 2, ), }, "cost": proxy.cost_tracker.stats() if proxy.cost_tracker else None, @@ -1398,8 +1478,7 @@ def create_app(config: ProxyConfig | None = None) -> FastAPI: "retrieval_count": entry.retrieval_count, } raise HTTPException( - status_code=404, - detail="Entry not found or expired (TTL: 5 minutes)" + status_code=404, detail="Entry not found or expired (TTL: 5 minutes)" ) @app.get("/v1/retrieve/stats") @@ -1443,7 +1522,9 @@ def create_app(config: ProxyConfig | None = None) -> FastAPI: "hints_example": { tool_name: { "hints": { - "max_items": hints.max_items if (hints := feedback.get_compression_hints(tool_name)) else 15, + "max_items": hints.max_items + if (hints := feedback.get_compression_hints(tool_name)) + else 15, "suggested_items": hints.suggested_items if hints else None, "skip_compression": hints.skip_compression if hints else False, "preserve_fields": hints.preserve_fields if hints else [], @@ -1484,7 +1565,9 @@ def create_app(config: ProxyConfig | None = None) -> FastAPI: "search_rate": patterns.search_rate if patterns else 0.0, "common_queries": list(patterns.common_queries.keys())[:10] if patterns else [], "queried_fields": list(patterns.queried_fields.keys())[:10] if patterns else [], - } if patterns else None, + } + if patterns + else None, } # Telemetry endpoints (Data Flywheel) @@ -1554,10 +1637,7 @@ def create_app(config: ProxyConfig | None = None) -> FastAPI: all_stats = telemetry.get_all_tool_stats() return { "tool_count": len(all_stats), - "tools": { - sig_hash: stats.to_dict() - for sig_hash, stats in all_stats.items() - }, + "tools": {sig_hash: stats.to_dict() for sig_hash, stats in all_stats.items()}, } @app.get("/v1/telemetry/tools/{signature_hash}") @@ -1572,8 +1652,7 @@ def create_app(config: ProxyConfig | None = None) -> FastAPI: if stats is None: raise HTTPException( - status_code=404, - detail=f"No telemetry found for signature: {signature_hash}" + status_code=404, detail=f"No telemetry found for signature: {signature_hash}" ) return { @@ -1607,10 +1686,7 @@ def create_app(config: ProxyConfig | None = None) -> FastAPI: "tool_name": entry.tool_name, "retrieval_count": entry.retrieval_count, } - raise HTTPException( - status_code=404, - detail="Entry not found or expired" - ) + raise HTTPException(status_code=404, detail="Entry not found or expired") # CCR Tool Call Handler - for agent frameworks to call when LLM uses headroom_retrieve @app.post("/v1/retrieve/tool_call") @@ -1659,8 +1735,7 @@ def create_app(config: ProxyConfig | None = None) -> FastAPI: if hash_key is None: raise HTTPException( - status_code=400, - detail=f"Invalid tool call or not a {CCR_TOOL_NAME} call" + status_code=400, detail=f"Invalid tool call or not a {CCR_TOOL_NAME} call" ) # Perform retrieval @@ -1760,11 +1835,11 @@ def run_server(config: ProxyConfig | None = None): ║ Listening: http://{config.host}:{config.port:<5} ║ ╠══════════════════════════════════════════════════════════════════════╣ ║ FEATURES: ║ -║ Optimization: {'ENABLED ' if config.optimize else 'DISABLED'} ║ -║ Caching: {'ENABLED ' if config.cache_enabled else 'DISABLED'} (TTL: {config.cache_ttl_seconds}s) ║ -║ Rate Limiting: {'ENABLED ' if config.rate_limit_enabled else 'DISABLED'} ({config.rate_limit_requests_per_minute} req/min, {config.rate_limit_tokens_per_minute:,} tok/min) ║ -║ Retry: {'ENABLED ' if config.retry_enabled else 'DISABLED'} (max {config.retry_max_attempts} attempts) ║ -║ Cost Tracking: {'ENABLED ' if config.cost_tracking_enabled else 'DISABLED'} (budget: {'$' + str(config.budget_limit_usd) + '/' + config.budget_period if config.budget_limit_usd else 'unlimited'}) ║ +║ Optimization: {"ENABLED " if config.optimize else "DISABLED"} ║ +║ Caching: {"ENABLED " if config.cache_enabled else "DISABLED"} (TTL: {config.cache_ttl_seconds}s) ║ +║ Rate Limiting: {"ENABLED " if config.rate_limit_enabled else "DISABLED"} ({config.rate_limit_requests_per_minute} req/min, {config.rate_limit_tokens_per_minute:,} tok/min) ║ +║ Retry: {"ENABLED " if config.retry_enabled else "DISABLED"} (max {config.retry_max_attempts} attempts) ║ +║ Cost Tracking: {"ENABLED " if config.cost_tracking_enabled else "DISABLED"} (budget: {"$" + str(config.budget_limit_usd) + "/" + config.budget_period if config.budget_limit_usd else "unlimited"}) ║ ╠══════════════════════════════════════════════════════════════════════╣ ║ USAGE: ║ ║ Claude Code: ANTHROPIC_BASE_URL=http://{config.host}:{config.port} claude ║ diff --git a/headroom/relevance/bm25.py b/headroom/relevance/bm25.py index 7d77ed192..ef9be9124 100644 --- a/headroom/relevance/bm25.py +++ b/headroom/relevance/bm25.py @@ -217,10 +217,7 @@ class BM25Scorer(RelevanceScorer): context_tokens = self._tokenize(context) if not context_tokens: - return [ - RelevanceScore(score=0.0, reason="BM25: empty context") - for _ in items - ] + return [RelevanceScore(score=0.0, reason="BM25: empty context") for _ in items] # Compute average document length for normalization all_tokens = [self._tokenize(item) for item in items] @@ -228,9 +225,7 @@ class BM25Scorer(RelevanceScorer): results = [] for item_tokens in all_tokens: - raw_score, matched = self._bm25_score( - item_tokens, context_tokens, avg_doc_len=avg_len - ) + raw_score, matched = self._bm25_score(item_tokens, context_tokens, avg_doc_len=avg_len) # Normalize if self.normalize_score: diff --git a/headroom/relevance/embedding.py b/headroom/relevance/embedding.py index 236823dd8..a428fe7c7 100644 --- a/headroom/relevance/embedding.py +++ b/headroom/relevance/embedding.py @@ -36,13 +36,14 @@ def _get_numpy(): import numpy as np _numpy = np - except ImportError: + except ImportError as e: raise ImportError( "numpy is required for EmbeddingScorer. " "Install with: pip install headroom[relevance]" - ) + ) from e return _numpy + if TYPE_CHECKING: from sentence_transformers import SentenceTransformer @@ -122,7 +123,7 @@ class EmbeddingScorer(RelevanceScorer): True if the package is available. """ try: - import sentence_transformers + import sentence_transformers # noqa: F401 return True except ImportError: @@ -220,10 +221,7 @@ class EmbeddingScorer(RelevanceScorer): return [] if not context: - return [ - RelevanceScore(score=0.0, reason="Embedding: empty context") - for _ in items - ] + return [RelevanceScore(score=0.0, reason="Embedding: empty context") for _ in items] # Encode all texts in one batch all_texts = items + [context] diff --git a/headroom/relevance/hybrid.py b/headroom/relevance/hybrid.py index 63e0b00c4..319ff7f57 100644 --- a/headroom/relevance/hybrid.py +++ b/headroom/relevance/hybrid.py @@ -57,9 +57,7 @@ class HybridScorer(RelevanceScorer): _HOSTNAME_PATTERN = re.compile( r"\b[a-zA-Z0-9][-a-zA-Z0-9]*\.[a-zA-Z0-9][-a-zA-Z0-9]*(?:\.[a-zA-Z]{2,})?\b" ) - _EMAIL_PATTERN = re.compile( - r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b" - ) + _EMAIL_PATTERN = re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b") def __init__( self, diff --git a/headroom/reporting/generator.py b/headroom/reporting/generator.py index 1def49330..e1d7bbaab 100644 --- a/headroom/reporting/generator.py +++ b/headroom/reporting/generator.py @@ -7,9 +7,10 @@ from pathlib import Path from typing import TYPE_CHECKING, Any from ..storage import create_storage +from ..utils import estimate_cost, format_cost if TYPE_CHECKING: - from jinja2 import Template + pass def _get_jinja2_template(template_str: str): @@ -18,15 +19,12 @@ def _get_jinja2_template(template_str: str): from jinja2 import Template return Template(template_str) - except ImportError: + except ImportError as e: raise ImportError( - "jinja2 is required for report generation. " - "Install with: pip install headroom[reports]" - ) + "jinja2 is required for report generation. Install with: pip install headroom[reports]" + ) from e -from ..utils import estimate_cost, format_cost - # HTML template embedded as string REPORT_TEMPLATE = """ @@ -433,11 +431,13 @@ def _build_waste_histogram( histogram = [] for key, tokens in sorted(totals.items(), key=lambda x: x[1], reverse=True): percentage = (tokens / max_val * 100) if max_val > 0 else 0 - histogram.append({ - "label": labels.get(key, key), - "tokens": tokens, - "percentage": percentage, - }) + histogram.append( + { + "label": labels.get(key, key), + "tokens": tokens, + "percentage": percentage, + } + ) return histogram @@ -459,14 +459,16 @@ def _get_top_waste_requests( tokens_saved = metrics.tokens_input_before - metrics.tokens_input_after - requests.append({ - "request_id": metrics.request_id, - "model": metrics.model, - "mode": metrics.mode, - "tokens_before": metrics.tokens_input_before, - "tokens_saved": tokens_saved, - "cache_alignment": metrics.cache_alignment_score, - }) + requests.append( + { + "request_id": metrics.request_id, + "model": metrics.model, + "mode": metrics.mode, + "tokens_before": metrics.tokens_input_before, + "tokens_saved": tokens_saved, + "cache_alignment": metrics.cache_alignment_score, + } + ) # Sort by tokens saved (waste potential) requests.sort(key=lambda x: x["tokens_before"], reverse=True) @@ -484,52 +486,64 @@ def _generate_recommendations( # Check cache alignment if stats["avg_cache_alignment"] < 50: - recommendations.append({ - "title": "Improve Cache Alignment", - "description": "Your cache alignment score is low. Consider moving dynamic content " - "(dates, timestamps, session IDs) out of system prompts into user messages.", - }) + recommendations.append( + { + "title": "Improve Cache Alignment", + "description": "Your cache alignment score is low. Consider moving dynamic content " + "(dates, timestamps, session IDs) out of system prompts into user messages.", + } + ) # Check for tool JSON bloat for item in waste_histogram: if item["label"] == "Tool JSON Bloat" and item["tokens"] > 10000: - recommendations.append({ - "title": "Enable Tool Output Compression", - "description": f"Detected {item['tokens']:,} tokens of tool JSON bloat. " - "Switch to 'optimize' mode and configure tool profiles to compress large tool outputs.", - }) + recommendations.append( + { + "title": "Enable Tool Output Compression", + "description": f"Detected {item['tokens']:,} tokens of tool JSON bloat. " + "Switch to 'optimize' mode and configure tool profiles to compress large tool outputs.", + } + ) break # Check for history bloat for item in waste_histogram: if item["label"] == "History Bloat" and item["tokens"] > 50000: - recommendations.append({ - "title": "Review Rolling Window Settings", - "description": f"Detected {item['tokens']:,} tokens of history bloat. " - "Consider reducing keep_last_turns or increasing output_buffer_tokens.", - }) + recommendations.append( + { + "title": "Review Rolling Window Settings", + "description": f"Detected {item['tokens']:,} tokens of history bloat. " + "Consider reducing keep_last_turns or increasing output_buffer_tokens.", + } + ) break # Check audit vs optimize ratio if stats["audit_count"] > stats["optimize_count"] * 2: - recommendations.append({ - "title": "Switch to Optimize Mode", - "description": f"{stats['audit_count']} requests in audit mode vs {stats['optimize_count']} in optimize. " - "Consider switching default_mode to 'optimize' to realize token savings.", - }) + recommendations.append( + { + "title": "Switch to Optimize Mode", + "description": f"{stats['audit_count']} requests in audit mode vs {stats['optimize_count']} in optimize. " + "Consider switching default_mode to 'optimize' to realize token savings.", + } + ) # General recommendation if stats["total_tokens_saved"] > 0: - recommendations.append({ - "title": "Continue Monitoring", - "description": f"You've saved {stats['total_tokens_saved']:,} tokens so far. " - f"Estimated cost savings: {stats['estimated_savings']}. Keep up the good work!", - }) + recommendations.append( + { + "title": "Continue Monitoring", + "description": f"You've saved {stats['total_tokens_saved']:,} tokens so far. " + f"Estimated cost savings: {stats['estimated_savings']}. Keep up the good work!", + } + ) else: - recommendations.append({ - "title": "Get Started", - "description": "No optimizations applied yet. Try setting headroom_mode='optimize' " - "on your next request to start seeing token savings.", - }) + recommendations.append( + { + "title": "Get Started", + "description": "No optimizations applied yet. Try setting headroom_mode='optimize' " + "on your next request to start seeing token savings.", + } + ) return recommendations diff --git a/headroom/storage/base.py b/headroom/storage/base.py index a71ff6466..76b98fa92 100644 --- a/headroom/storage/base.py +++ b/headroom/storage/base.py @@ -112,7 +112,7 @@ class Storage(ABC): """ pass - def close(self) -> None: + def close(self) -> None: # noqa: B027 """Close storage connection if applicable.""" pass diff --git a/headroom/telemetry/collector.py b/headroom/telemetry/collector.py index fc1537a4f..a0f97bc05 100644 --- a/headroom/telemetry/collector.py +++ b/headroom/telemetry/collector.py @@ -11,7 +11,7 @@ import json import os import threading import time -from dataclasses import dataclass, field +from dataclasses import dataclass from pathlib import Path from typing import Any @@ -184,7 +184,7 @@ class TelemetryCollector: # Store event self._events.append(event) if len(self._events) > self._config.max_events_in_memory: - self._events = self._events[-self._config.max_events_in_memory:] + self._events = self._events[-self._config.max_events_in_memory :] # Update aggregated stats self._update_tool_stats(signature, event) @@ -318,9 +318,7 @@ class TelemetryCollector: "confidence": stats.confidence, "based_on_samples": stats.sample_size, "retrieval_rate": ( - stats.retrieval_stats.retrieval_rate - if stats.retrieval_stats - else None + stats.retrieval_stats.retrieval_rate if stats.retrieval_stats else None ), } @@ -344,8 +342,7 @@ class TelemetryCollector: "tool_signatures_tracked": len(self._tool_stats), }, "tool_stats": { - sig_hash: stats.to_dict() - for sig_hash, stats in self._tool_stats.items() + sig_hash: stats.to_dict() for sig_hash, stats in self._tool_stats.items() }, } @@ -422,8 +419,7 @@ class TelemetryCollector: "tool_signatures_tracked": len(self._tool_stats), }, "tool_stats": { - sig_hash: stats.to_dict() - for sig_hash, stats in self._tool_stats.items() + sig_hash: stats.to_dict() for sig_hash, stats in self._tool_stats.items() }, } @@ -473,7 +469,7 @@ class TelemetryCollector: # Get all field names from first item sample = items[0] if isinstance(items[0], dict) else {} - for field_name, sample_value in sample.items(): + for field_name, _sample_value in sample.items(): # Collect all values for this field values = [ item.get(field_name) @@ -538,11 +534,20 @@ class TelemetryCollector: elif field_type == "numeric": num_values = [v for v in values if isinstance(v, (int, float))] # Filter out infinity and NaN which can cause issues - num_values = [v for v in num_values if not (isinstance(v, float) and (v != v or v == float('inf') or v == float('-inf')))] + num_values = [ + v + for v in num_values + if not ( + isinstance(v, float) and (v != v or v == float("inf") or v == float("-inf")) + ) + ] if num_values: dist.has_negative = any(v < 0 for v in num_values) # Safe integer check (avoid OverflowError from int(inf)) - dist.is_integer = all(isinstance(v, int) or (isinstance(v, float) and v.is_integer()) for v in num_values) + dist.is_integer = all( + isinstance(v, int) or (isinstance(v, float) and v.is_integer()) + for v in num_values + ) if len(num_values) > 1: mean = sum(num_values) / len(num_values) @@ -559,7 +564,7 @@ class TelemetryCollector: dist.variance_bucket = "high" # Check for outliers - std = variance ** 0.5 + std = variance**0.5 if std > 0: outliers = sum(1 for v in num_values if abs(v - mean) > 2 * std) dist.has_outliers = outliers > 0 @@ -567,8 +572,7 @@ class TelemetryCollector: # Pattern detection sorted_vals = sorted(num_values) is_monotonic = ( - sorted_vals == num_values or - list(reversed(sorted_vals)) == num_values + sorted_vals == num_values or list(reversed(sorted_vals)) == num_values ) if is_monotonic and dist.variance_bucket in ("medium", "high"): dist.is_likely_score = True @@ -598,11 +602,11 @@ class TelemetryCollector: # Update averages (rolling) n = stats.total_compressions stats.avg_compression_ratio = ( - (stats.avg_compression_ratio * (n - 1) + event.compression_ratio) / n - ) + stats.avg_compression_ratio * (n - 1) + event.compression_ratio + ) / n stats.avg_token_reduction = ( - (stats.avg_token_reduction * (n - 1) + event.token_reduction_ratio) / n - ) + stats.avg_token_reduction * (n - 1) + event.token_reduction_ratio + ) / n # Update strategy counts strategy = event.strategy @@ -672,20 +676,17 @@ class TelemetryCollector: existing.total_items_seen += imported.total_items_seen existing.total_items_kept += imported.total_items_kept existing.avg_compression_ratio = ( - existing.avg_compression_ratio * w_existing + - imported.avg_compression_ratio * w_imported + existing.avg_compression_ratio * w_existing + + imported.avg_compression_ratio * w_imported ) existing.avg_token_reduction = ( - existing.avg_token_reduction * w_existing + - imported.avg_token_reduction * w_imported + existing.avg_token_reduction * w_existing + imported.avg_token_reduction * w_imported ) existing.sample_size = total_samples # Merge strategy counts for strategy, count in imported.strategy_counts.items(): - existing.strategy_counts[strategy] = ( - existing.strategy_counts.get(strategy, 0) + count - ) + existing.strategy_counts[strategy] = existing.strategy_counts.get(strategy, 0) + count # Update confidence existing.confidence = min(0.95, total_samples / 100) diff --git a/headroom/telemetry/models.py b/headroom/telemetry/models.py index 84ab8634d..fa1d1d284 100644 --- a/headroom/telemetry/models.py +++ b/headroom/telemetry/models.py @@ -152,7 +152,9 @@ class ToolSignature: return current_depth @staticmethod - def _matches_pattern(key_lower: str, patterns: list[str], original_key: str | None = None) -> bool: + def _matches_pattern( + key_lower: str, patterns: list[str], original_key: str | None = None + ) -> bool: """Check if key matches patterns using word boundary matching. MEDIUM FIX #14: Prevent false positives like "hidden" matching "id". @@ -191,7 +193,7 @@ class ToolSignature: # Pattern capitalized (e.g., "Id" for "id") cap_pattern = pattern.capitalize() # Look for capital letter at start of pattern, preceded by lowercase - camel_regex = rf'(?<=[a-z]){re.escape(cap_pattern)}(?=[A-Z]|$)' + camel_regex = rf"(?<=[a-z]){re.escape(cap_pattern)}(?=[A-Z]|$)" if re.search(camel_regex, original_key): return True @@ -205,6 +207,7 @@ class ToolSignature: # different tools' empty responses from colliding into one pattern. # Use a random component to ensure uniqueness across tool types. import uuid + # MEDIUM FIX #15: Use 24 chars (96 bits) instead of 16 (64 bits) to reduce collision risk empty_hash = hashlib.sha256(f"empty:{uuid.uuid4()}".encode()).hexdigest()[:24] return cls( @@ -306,19 +309,31 @@ class ToolSignature: # MEDIUM FIX #14: Pattern detection with word boundary matching # Prevents false positives like "hidden" matching "id" # Pass original key for camelCase detection - if cls._matches_pattern(key_lower, ["id", "uuid", "guid"], key) or key_lower.endswith("key"): + if cls._matches_pattern(key_lower, ["id", "uuid", "guid"], key) or key_lower.endswith( + "key" + ): has_id = True - if cls._matches_pattern(key_lower, ["score", "rank", "rating", "relevance", "priority"], key): + if cls._matches_pattern( + key_lower, ["score", "rank", "rating", "relevance", "priority"], key + ): has_score = True - if cls._matches_pattern(key_lower, ["time", "date", "timestamp"], key) or \ - key_lower.endswith("_at") or key_lower in ["created", "updated"]: + if ( + cls._matches_pattern(key_lower, ["time", "date", "timestamp"], key) + or key_lower.endswith("_at") + or key_lower in ["created", "updated"] + ): has_timestamp = True - if cls._matches_pattern(key_lower, ["status", "state"], key) or \ - key_lower in ["level", "type", "kind"]: + if cls._matches_pattern(key_lower, ["status", "state"], key) or key_lower in [ + "level", + "type", + "kind", + ]: has_status = True if cls._matches_pattern(key_lower, ["error", "exception", "fail", "warning"], key): has_error = True - if cls._matches_pattern(key_lower, ["message", "msg", "text", "content", "body", "description"], key): + if cls._matches_pattern( + key_lower, ["message", "msg", "text", "content", "body", "description"], key + ): has_message = True # Create structure hash @@ -485,7 +500,9 @@ class AnonymizedToolStats: # Strategy distribution strategy_counts: dict[str, int] = field(default_factory=dict) # strategy -> count - strategy_success_rate: dict[str, float] = field(default_factory=dict) # strategy -> success rate + strategy_success_rate: dict[str, float] = field( + default_factory=dict + ) # strategy -> success rate # Retrieval statistics retrieval_stats: RetrievalStats | None = None diff --git a/headroom/telemetry/toin.py b/headroom/telemetry/toin.py index f6f3a795c..f601b47b0 100644 --- a/headroom/telemetry/toin.py +++ b/headroom/telemetry/toin.py @@ -47,9 +47,12 @@ import json import logging import threading import time +from collections.abc import Callable from dataclasses import dataclass, field from pathlib import Path -from typing import Any, Callable, Literal +from typing import Any, Literal + +from .models import ToolSignature logger = logging.getLogger(__name__) @@ -57,8 +60,6 @@ logger = logging.getLogger(__name__) # These allow users to plug in their own metrics collection (Prometheus, StatsD, etc.) MetricsCallback = Callable[[str, dict[str, Any]], None] # (event_name, event_data) -> None -from .models import ToolSignature - @dataclass class ToolPattern: @@ -171,9 +172,9 @@ class ToolPattern: # CRITICAL FIX: Track if truncation occurred during serialization # This tells from_dict() that some users were lost and prevents double-counting "tracking_truncated": ( - self._tracking_truncated or - self.user_count > len(self._seen_instance_hashes) or - len(self._all_seen_instances) > 100 + self._tracking_truncated + or self.user_count > len(self._seen_instance_hashes) + or len(self._all_seen_instances) > 100 ), } @@ -182,13 +183,28 @@ class ToolPattern: """Create from dictionary.""" # Filter to only valid fields valid_fields = { - "tool_signature_hash", "total_compressions", "total_items_seen", - "total_items_kept", "avg_compression_ratio", "avg_token_reduction", - "total_retrievals", "full_retrievals", "search_retrievals", - "commonly_retrieved_fields", "field_retrieval_frequency", - "common_query_patterns", "query_pattern_frequency", "optimal_strategy", - "strategy_success_rates", "optimal_max_items", "skip_compression_recommended", - "preserve_fields", "sample_size", "user_count", "confidence", "last_updated", + "tool_signature_hash", + "total_compressions", + "total_items_seen", + "total_items_kept", + "avg_compression_ratio", + "avg_token_reduction", + "total_retrievals", + "full_retrievals", + "search_retrievals", + "commonly_retrieved_fields", + "field_retrieval_frequency", + "common_query_patterns", + "query_pattern_frequency", + "optimal_strategy", + "strategy_success_rates", + "optimal_max_items", + "skip_compression_recommended", + "preserve_fields", + "sample_size", + "user_count", + "confidence", + "last_updated", } filtered = {k: v for k, v in data.items() if k in valid_fields} @@ -320,15 +336,18 @@ class ToolIntelligenceNetwork: """ if self._config.storage_path: # Derive from storage path - same path = same instance - return hashlib.sha256( - self._config.storage_path.encode() - ).hexdigest()[:16] # HIGH FIX: 64 bits instead of 32 + return hashlib.sha256(self._config.storage_path.encode()).hexdigest()[ + :16 + ] # HIGH FIX: 64 bits instead of 32 else: # No storage - use a combination of hostname and process info # This is less stable but better than pure random import os import socket - machine_info = f"{socket.gethostname()}:{os.getuid() if hasattr(os, 'getuid') else 'unknown'}" + + machine_info = ( + f"{socket.gethostname()}:{os.getuid() if hasattr(os, 'getuid') else 'unknown'}" + ) return hashlib.sha256(machine_info.encode()).hexdigest()[:16] # HIGH FIX: 64 bits def _emit_metric(self, event_name: str, event_data: dict[str, Any]) -> None: @@ -380,22 +399,23 @@ class ToolIntelligenceNetwork: sig_hash = tool_signature.structure_hash # LOW FIX #22: Emit compression metric - self._emit_metric("toin.compression", { - "signature_hash": sig_hash, - "original_count": original_count, - "compressed_count": compressed_count, - "original_tokens": original_tokens, - "compressed_tokens": compressed_tokens, - "strategy": strategy, - "compression_ratio": compressed_count / original_count if original_count > 0 else 0, - }) + self._emit_metric( + "toin.compression", + { + "signature_hash": sig_hash, + "original_count": original_count, + "compressed_count": compressed_count, + "original_tokens": original_tokens, + "compressed_tokens": compressed_tokens, + "strategy": strategy, + "compression_ratio": compressed_count / original_count if original_count > 0 else 0, + }, + ) with self._lock: # Get or create pattern if sig_hash not in self._patterns: - self._patterns[sig_hash] = ToolPattern( - tool_signature_hash=sig_hash - ) + self._patterns[sig_hash] = ToolPattern(tool_signature_hash=sig_hash) pattern = self._patterns[sig_hash] @@ -408,14 +428,16 @@ class ToolIntelligenceNetwork: # Update rolling averages n = pattern.total_compressions compression_ratio = compressed_count / original_count if original_count > 0 else 0.0 - token_reduction = 1 - (compressed_tokens / original_tokens) if original_tokens > 0 else 0.0 + token_reduction = ( + 1 - (compressed_tokens / original_tokens) if original_tokens > 0 else 0.0 + ) pattern.avg_compression_ratio = ( - (pattern.avg_compression_ratio * (n - 1) + compression_ratio) / n - ) + pattern.avg_compression_ratio * (n - 1) + compression_ratio + ) / n pattern.avg_token_reduction = ( - (pattern.avg_token_reduction * (n - 1) + token_reduction) / n - ) + pattern.avg_token_reduction * (n - 1) + token_reduction + ) / n # Update strategy stats if strategy not in pattern.strategy_success_rates: @@ -481,14 +503,14 @@ class ToolIntelligenceNetwork: pattern.common_query_patterns, key=lambda p: pattern.query_pattern_frequency.get(p, 0), reverse=True, - )[:self._config.max_query_patterns] + )[: self._config.max_query_patterns] # Also limit the frequency dict if len(pattern.query_pattern_frequency) > self._config.max_query_patterns * 2: top_patterns = sorted( pattern.query_pattern_frequency.items(), key=lambda x: x[1], reverse=True, - )[:self._config.max_query_patterns * 2] + )[: self._config.max_query_patterns * 2] pattern.query_pattern_frequency = dict(top_patterns) # Periodically update recommendations even without retrievals @@ -527,13 +549,16 @@ class ToolIntelligenceNetwork: return # LOW FIX #22: Emit retrieval metric - self._emit_metric("toin.retrieval", { - "signature_hash": tool_signature_hash, - "retrieval_type": retrieval_type, - "has_query": query is not None, - "query_fields_count": len(query_fields) if query_fields else 0, - "strategy": strategy, - }) + self._emit_metric( + "toin.retrieval", + { + "signature_hash": tool_signature_hash, + "retrieval_type": retrieval_type, + "has_query": query is not None, + "query_fields_count": len(query_fields) if query_fields else 0, + "strategy": strategy, + }, + ) with self._lock: if tool_signature_hash not in self._patterns: @@ -609,7 +634,7 @@ class ToolIntelligenceNetwork: pattern.common_query_patterns, key=lambda p: pattern.query_pattern_frequency.get(p, 0), reverse=True, - )[:self._config.max_query_patterns] + )[: self._config.max_query_patterns] # Update recommendations based on new retrieval data self._update_recommendations(pattern) @@ -659,7 +684,27 @@ class ToolIntelligenceNetwork: based_on_samples=pattern.sample_size, ) # LOW FIX #22: Emit recommendation metric - self._emit_metric("toin.recommendation", { + self._emit_metric( + "toin.recommendation", + { + "signature_hash": sig_hash, + "source": hint.source, + "confidence": hint.confidence, + "skip_compression": hint.skip_compression, + "max_items": hint.max_items, + "compression_level": hint.compression_level, + "based_on_samples": hint.based_on_samples, + }, + ) + return hint + + # Build recommendation based on learned patterns + hint = self._build_recommendation(pattern, query_context) + + # LOW FIX #22: Emit recommendation metric + self._emit_metric( + "toin.recommendation", + { "signature_hash": sig_hash, "source": hint.source, "confidence": hint.confidence, @@ -667,22 +712,8 @@ class ToolIntelligenceNetwork: "max_items": hint.max_items, "compression_level": hint.compression_level, "based_on_samples": hint.based_on_samples, - }) - return hint - - # Build recommendation based on learned patterns - hint = self._build_recommendation(pattern, query_context) - - # LOW FIX #22: Emit recommendation metric - self._emit_metric("toin.recommendation", { - "signature_hash": sig_hash, - "source": hint.source, - "confidence": hint.confidence, - "skip_compression": hint.skip_compression, - "max_items": hint.max_items, - "compression_level": hint.compression_level, - "based_on_samples": hint.based_on_samples, - }) + }, + ) return hint def _build_recommendation( @@ -692,7 +723,9 @@ class ToolIntelligenceNetwork: ) -> CompressionHint: """Build a recommendation based on pattern data and query context.""" hint = CompressionHint( - source="network" if pattern.user_count >= self._config.min_users_for_network_effect else "local", + source="network" + if pattern.user_count >= self._config.min_users_for_network_effect + else "local", confidence=pattern.confidence, based_on_samples=pattern.sample_size, ) @@ -734,7 +767,8 @@ class ToolIntelligenceNetwork: if query_context and pattern.field_retrieval_frequency: # Extract field names from query context import re - query_field_names = re.findall(r'(\w+)[=:]', query_context.lower()) + + query_field_names = re.findall(r"(\w+)[=:]", query_context.lower()) # Hash them and check if they're in our frequency data for field_name in query_field_names: @@ -762,9 +796,7 @@ class ToolIntelligenceNetwork: # Use optimal strategy if known AND it has good success rate if pattern.optimal_strategy != "default": - success_rate = pattern.strategy_success_rates.get( - pattern.optimal_strategy, 1.0 - ) + success_rate = pattern.strategy_success_rates.get(pattern.optimal_strategy, 1.0) # Only recommend strategy if success rate >= 0.5 # Lower success rates mean this strategy often causes retrievals if success_rate >= 0.5: @@ -772,7 +804,9 @@ class ToolIntelligenceNetwork: else: # Strategy has poor success rate - reduce confidence hint.confidence *= success_rate - hint.reason += f" (strategy {pattern.optimal_strategy} has low success: {success_rate:.1%})" + hint.reason += ( + f" (strategy {pattern.optimal_strategy} has low success: {success_rate:.1%})" + ) # Try to find a better strategy best_strategy = self._find_best_strategy(pattern) if best_strategy and best_strategy != pattern.optimal_strategy: @@ -804,16 +838,12 @@ class ToolIntelligenceNetwork: # Partial match: check if any stored pattern is contained in query for stored_pattern in pattern.common_query_patterns: # Check if key fields match (e.g., "status:*" in both) - stored_fields = set( - f.split(":")[0] - for f in stored_pattern.split() - if ":" in f - ) - query_fields = set( - f.split(":")[0] - for f in query_pattern.split() - if ":" in f - ) + stored_fields = { + f.split(":")[0] for f in stored_pattern.split() if ":" in f + } + query_fields = { + f.split(":")[0] for f in query_pattern.split() if ":" in f + } # If query uses same fields as a problematic pattern, be conservative if stored_fields and stored_fields.issubset(query_fields): hint.max_items = max(hint.max_items, 25) @@ -849,7 +879,9 @@ class ToolIntelligenceNetwork: if retrieval_rate > self._config.high_retrieval_threshold: if pattern.full_retrieval_rate > 0.8: pattern.skip_compression_recommended = True - pattern.optimal_max_items = pattern.total_items_seen // max(1, pattern.total_compressions) + pattern.optimal_max_items = pattern.total_items_seen // max( + 1, pattern.total_compressions + ) else: pattern.optimal_max_items = 50 elif retrieval_rate > self._config.medium_retrieval_threshold: @@ -906,8 +938,9 @@ class ToolIntelligenceNetwork: # Simple pattern extraction: replace values after : or = import re + # Match field:value or field="value" patterns, but don't include spaces in unquoted values - pattern = re.sub(r'(\w+)[=:](?:"[^"]*"|\'[^\']*\'|\w+)', r'\1:*', query) + pattern = re.sub(r'(\w+)[=:](?:"[^"]*"|\'[^\']*\'|\w+)', r"\1:*", query) # Remove if it's just generic if pattern in ("*", ""): @@ -927,11 +960,11 @@ class ToolIntelligenceNetwork: "total_compressions": total_compressions, "total_retrievals": total_retrievals, "global_retrieval_rate": ( - total_retrievals / total_compressions - if total_compressions > 0 else 0.0 + total_retrievals / total_compressions if total_compressions > 0 else 0.0 ), "patterns_with_recommendations": sum( - 1 for p in self._patterns.values() + 1 + for p in self._patterns.values() if p.sample_size >= self._config.min_samples_for_recommendation ), } @@ -942,6 +975,7 @@ class ToolIntelligenceNetwork: HIGH FIX: Returns a deep copy to prevent external mutation of internal state. """ import copy + with self._lock: pattern = self._patterns.get(signature_hash) if pattern is not None: @@ -956,8 +990,7 @@ class ToolIntelligenceNetwork: "export_timestamp": time.time(), "instance_id": self._instance_id, "patterns": { - sig_hash: pattern.to_dict() - for sig_hash, pattern in self._patterns.items() + sig_hash: pattern.to_dict() for sig_hash, pattern in self._patterns.items() }, } @@ -1019,12 +1052,11 @@ class ToolIntelligenceNetwork: # Weighted averages existing.avg_compression_ratio = ( - existing.avg_compression_ratio * w_existing + - imported.avg_compression_ratio * w_imported + existing.avg_compression_ratio * w_existing + + imported.avg_compression_ratio * w_imported ) existing.avg_token_reduction = ( - existing.avg_token_reduction * w_existing + - imported.avg_token_reduction * w_imported + existing.avg_token_reduction * w_existing + imported.avg_token_reduction * w_imported ) # Merge field frequencies @@ -1073,22 +1105,21 @@ class ToolIntelligenceNetwork: existing.common_query_patterns, key=lambda p: existing.query_pattern_frequency.get(p, 0), reverse=True, - )[:self._config.max_query_patterns] + )[: self._config.max_query_patterns] # Limit frequency dict if len(existing.query_pattern_frequency) > self._config.max_query_patterns * 2: top_patterns = sorted( existing.query_pattern_frequency.items(), key=lambda x: x[1], reverse=True, - )[:self._config.max_query_patterns * 2] + )[: self._config.max_query_patterns * 2] existing.query_pattern_frequency = dict(top_patterns) # Merge strategy success rates (weighted average) for strategy, rate in imported.strategy_success_rates.items(): if strategy in existing.strategy_success_rates: existing.strategy_success_rates[strategy] = ( - existing.strategy_success_rates[strategy] * w_existing + - rate * w_imported + existing.strategy_success_rates[strategy] * w_existing + rate * w_imported ) else: existing.strategy_success_rates[strategy] = rate @@ -1103,9 +1134,9 @@ class ToolIntelligenceNetwork: existing.strategy_success_rates = dict(sorted_strategies) # Merge preserve_fields (union of both, deduplicated) - for field in imported.preserve_fields: - if field not in existing.preserve_fields: - existing.preserve_fields.append(field) + for preserve_field in imported.preserve_fields: + if preserve_field not in existing.preserve_fields: + existing.preserve_fields.append(preserve_field) # Keep only top 10 most important fields if len(existing.preserve_fields) > 10: # Prioritize by retrieval frequency if available @@ -1126,12 +1157,12 @@ class ToolIntelligenceNetwork: # Merge optimal_strategy (prefer the one with better success rate) if imported.optimal_strategy != "default": - imported_rate = imported.strategy_success_rates.get( - imported.optimal_strategy, 0.5 + imported_rate = imported.strategy_success_rates.get(imported.optimal_strategy, 0.5) + existing_rate = ( + existing.strategy_success_rates.get(existing.optimal_strategy, 0.5) + if existing.optimal_strategy != "default" + else 0.0 ) - existing_rate = existing.strategy_success_rates.get( - existing.optimal_strategy, 0.5 - ) if existing.optimal_strategy != "default" else 0.0 if imported_rate > existing_rate: existing.optimal_strategy = imported.optimal_strategy @@ -1139,8 +1170,7 @@ class ToolIntelligenceNetwork: # Merge optimal_max_items (weighted average with bounds) if imported.optimal_max_items > 0: merged_max_items = int( - existing.optimal_max_items * w_existing + - imported.optimal_max_items * w_imported + existing.optimal_max_items * w_existing + imported.optimal_max_items * w_imported ) # Ensure valid bounds: min 3 items, max 1000 items existing.optimal_max_items = max(3, min(1000, merged_max_items)) @@ -1177,8 +1207,7 @@ class ToolIntelligenceNetwork: # that imported had beyond what we could deduplicate (when both hit caps). # imported.user_count may be > len(imported._all_seen_instances) if they hit cap users_beyond_imported_tracking = max( - 0, - imported.user_count - len(imported._all_seen_instances) + 0, imported.user_count - len(imported._all_seen_instances) ) existing.user_count += new_users_found + users_beyond_imported_tracking @@ -1217,11 +1246,7 @@ class ToolIntelligenceNetwork: # Write to temporary file first (atomic write pattern) # Use same directory to ensure same filesystem for rename - fd, tmp_path = tempfile.mkstemp( - dir=path.parent, - prefix=".toin_", - suffix=".tmp" - ) + fd, tmp_path = tempfile.mkstemp(dir=path.parent, prefix=".toin_", suffix=".tmp") try: with open(fd, "w") as f: f.write(json_data) diff --git a/headroom/tokenizers/__init__.py b/headroom/tokenizers/__init__.py index 7522a188c..c02ee663b 100644 --- a/headroom/tokenizers/__init__.py +++ b/headroom/tokenizers/__init__.py @@ -37,18 +37,21 @@ from .tiktoken_counter import TiktokenCounter def get_huggingface_tokenizer(): """Get HuggingFaceTokenizer class (requires transformers).""" from .huggingface import HuggingFaceTokenizer + return HuggingFaceTokenizer def get_mistral_tokenizer(): """Get MistralTokenizer class (requires mistral-common).""" from .mistral import MistralTokenizer + return MistralTokenizer def is_mistral_tokenizer_available() -> bool: """Check if Mistral tokenizer is available.""" from .mistral import is_mistral_available + return is_mistral_available() diff --git a/headroom/tokenizers/base.py b/headroom/tokenizers/base.py index 21a95397a..d867a52a0 100644 --- a/headroom/tokenizers/base.py +++ b/headroom/tokenizers/base.py @@ -179,9 +179,7 @@ class BaseTokenizer(ABC): Raises: NotImplementedError: If encoding is not supported. """ - raise NotImplementedError( - f"{self.__class__.__name__} does not support encoding" - ) + raise NotImplementedError(f"{self.__class__.__name__} does not support encoding") def decode(self, tokens: list[int]) -> str: """Decode token IDs to text. @@ -198,6 +196,4 @@ class BaseTokenizer(ABC): Raises: NotImplementedError: If decoding is not supported. """ - raise NotImplementedError( - f"{self.__class__.__name__} does not support decoding" - ) + raise NotImplementedError(f"{self.__class__.__name__} does not support decoding") diff --git a/headroom/tokenizers/estimator.py b/headroom/tokenizers/estimator.py index d2537c019..89fb0d6d1 100644 --- a/headroom/tokenizers/estimator.py +++ b/headroom/tokenizers/estimator.py @@ -44,16 +44,15 @@ class EstimatingTokenCounter(BaseTokenizer): # Patterns for content type detection CODE_PATTERN = re.compile( - r'(?:def |class |function |const |let |var |import |from |' - r'if \(|for \(|while \(|switch \(|try \{|catch \(|' - r'=>|->|\{\{|\}\}|;$)', - re.MULTILINE + r"(?:def |class |function |const |let |var |import |from |" + r"if \(|for \(|while \(|switch \(|try \{|catch \(|" + r"=>|->|\{\{|\}\}|;$)", + re.MULTILINE, ) - JSON_PATTERN = re.compile(r'^\s*[\[\{]') - URL_PATTERN = re.compile(r'https?://\S+') + JSON_PATTERN = re.compile(r"^\s*[\[\{]") + URL_PATTERN = re.compile(r"https?://\S+") UUID_PATTERN = re.compile( - r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}', - re.IGNORECASE + r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", re.IGNORECASE ) def __init__(self, chars_per_token: float | None = None): @@ -134,7 +133,7 @@ class EstimatingTokenCounter(BaseTokenizer): urls = self.URL_PATTERN.findall(text) for url in urls: # Each URL component adds overhead - overhead += url.count('/') + url.count('?') + url.count('&') + overhead += url.count("/") + url.count("?") + url.count("&") # UUIDs are typically 8-10 tokens despite being 36 chars uuids = self.UUID_PATTERN.findall(text) diff --git a/headroom/tokenizers/huggingface.py b/headroom/tokenizers/huggingface.py index 3cac028f8..62e6e9b56 100644 --- a/headroom/tokenizers/huggingface.py +++ b/headroom/tokenizers/huggingface.py @@ -298,7 +298,8 @@ class HuggingFaceTokenizer(BaseTokenizer): True if transformers is installed. """ try: - import transformers + import transformers # noqa: F401 + return True except ImportError: return False diff --git a/headroom/tokenizers/mistral.py b/headroom/tokenizers/mistral.py index 12317b84b..a5af1f9a8 100644 --- a/headroom/tokenizers/mistral.py +++ b/headroom/tokenizers/mistral.py @@ -25,6 +25,7 @@ try: ) from mistral_common.protocol.instruct.request import ChatCompletionRequest from mistral_common.tokens.tokenizers.mistral import MistralTokenizer as _MistralTokenizer + MISTRAL_AVAILABLE = True except ImportError: MISTRAL_AVAILABLE = False diff --git a/headroom/tokenizers/registry.py b/headroom/tokenizers/registry.py index 4934868da..797f0b642 100644 --- a/headroom/tokenizers/registry.py +++ b/headroom/tokenizers/registry.py @@ -157,8 +157,7 @@ class TokenizerRegistry: except Exception as e: if fallback: logger.warning( - f"Failed to create tokenizer for {model}: {e}. " - "Falling back to estimation." + f"Failed to create tokenizer for {model}: {e}. Falling back to estimation." ) tokenizer = EstimatingTokenCounter() registry._cache[cache_key] = tokenizer @@ -257,6 +256,7 @@ class TokenizerRegistry: """Create Mistral tokenizer using official mistral-common.""" try: from .mistral import MistralTokenizer, is_mistral_available + if is_mistral_available(): return MistralTokenizer(model) except ImportError: @@ -290,17 +290,17 @@ class TokenizerRegistry: """Create tiktoken-based tokenizer.""" try: from .tiktoken_counter import TiktokenCounter + return TiktokenCounter(model) except ImportError: - logger.warning( - "tiktoken not installed. Install with: pip install tiktoken" - ) + logger.warning("tiktoken not installed. Install with: pip install tiktoken") return EstimatingTokenCounter() def _create_huggingface(self, model: str) -> TokenCounter: """Create HuggingFace-based tokenizer.""" try: from .huggingface import HuggingFaceTokenizer + return HuggingFaceTokenizer(model) except ImportError: logger.warning( @@ -395,4 +395,4 @@ def list_supported_models() -> dict[str, str]: Returns: Dict mapping model pattern to backend. """ - return {pattern: backend for pattern, backend in MODEL_PATTERNS} + return dict(MODEL_PATTERNS) diff --git a/headroom/tokenizers/tiktoken_counter.py b/headroom/tokenizers/tiktoken_counter.py index 498e84ac4..6808ed3fa 100644 --- a/headroom/tokenizers/tiktoken_counter.py +++ b/headroom/tokenizers/tiktoken_counter.py @@ -80,6 +80,7 @@ DEFAULT_ENCODING = "cl100k_base" def _get_encoding(encoding_name: str): """Get tiktoken encoding, cached for performance.""" import tiktoken + return tiktoken.get_encoding(encoding_name) diff --git a/headroom/transforms/cache_aligner.py b/headroom/transforms/cache_aligner.py index 4e98cfee1..8214ed9e3 100644 --- a/headroom/transforms/cache_aligner.py +++ b/headroom/transforms/cache_aligner.py @@ -6,13 +6,13 @@ import logging import re from typing import Any -logger = logging.getLogger(__name__) - from ..config import CacheAlignerConfig, CachePrefixMetrics, TransformResult from ..tokenizer import Tokenizer from ..utils import compute_short_hash, deep_copy_messages from .base import Transform +logger = logging.getLogger(__name__) + class CacheAligner(Transform): """ @@ -44,9 +44,7 @@ class CacheAligner(Transform): def _compile_patterns(self) -> None: """Compile regex patterns for efficiency.""" - self._compiled_patterns = [ - re.compile(pattern) for pattern in self.config.date_patterns - ] + self._compiled_patterns = [re.compile(pattern) for pattern in self.config.date_patterns] def should_apply( self, @@ -121,8 +119,7 @@ class CacheAligner(Transform): prefix_bytes = len(stable_prefix_content.encode("utf-8")) prefix_tokens_est = tokenizer.count_text(stable_prefix_content) prefix_changed = ( - self._previous_prefix_hash is not None - and self._previous_prefix_hash != stable_hash + self._previous_prefix_hash is not None and self._previous_prefix_hash != stable_hash ) previous_hash = self._previous_prefix_hash diff --git a/headroom/transforms/pipeline.py b/headroom/transforms/pipeline.py index d539e16a7..c0b690789 100644 --- a/headroom/transforms/pipeline.py +++ b/headroom/transforms/pipeline.py @@ -5,8 +5,6 @@ from __future__ import annotations import logging from typing import TYPE_CHECKING, Any -logger = logging.getLogger(__name__) - from ..config import ( CacheAlignerConfig, DiffArtifact, @@ -27,6 +25,8 @@ from .tool_crusher import ToolCrusher if TYPE_CHECKING: from ..providers.base import Provider +logger = logging.getLogger(__name__) + class TransformPipeline: """ @@ -75,6 +75,7 @@ class TransformPipeline: if self.config.smart_crusher.enabled: # Use smart statistical crushing from .smart_crusher import SmartCrusherConfig as SCConfig + smart_config = SCConfig( enabled=True, min_items_to_analyze=self.config.smart_crusher.min_items_to_analyze, @@ -196,13 +197,17 @@ class TransformPipeline: # Record diff if enabled if generate_diff: - transform_diffs.append(TransformDiff( - transform_name=transform.name, - tokens_before=tokens_before_transform, - tokens_after=tokens_after_transform, - tokens_saved=tokens_before_transform - tokens_after_transform, - details=", ".join(result.transforms_applied) if result.transforms_applied else "", - )) + transform_diffs.append( + TransformDiff( + transform_name=transform.name, + tokens_before=tokens_before_transform, + tokens_after=tokens_after_transform, + tokens_saved=tokens_before_transform - tokens_after_transform, + details=", ".join(result.transforms_applied) + if result.transforms_applied + else "", + ) + ) # Final token count tokens_after = tokenizer.count_messages(current_messages) diff --git a/headroom/transforms/rolling_window.py b/headroom/transforms/rolling_window.py index 85b5ccb9c..2e276727a 100644 --- a/headroom/transforms/rolling_window.py +++ b/headroom/transforms/rolling_window.py @@ -5,14 +5,14 @@ from __future__ import annotations import logging from typing import Any -logger = logging.getLogger(__name__) - from ..config import RollingWindowConfig, TransformResult from ..parser import find_tool_units from ..tokenizer import Tokenizer from ..utils import create_dropped_context_marker, deep_copy_messages from .base import Transform +logger = logging.getLogger(__name__) + class RollingWindow(Transform): """ @@ -109,9 +109,7 @@ class RollingWindow(Transform): tool_units = find_tool_units(result_messages) # Create drop candidates with priorities - drop_candidates = self._build_drop_candidates( - result_messages, protected, tool_units - ) + drop_candidates = self._build_drop_candidates(result_messages, protected, tool_units) # Drop until under budget indices_to_drop: set[int] = set() @@ -261,12 +259,14 @@ class RollingWindow(Transform): continue all_indices = [assistant_idx] + response_indices - candidates.append({ - "type": "tool_unit", - "indices": all_indices, - "priority": 1, - "position": assistant_idx, # For sorting by age - }) + candidates.append( + { + "type": "tool_unit", + "indices": all_indices, + "priority": 1, + "position": assistant_idx, # For sorting by age + } + ) # Priority 2: Oldest non-tool messages (user/assistant pairs) i = 0 @@ -283,22 +283,26 @@ class RollingWindow(Transform): if role == "user" and i + 1 < len(messages): next_msg = messages[i + 1] if next_msg.get("role") == "assistant" and i + 1 not in tool_unit_indices: - candidates.append({ - "type": "turn", - "indices": [i, i + 1], - "priority": 2, - "position": i, - }) + candidates.append( + { + "type": "turn", + "indices": [i, i + 1], + "priority": 2, + "position": i, + } + ) i += 2 continue # Single message - candidates.append({ - "type": "single", - "indices": [i], - "priority": 2, - "position": i, - }) + candidates.append( + { + "type": "single", + "indices": [i], + "priority": 2, + "position": i, + } + ) i += 1 diff --git a/headroom/transforms/smart_crusher.py b/headroom/transforms/smart_crusher.py index ec72f8b3b..f5df034f5 100644 --- a/headroom/transforms/smart_crusher.py +++ b/headroom/transforms/smart_crusher.py @@ -28,9 +28,9 @@ Key Features: from __future__ import annotations import hashlib +import json import logging import math -import json import re import statistics import threading @@ -45,6 +45,15 @@ from ..config import CCRConfig, RelevanceScorerConfig, TransformResult from ..relevance import RelevanceScorer, create_scorer from ..telemetry import TelemetryCollector, ToolSignature, get_telemetry_collector from ..telemetry.toin import ToolIntelligenceNetwork, get_toin +from ..tokenizer import Tokenizer +from ..utils import ( + compute_short_hash, + create_tool_digest_marker, + deep_copy_messages, + safe_json_dumps, + safe_json_loads, +) +from .base import Transform logger = logging.getLogger(__name__) @@ -192,7 +201,7 @@ def _item_has_preserve_field_match( query_lower = query_context.lower() - for field_name, value in _get_preserve_field_values(item, preserve_field_hashes): + for _field_name, value in _get_preserve_field_values(item, preserve_field_hashes): if value is not None: value_str = str(value).lower() if value_str in query_lower or query_lower in value_str: @@ -201,25 +210,15 @@ def _item_has_preserve_field_match( return False -from ..tokenizer import Tokenizer -from ..utils import ( - compute_short_hash, - create_tool_digest_marker, - deep_copy_messages, - safe_json_dumps, - safe_json_loads, -) -from .base import Transform - - class CompressionStrategy(Enum): """Compression strategies based on data patterns.""" - NONE = "none" # No compression needed - SKIP = "skip" # Explicitly skip - not safe to crush - TIME_SERIES = "time_series" # Keep change points, summarize stable - CLUSTER_SAMPLE = "cluster" # Dedupe similar items - TOP_N = "top_n" # Keep highest scored items - SMART_SAMPLE = "smart_sample" # Statistical sampling with constants + + NONE = "none" # No compression needed + SKIP = "skip" # Explicitly skip - not safe to crush + TIME_SERIES = "time_series" # Keep change points, summarize stable + CLUSTER_SAMPLE = "cluster" # Dedupe similar items + TOP_N = "top_n" # Keep highest scored items + SMART_SAMPLE = "smart_sample" # Statistical sampling with constants # ===================================================================== @@ -262,6 +261,7 @@ def _calculate_string_entropy(s: str) -> float: # Calculate entropy import math + entropy = 0.0 length = len(s) for count in freq.values(): @@ -305,7 +305,7 @@ def _detect_sequential_pattern(values: list[Any], check_order: bool = True) -> b # Check if sorted values form a near-sequence sorted_nums = sorted(nums) - diffs = [sorted_nums[i+1] - sorted_nums[i] for i in range(len(sorted_nums)-1)] + diffs = [sorted_nums[i + 1] - sorted_nums[i] for i in range(len(sorted_nums) - 1)] if not diffs: return False @@ -321,7 +321,7 @@ def _detect_sequential_pattern(values: list[Any], check_order: bool = True) -> b # Scores sorted by relevance are typically in DESCENDING order if check_order and is_sequential: # Check if original order is ascending (like IDs) - ascending_count = sum(1 for i in range(len(nums)-1) if nums[i] <= nums[i+1]) + ascending_count = sum(1 for i in range(len(nums) - 1) if nums[i] <= nums[i + 1]) is_ascending = ascending_count / (len(nums) - 1) > 0.7 return is_ascending # Only flag as sequential if ascending (ID-like) @@ -330,7 +330,7 @@ def _detect_sequential_pattern(values: list[Any], check_order: bool = True) -> b return False -def _detect_id_field_statistically(stats: "FieldStats", values: list[Any]) -> tuple[bool, float]: +def _detect_id_field_statistically(stats: FieldStats, values: list[Any]) -> tuple[bool, float]: """Detect if a field is an ID field using statistical properties. Returns (is_id_field, confidence). @@ -354,7 +354,9 @@ def _detect_id_field_statistically(stats: "FieldStats", values: list[Any]) -> tu # Check for high entropy (random string IDs) if sample_values: - avg_entropy = sum(_calculate_string_entropy(v) for v in sample_values) / len(sample_values) + avg_entropy = sum(_calculate_string_entropy(v) for v in sample_values) / len( + sample_values + ) if avg_entropy > 0.7 and stats.unique_ratio > 0.95: confidence = 0.8 return True, confidence @@ -377,7 +379,7 @@ def _detect_id_field_statistically(stats: "FieldStats", values: list[Any]) -> tu return False, 0.0 -def _detect_score_field_statistically(stats: "FieldStats", items: list[dict]) -> tuple[bool, float]: +def _detect_score_field_statistically(stats: FieldStats, items: list[dict]) -> tuple[bool, float]: """Detect if a field is a score/ranking field using statistical properties. Returns (is_score_field, confidence). @@ -397,7 +399,7 @@ def _detect_score_field_statistically(stats: "FieldStats", items: list[dict]) -> confidence = 0.0 # Check for bounded range typical of scores - value_range = stats.max_val - stats.min_val + stats.max_val - stats.min_val min_val, max_val = stats.min_val, stats.max_val # Common score ranges: [0,1], [0,10], [0,100], [-1,1], [0,5] @@ -426,22 +428,26 @@ def _detect_score_field_statistically(stats: "FieldStats", items: list[dict]) -> # Check if data appears sorted by this field (descending = relevance sorted) # Filter out NaN/Inf which break comparisons values_in_order = [ - item.get(stats.name) for item in items + item.get(stats.name) + for item in items if stats.name in item and isinstance(item.get(stats.name), (int, float)) and math.isfinite(item.get(stats.name)) ] if len(values_in_order) >= 5: # Check for descending sort - descending_count = sum(1 for i in range(len(values_in_order)-1) if values_in_order[i] >= values_in_order[i+1]) + descending_count = sum( + 1 + for i in range(len(values_in_order) - 1) + if values_in_order[i] >= values_in_order[i + 1] + ) if descending_count / (len(values_in_order) - 1) > 0.7: confidence += 0.3 # Score fields often have floating point values # Filter out NaN/Inf which can't be converted to int float_count = sum( - 1 for v in values_in_order[:20] - if isinstance(v, float) and math.isfinite(v) and v != int(v) + 1 for v in values_in_order[:20] if isinstance(v, float) and math.isfinite(v) and v != int(v) ) if float_count > len(values_in_order[:20]) * 0.3: confidence += 0.1 @@ -505,12 +511,14 @@ def _detect_rare_status_values(items: list[dict], common_fields: set[str]) -> li outlier_indices: list[int] = [] # Find potential status fields (low cardinality) - for field in common_fields: - values = [item.get(field) for item in items if isinstance(item, dict) and field in item] + for field_name in common_fields: + values = [ + item.get(field_name) for item in items if isinstance(item, dict) and field_name in item + ] # Skip if too few values or non-hashable try: - unique_values = set(str(v) for v in values if v is not None) + unique_values = {str(v) for v in values if v is not None} except Exception: continue @@ -536,9 +544,9 @@ def _detect_rare_status_values(items: list[dict], common_fields: set[str]) -> li dominant_value = max(value_counts.keys(), key=lambda k: value_counts[k]) for i, item in enumerate(items): - if not isinstance(item, dict) or field not in item: + if not isinstance(item, dict) or field_name not in item: continue - item_value = str(item[field]) if item[field] is not None else "__none__" + item_value = str(item[field_name]) if item[field_name] is not None else "__none__" if item_value != dominant_value: outlier_indices.append(i) @@ -548,10 +556,22 @@ def _detect_rare_status_values(items: list[dict], common_fields: set[str]) -> li # Error keywords for PRESERVATION guarantee (not crushability detection) # This is for the quality guarantee: "ALL error items are ALWAYS preserved" # regardless of how common they are. Used in _prioritize_indices(). -_ERROR_KEYWORDS_FOR_PRESERVATION = frozenset({ - "error", "exception", "failed", "failure", "critical", "fatal", - "crash", "panic", "abort", "timeout", "denied", "rejected", -}) +_ERROR_KEYWORDS_FOR_PRESERVATION = frozenset( + { + "error", + "exception", + "failed", + "failure", + "critical", + "fatal", + "crash", + "panic", + "abort", + "timeout", + "denied", + "rejected", + } +) def _detect_error_items_for_preservation(items: list[dict]) -> list[int]: @@ -599,6 +619,7 @@ class CrushabilityAnalysis: High variability + No signal = DON'T CRUSH """ + crushable: bool confidence: float # 0.0 to 1.0 reason: str @@ -617,6 +638,7 @@ class CrushabilityAnalysis: @dataclass class FieldStats: """Statistics for a single field across array items.""" + name: str field_type: str # "numeric", "string", "boolean", "object", "array", "null" count: int @@ -640,6 +662,7 @@ class FieldStats: @dataclass class ArrayAnalysis: """Complete analysis of an array.""" + item_count: int field_stats: dict[str, FieldStats] detected_pattern: str # "time_series", "logs", "search_results", "generic" @@ -652,6 +675,7 @@ class ArrayAnalysis: @dataclass class CompressionPlan: """Plan for how to compress an array.""" + strategy: CompressionStrategy keep_indices: list[int] = field(default_factory=list) constant_fields: dict[str, Any] = field(default_factory=dict) @@ -668,19 +692,20 @@ class SmartCrusherConfig: SCHEMA-PRESERVING: Output contains only items from the original array. No wrappers, no generated text, no metadata keys. """ + enabled: bool = True - min_items_to_analyze: int = 5 # Don't analyze tiny arrays - min_tokens_to_crush: int = 200 # Only crush if > N tokens - variance_threshold: float = 2.0 # Std devs for change point detection + min_items_to_analyze: int = 5 # Don't analyze tiny arrays + min_tokens_to_crush: int = 200 # Only crush if > N tokens + variance_threshold: float = 2.0 # Std devs for change point detection uniqueness_threshold: float = 0.1 # Below this = nearly constant similarity_threshold: float = 0.8 # For clustering similar strings - max_items_after_crush: int = 15 # Target max items in output + max_items_after_crush: int = 15 # Target max items in output preserve_change_points: bool = True factor_out_constants: bool = False # Disabled - preserves original schema - include_summaries: bool = False # Disabled - no generated text + include_summaries: bool = False # Disabled - no generated text # Feedback loop integration - use_feedback_hints: bool = True # Use learned patterns to adjust compression + use_feedback_hints: bool = True # Use learned patterns to adjust compression # LOW FIX #21: Make TOIN confidence threshold configurable # Minimum confidence required to apply TOIN recommendations @@ -719,11 +744,7 @@ class SmartAnalyzer: pattern = self._detect_pattern(field_stats, items) # Extract constants - constant_fields = { - k: v.constant_value - for k, v in field_stats.items() - if v.is_constant - } + constant_fields = {k: v.constant_value for k, v in field_stats.items() if v.is_constant} # CRITICAL: Analyze crushability BEFORE selecting strategy crushability = self.analyze_crushability(items, field_stats) @@ -801,10 +822,7 @@ class SmartAnalyzer: # Numeric-specific analysis if field_type == "numeric": # Filter out NaN and Infinity which break statistics functions - nums = [ - v for v in non_null_values - if isinstance(v, (int, float)) and math.isfinite(v) - ] + nums = [v for v in non_null_values if isinstance(v, (int, float)) and math.isfinite(v)] if nums: try: stats.min_val = min(nums) @@ -845,8 +863,8 @@ class SmartAnalyzer: # Sliding window comparison for i in range(window, len(values) - window): - before_mean = statistics.mean(values[i-window:i]) - after_mean = statistics.mean(values[i:i+window]) + before_mean = statistics.mean(values[i - window : i]) + after_mean = statistics.mean(values[i : i + window]) if abs(after_mean - before_mean) > threshold: change_points.append(i) @@ -875,8 +893,7 @@ class SmartAnalyzer: numeric_fields = [k for k, v in field_stats.items() if v.field_type == "numeric"] has_numeric_with_variance = any( - field_stats[k].variance and field_stats[k].variance > 0 - for k in numeric_fields + field_stats[k].variance and field_stats[k].variance > 0 for k in numeric_fields ) if has_timestamp and has_numeric_with_variance: @@ -887,7 +904,7 @@ class SmartAnalyzer: has_message_like = False has_level_like = False - for name, stats in field_stats.items(): + for _name, stats in field_stats.items(): if stats.field_type == "string": # High-cardinality string = likely message field if stats.unique_ratio > 0.5 and stats.avg_length and stats.avg_length > 20: @@ -900,7 +917,7 @@ class SmartAnalyzer: return "logs" # Check for search results pattern using STATISTICAL score detection - for name, stats in field_stats.items(): + for _name, stats in field_stats.items(): is_score, confidence = _detect_score_field_statistically(stats, items) if is_score and confidence >= 0.5: return "search_results" @@ -920,13 +937,13 @@ class SmartAnalyzer: if stats.field_type == "string": # Sample some values sample_values = [ - item.get(name) for item in items[:10] - if isinstance(item.get(name), str) + item.get(name) for item in items[:10] if isinstance(item.get(name), str) ] if sample_values: # Check if values look like dates/datetimes iso_count = sum( - 1 for v in sample_values + 1 + for v in sample_values if iso_datetime_pattern.match(v) or iso_date_pattern.match(v) ) if iso_count / len(sample_values) > 0.5: @@ -982,12 +999,10 @@ class SmartAnalyzer: # 2. Detect score/rank field STATISTICALLY (no hardcoded field names) has_score_field = False - score_field_name = None for name, stats in field_stats.items(): is_score, confidence = _detect_score_field_statistically(stats, items) if is_score: has_score_field = True - score_field_name = name signals_present.append(f"score_field:{name}(conf={confidence:.2f})") break if not has_score_field: @@ -1019,7 +1034,7 @@ class SmartAnalyzer: anomaly_indices: set[int] = set() for stats in field_stats.values(): if stats.field_type == "numeric" and stats.mean_val is not None and stats.variance: - std = stats.variance ** 0.5 + std = stats.variance**0.5 if std > 0: threshold = self.config.variance_threshold * std for i, item in enumerate(items): @@ -1036,22 +1051,20 @@ class SmartAnalyzer: # 5. Compute average string uniqueness (EXCLUDING statistically-detected ID fields) string_stats = [ - s for s in field_stats.values() - if s.field_type == "string" and s.name != id_field_name + s for s in field_stats.values() if s.field_type == "string" and s.name != id_field_name ] avg_string_uniqueness = ( - statistics.mean(s.unique_ratio for s in string_stats) - if string_stats else 0.0 + statistics.mean(s.unique_ratio for s in string_stats) if string_stats else 0.0 ) # Compute uniqueness of non-ID numeric fields non_id_numeric_stats = [ - s for s in field_stats.values() - if s.field_type == "numeric" and s.name != id_field_name + s for s in field_stats.values() if s.field_type == "numeric" and s.name != id_field_name ] avg_non_id_numeric_uniqueness = ( statistics.mean(s.unique_ratio for s in non_id_numeric_stats) - if non_id_numeric_stats else 0.0 + if non_id_numeric_stats + else 0.0 ) # Combined uniqueness metric (including ID fields) @@ -1062,8 +1075,7 @@ class SmartAnalyzer: # 6. Check for change points (importance signal for time series) has_change_points = any( - stats.change_points for stats in field_stats.values() - if stats.field_type == "numeric" + stats.change_points for stats in field_stats.values() if stats.field_type == "numeric" ) if has_change_points: signals_present.append("change_points") @@ -1195,8 +1207,7 @@ class SmartAnalyzer: if pattern == "logs": # Check if messages are clusterable (low-medium uniqueness) message_field = next( - (v for k, v in field_stats.items() if "message" in k.lower()), - None + (v for k, v in field_stats.items() if "message" in k.lower()), None ) if message_field and message_field.unique_ratio < 0.5: return CompressionStrategy.CLUSTER_SAMPLE @@ -1208,10 +1219,7 @@ class SmartAnalyzer: return CompressionStrategy.SMART_SAMPLE def _estimate_reduction( - self, - field_stats: dict[str, FieldStats], - strategy: CompressionStrategy, - item_count: int + self, field_stats: dict[str, FieldStats], strategy: CompressionStrategy, item_count: int ) -> float: """Estimate token reduction ratio.""" if strategy == CompressionStrategy.NONE: @@ -1473,7 +1481,7 @@ class SmartCrusher(Transform): if analysis and analysis.field_stats: for field_name, stats in analysis.field_stats.items(): if stats.field_type == "numeric" and stats.mean_val is not None and stats.variance: - std = stats.variance ** 0.5 + std = stats.variance**0.5 if std > 0: threshold = self.config.variance_threshold * std for i, item in enumerate(items): @@ -1668,9 +1676,7 @@ class SmartCrusher(Transform): warnings=warnings, ) - def _extract_context_from_messages( - self, messages: list[dict[str, Any]] - ) -> str: + def _extract_context_from_messages(self, messages: list[dict[str, Any]]) -> str: """Extract query context from recent messages for relevance scoring. Builds a context string from: @@ -1772,8 +1778,7 @@ class SmartCrusher(Transform): # Check if this array should be crushed # Must have enough items AND all items must be dicts (not mixed types) all_dicts = value and all(isinstance(item, dict) for item in value) - if (len(value) >= self.config.min_items_to_analyze and all_dicts): - + if len(value) >= self.config.min_items_to_analyze and all_dicts: crushed, strategy, ccr_hash = self._crush_array(value, query_context, tool_name) info_parts.append(f"{strategy}({len(value)}->{len(crushed)})") @@ -1786,7 +1791,9 @@ class SmartCrusher(Transform): # Process items recursively processed = [] for item in value: - p_item, p_info, p_markers = self._process_value(item, depth + 1, query_context, tool_name) + p_item, p_info, p_markers = self._process_value( + item, depth + 1, query_context, tool_name + ) processed.append(p_item) if p_info: info_parts.append(p_info) @@ -1797,7 +1804,9 @@ class SmartCrusher(Transform): # Process values recursively processed = {} for k, v in value.items(): - p_val, p_info, p_markers = self._process_value(v, depth + 1, query_context, tool_name) + p_val, p_info, p_markers = self._process_value( + v, depth + 1, query_context, tool_name + ) processed[k] = p_val if p_info: info_parts.append(p_info) @@ -1850,7 +1859,10 @@ class SmartCrusher(Transform): toin_recommended_strategy: str | None = None toin_compression_level: str | None = None # LOW FIX #21: Use configurable threshold instead of hardcoded 0.5 - if toin_hint.source in ("network", "local") and toin_hint.confidence >= self.config.toin_confidence_threshold: + if ( + toin_hint.source in ("network", "local") + and toin_hint.confidence >= self.config.toin_confidence_threshold + ): # TOIN recommendations take precedence over local feedback effective_max_items = toin_hint.max_items toin_preserve_fields = toin_hint.preserve_fields # Fields to never remove @@ -1879,9 +1891,7 @@ class SmartCrusher(Transform): # Note: CompressionFeedback stores actual field names, but _plan methods # expect SHA256[:8] hashes for privacy-preserving comparison if hints.preserve_fields: - toin_preserve_fields = [ - _hash_field_name(field) for field in hints.preserve_fields - ] + toin_preserve_fields = [_hash_field_name(field) for field in hints.preserve_fields] # Use recommended_strategy from local feedback if not already set by TOIN if hints.recommended_strategy and not toin_recommended_strategy: @@ -1916,10 +1926,7 @@ class SmartCrusher(Transform): return items, "skip:toin_level_none", None elif toin_compression_level == "conservative": # Be conservative - keep more items - effective_max_items = max( - effective_max_items, - min(50, len(items) // 2) - ) + effective_max_items = max(effective_max_items, min(50, len(items) // 2)) elif toin_compression_level == "aggressive": # Be aggressive - keep fewer items effective_max_items = min(effective_max_items, 15) @@ -1928,7 +1935,9 @@ class SmartCrusher(Transform): # Pass TOIN preserve_fields so items with those fields get priority # Pass effective_max_items for thread-safe compression plan = self._create_plan( - analysis, items, query_context, + analysis, + items, + query_context, preserve_fields=toin_preserve_fields or None, effective_max_items=effective_max_items, ) @@ -2035,7 +2044,11 @@ class SmartCrusher(Transform): effective_max_items: Thread-safe max items limit (defaults to config value). """ # Use provided effective_max_items or fall back to config - max_items = effective_max_items if effective_max_items is not None else self.config.max_items_after_crush + max_items = ( + effective_max_items + if effective_max_items is not None + else self.config.max_items_after_crush + ) plan = CompressionPlan( strategy=analysis.recommended_strategy, @@ -2048,16 +2061,24 @@ class SmartCrusher(Transform): return plan if analysis.recommended_strategy == CompressionStrategy.TIME_SERIES: - plan = self._plan_time_series(analysis, items, plan, query_context, preserve_fields, max_items) + plan = self._plan_time_series( + analysis, items, plan, query_context, preserve_fields, max_items + ) elif analysis.recommended_strategy == CompressionStrategy.CLUSTER_SAMPLE: - plan = self._plan_cluster_sample(analysis, items, plan, query_context, preserve_fields, max_items) + plan = self._plan_cluster_sample( + analysis, items, plan, query_context, preserve_fields, max_items + ) elif analysis.recommended_strategy == CompressionStrategy.TOP_N: - plan = self._plan_top_n(analysis, items, plan, query_context, preserve_fields, max_items) + plan = self._plan_top_n( + analysis, items, plan, query_context, preserve_fields, max_items + ) else: # SMART_SAMPLE or NONE - plan = self._plan_smart_sample(analysis, items, plan, query_context, preserve_fields, max_items) + plan = self._plan_smart_sample( + analysis, items, plan, query_context, preserve_fields, max_items + ) return plan @@ -2262,17 +2283,16 @@ class SmartCrusher(Transform): max_confidence = confidence if not score_field: - return self._plan_smart_sample(analysis, items, plan, query_context, preserve_fields, effective_max) + return self._plan_smart_sample( + analysis, items, plan, query_context, preserve_fields, effective_max + ) plan.sort_field = score_field keep_indices = set() # 1. TOP N by score FIRST (the primary relevance signal) # The original system's score field is the authoritative ranking - scored_items = [ - (i, item.get(score_field, 0)) - for i, item in enumerate(items) - ] + scored_items = [(i, item.get(score_field, 0)) for i, item in enumerate(items)] scored_items.sort(key=lambda x: x[1], reverse=True) # Reserve slots for outliers @@ -2370,7 +2390,7 @@ class SmartCrusher(Transform): # 4. Anomalous numeric items (> 2 std from mean) for name, stats in analysis.field_stats.items(): if stats.field_type == "numeric" and stats.mean_val is not None and stats.variance: - std = stats.variance ** 0.5 + std = stats.variance**0.5 if std > 0: threshold = self.config.variance_threshold * std for i, item in enumerate(items): @@ -2412,10 +2432,7 @@ class SmartCrusher(Transform): return plan def _execute_plan( - self, - plan: CompressionPlan, - items: list[dict], - analysis: ArrayAnalysis + self, plan: CompressionPlan, items: list[dict], analysis: ArrayAnalysis ) -> list: """Execute a compression plan and return crushed array. diff --git a/headroom/transforms/tool_crusher.py b/headroom/transforms/tool_crusher.py index 0d78d20c1..693d5f370 100644 --- a/headroom/transforms/tool_crusher.py +++ b/headroom/transforms/tool_crusher.py @@ -5,8 +5,6 @@ from __future__ import annotations import logging from typing import Any -logger = logging.getLogger(__name__) - from ..config import ToolCrusherConfig, TransformResult from ..tokenizer import Tokenizer from ..utils import ( @@ -18,6 +16,8 @@ from ..utils import ( ) from .base import Transform +logger = logging.getLogger(__name__) + class ToolCrusher(Transform): """ @@ -253,7 +253,10 @@ class ToolCrusher(Transform): elif isinstance(value, list): return {"__headroom_depth_exceeded": len(value)} elif isinstance(value, str) and len(value) > max_string_length: - return value[:max_string_length] + f"...[truncated {len(value) - max_string_length} chars]" + return ( + value[:max_string_length] + + f"...[truncated {len(value) - max_string_length} chars]" + ) return value if isinstance(value, dict): @@ -297,7 +300,10 @@ class ToolCrusher(Transform): elif isinstance(value, str): if len(value) > max_string_length: - return value[:max_string_length] + f"...[truncated {len(value) - max_string_length} chars]" + return ( + value[:max_string_length] + + f"...[truncated {len(value) - max_string_length} chars]" + ) return value else: diff --git a/tests/conftest.py b/tests/conftest.py index 7d23b2490..eaec1d969 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,11 +1,13 @@ """Shared pytest fixtures for Headroom tests.""" import json -import pytest import tempfile from datetime import datetime from pathlib import Path -from unittest.mock import Mock, MagicMock +from unittest.mock import Mock + +import pytest + # Sample messages fixtures @pytest.fixture @@ -17,6 +19,7 @@ def sample_messages(): {"role": "assistant", "content": "I'm doing well, thank you!"}, ] + @pytest.fixture def sample_messages_with_tools(): """Conversation with tool calls and responses.""" @@ -30,28 +33,34 @@ def sample_messages_with_tools(): { "id": "call_123", "type": "function", - "function": { - "name": "search_user", - "arguments": '{"user_id": "12345"}' - } + "function": {"name": "search_user", "arguments": '{"user_id": "12345"}'}, } - ] + ], }, { "role": "tool", "tool_call_id": "call_123", - "content": '{"id": "12345", "name": "Alice", "email": "alice@example.com"}' + "content": '{"id": "12345", "name": "Alice", "email": "alice@example.com"}', }, {"role": "assistant", "content": "I found user Alice with ID 12345."}, ] + @pytest.fixture def sample_tool_output_large(): """Large tool output for compression testing (100 items).""" - return json.dumps([ - {"id": i, "name": f"Item {i}", "score": i * 0.1, "status": "active" if i % 2 == 0 else "inactive"} - for i in range(100) - ]) + return json.dumps( + [ + { + "id": i, + "name": f"Item {i}", + "score": i * 0.1, + "status": "active" if i % 2 == 0 else "inactive", + } + for i in range(100) + ] + ) + @pytest.fixture def sample_tool_output_with_errors(): @@ -61,11 +70,13 @@ def sample_tool_output_with_errors(): items[15] = {"id": 15, "status": "failed", "exception": "TimeoutError"} return json.dumps(items) + @pytest.fixture def sample_system_prompt_with_date(): """System prompt containing dynamic date.""" return "You are a helpful assistant. Current date: 2025-01-06. Help the user with their tasks." + @pytest.fixture def sample_anthropic_messages(): """Anthropic-style messages with content blocks.""" @@ -74,11 +85,15 @@ def sample_anthropic_messages(): "role": "user", "content": [ {"type": "text", "text": "Analyze this image"}, - {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": "..."}} - ] + { + "type": "image", + "source": {"type": "base64", "media_type": "image/png", "data": "..."}, + }, + ], } ] + # Mock client fixtures @pytest.fixture def mock_openai_response(): @@ -97,6 +112,7 @@ def mock_openai_response(): mock.choices[0].finish_reason = "stop" return mock + @pytest.fixture def mock_openai_client(mock_openai_response): """Mock OpenAI client.""" @@ -106,6 +122,7 @@ def mock_openai_client(mock_openai_response): client.chat.completions.create = Mock(return_value=mock_openai_response) return client + # Storage fixtures @pytest.fixture def temp_sqlite_db(): @@ -114,6 +131,7 @@ def temp_sqlite_db(): yield f.name Path(f.name).unlink(missing_ok=True) + @pytest.fixture def temp_jsonl_file(): """Temporary JSONL file path.""" @@ -121,30 +139,38 @@ def temp_jsonl_file(): yield f.name Path(f.name).unlink(missing_ok=True) + # Provider fixtures @pytest.fixture def openai_provider(): """OpenAI provider instance.""" from headroom.providers.openai import OpenAIProvider + return OpenAIProvider() + @pytest.fixture def openai_tokenizer(): """OpenAI token counter for gpt-4o.""" from headroom.providers.openai import OpenAITokenCounter + return OpenAITokenCounter("gpt-4o") + # Config fixtures @pytest.fixture def default_config(): """Default HeadroomConfig.""" from headroom.config import HeadroomConfig + return HeadroomConfig() + @pytest.fixture def smart_crusher_config(): """SmartCrusher config for testing.""" from headroom.config import SmartCrusherConfig + return SmartCrusherConfig( enabled=True, min_items_to_analyze=3, @@ -152,11 +178,13 @@ def smart_crusher_config(): max_items_after_crush=10, ) + # Helper for creating RequestMetrics @pytest.fixture def sample_request_metrics(): """Sample RequestMetrics for storage tests.""" from headroom.config import RequestMetrics + return RequestMetrics( request_id="test-123", timestamp=datetime(2025, 1, 6, 12, 0, 0), diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index 0e820034c..6a4d3e680 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -11,10 +11,9 @@ These are the 4 required acceptance tests from the spec: import pytest from headroom import OpenAIProvider, Tokenizer -from headroom.transforms import CacheAligner, RollingWindow, ToolCrusher +from headroom.transforms import CacheAligner, RollingWindow from headroom.transforms.tool_crusher import crush_tool_output - # Create a shared provider for tests _provider = OpenAIProvider() @@ -273,10 +272,26 @@ class TestToolOrphan: "role": "assistant", "content": None, "tool_calls": [ - {"id": "call_1", "type": "function", "function": {"name": "search_web", "arguments": '{"q": "a"}'}}, - {"id": "call_2", "type": "function", "function": {"name": "search_files", "arguments": '{"q": "b"}'}}, - {"id": "call_3", "type": "function", "function": {"name": "search_db", "arguments": '{"q": "c"}'}}, - {"id": "call_4", "type": "function", "function": {"name": "search_api", "arguments": '{"q": "d"}'}}, + { + "id": "call_1", + "type": "function", + "function": {"name": "search_web", "arguments": '{"q": "a"}'}, + }, + { + "id": "call_2", + "type": "function", + "function": {"name": "search_files", "arguments": '{"q": "b"}'}, + }, + { + "id": "call_3", + "type": "function", + "function": {"name": "search_db", "arguments": '{"q": "c"}'}, + }, + { + "id": "call_4", + "type": "function", + "function": {"name": "search_api", "arguments": '{"q": "d"}'}, + }, ], }, {"role": "tool", "tool_call_id": "call_1", "content": '{"results": ["web_result"]}'}, @@ -331,7 +346,9 @@ class TestStreaming: class MockChunk: def __init__(self, content: str): - self.choices = [type("Choice", (), {"delta": type("Delta", (), {"content": content})()})] + self.choices = [ + type("Choice", (), {"delta": type("Delta", (), {"content": content})()}) + ] class MockStream: def __init__(self): @@ -428,12 +445,13 @@ class TestQueryAnchorExtraction: def test_preserves_needle_by_name(self): """If user asks for 'Alice', item with Alice should be preserved.""" + import json + from headroom.transforms.smart_crusher import ( SmartCrusher, SmartCrusherConfig, extract_query_anchors, ) - import json # User is searching for 'Alice' messages = [ @@ -443,16 +461,20 @@ class TestQueryAnchorExtraction: "role": "assistant", "content": None, "tool_calls": [ - {"id": "call_1", "type": "function", "function": {"name": "find_users", "arguments": '{"name": "Alice"}'}} + { + "id": "call_1", + "type": "function", + "function": {"name": "find_users", "arguments": '{"name": "Alice"}'}, + } ], }, { "role": "tool", "tool_call_id": "call_1", - "content": json.dumps([ - {"id": i, "name": f"User{i}", "score": 0.1} - for i in range(50) - ] + [{"id": 42, "name": "Alice", "score": 0.1}]) # Alice is at the END, not in first/last K + "content": json.dumps( + [{"id": i, "name": f"User{i}", "score": 0.1} for i in range(50)] + + [{"id": 42, "name": "Alice", "score": 0.1}] + ), # Alice is at the END, not in first/last K }, ] @@ -481,12 +503,13 @@ class TestQueryAnchorExtraction: def test_preserves_needle_by_uuid(self): """If user asks for a UUID, item with that UUID should be preserved.""" + import json + from headroom.transforms.smart_crusher import ( SmartCrusher, SmartCrusherConfig, extract_query_anchors, ) - import json target_uuid = "550e8400-e29b-41d4-a716-446655440000" @@ -497,16 +520,20 @@ class TestQueryAnchorExtraction: "role": "assistant", "content": None, "tool_calls": [ - {"id": "call_1", "type": "function", "function": {"name": "get_requests", "arguments": "{}"}} + { + "id": "call_1", + "type": "function", + "function": {"name": "get_requests", "arguments": "{}"}, + } ], }, { "role": "tool", "tool_call_id": "call_1", - "content": json.dumps([ - {"request_id": f"other-{i}", "status": "ok"} - for i in range(50) - ] + [{"request_id": target_uuid, "status": "ok"}]) # Target at end + "content": json.dumps( + [{"request_id": f"other-{i}", "status": "ok"} for i in range(50)] + + [{"request_id": target_uuid, "status": "ok"}] + ), # Target at end }, ] diff --git a/tests/test_cache/test_anthropic.py b/tests/test_cache/test_anthropic.py index 81cdcb126..47d941985 100644 --- a/tests/test_cache/test_anthropic.py +++ b/tests/test_cache/test_anthropic.py @@ -1,12 +1,13 @@ """Tests for AnthropicCacheOptimizer.""" import pytest + from headroom.cache import ( AnthropicCacheOptimizer, CacheConfig, OptimizationContext, ) -from headroom.cache.base import CacheStrategy, BreakpointLocation +from headroom.cache.base import CacheStrategy class TestAnthropicCacheOptimizer: @@ -70,9 +71,7 @@ class TestAnthropicCacheOptimizer: system_content = result.messages[0]["content"] if isinstance(system_content, list): has_cache_control = any( - "cache_control" in block - for block in system_content - if isinstance(block, dict) + "cache_control" in block for block in system_content if isinstance(block, dict) ) assert has_cache_control @@ -89,7 +88,10 @@ class TestAnthropicCacheOptimizer: result = optimizer.optimize(messages, context) # Dates should be moved to end - assert "extracted_dates" in result.transforms_applied or result.metrics.breakpoints_inserted >= 0 + assert ( + "extracted_dates" in result.transforms_applied + or result.metrics.breakpoints_inserted >= 0 + ) def test_optimize_disabled(self, context): """Test optimization when disabled.""" @@ -134,9 +136,7 @@ class TestAnthropicCacheOptimizer: messages = [ { "role": "system", - "content": [ - {"type": "text", "text": "You are a helpful assistant. " * 500} - ], + "content": [{"type": "text", "text": "You are a helpful assistant. " * 500}], }, {"role": "user", "content": "Hello!"}, ] diff --git a/tests/test_cache/test_base.py b/tests/test_cache/test_base.py index b2d04d49c..219b8aa46 100644 --- a/tests/test_cache/test_base.py +++ b/tests/test_cache/test_base.py @@ -1,13 +1,12 @@ """Tests for cache base types and interfaces.""" -import pytest from headroom.cache.base import ( - CacheStrategy, BreakpointLocation, CacheBreakpoint, CacheConfig, CacheMetrics, CacheResult, + CacheStrategy, OptimizationContext, ) diff --git a/tests/test_cache/test_client_integration.py b/tests/test_cache/test_client_integration.py index d5fa7606a..c5a3963ef 100644 --- a/tests/test_cache/test_client_integration.py +++ b/tests/test_cache/test_client_integration.py @@ -1,15 +1,15 @@ """Tests for HeadroomClient cache optimizer integration.""" -import pytest -import tempfile import os -from unittest.mock import MagicMock, patch +import tempfile +from unittest.mock import MagicMock + +import pytest + from headroom import ( - HeadroomClient, AnthropicCacheOptimizer, - CacheOptimizerRegistry, + HeadroomClient, ) -from headroom.providers import AnthropicProvider, OpenAIProvider @pytest.fixture diff --git a/tests/test_cache/test_dynamic_detector.py b/tests/test_cache/test_dynamic_detector.py index 2617f3d8e..06d598d16 100644 --- a/tests/test_cache/test_dynamic_detector.py +++ b/tests/test_cache/test_dynamic_detector.py @@ -1,12 +1,12 @@ """Tests for the dynamic content detector.""" import pytest + from headroom.cache.dynamic_detector import ( DetectionResult, DetectorConfig, DynamicCategory, DynamicContentDetector, - DynamicSpan, RegexDetector, detect_dynamic_content, ) @@ -118,7 +118,7 @@ class TestRegexDetector: content = "Date: 2024-01-15" spans = detector.detect(content) assert len(spans) == 1 - assert content[spans[0].start:spans[0].end] == spans[0].text + assert content[spans[0].start : spans[0].end] == spans[0].text class TestDynamicContentDetector: @@ -221,7 +221,7 @@ Request ID: req_xyz789abc123def456ghi""" config = DetectorConfig(tiers=["regex", "ner", "semantic"]) detector = DynamicContentDetector(config) - result = detector.detect("Test content") + detector.detect("Test content") # If NER/semantic not installed, should have warnings # (This test passes either way - it's informational) @@ -366,7 +366,11 @@ Your task is to help users with coding questions.""" assert "2024" not in result.static_content or "January" in result.static_content # Dynamic content should have the dates - assert "January" in result.dynamic_content or "2024-01-15" in result.dynamic_content or "10:30" in result.dynamic_content + assert ( + "January" in result.dynamic_content + or "2024-01-15" in result.dynamic_content + or "10:30" in result.dynamic_content + ) def test_request_metadata(self): """Test extracting request metadata.""" @@ -381,7 +385,7 @@ Process the following query:""" result = detector.detect(content) # Should find request ID, UUID, timestamp - categories = {s.category for s in result.spans} + {s.category for s in result.spans} assert len(result.spans) >= 2 def test_mixed_static_dynamic(self): @@ -410,7 +414,7 @@ class TestNERDetector: @pytest.fixture def ner_detector(self): """Create detector with NER enabled.""" - from headroom.cache.dynamic_detector import NERDetector, _SPACY_AVAILABLE + from headroom.cache.dynamic_detector import _SPACY_AVAILABLE, NERDetector if not _SPACY_AVAILABLE: pytest.skip("spaCy not installed") @@ -427,7 +431,7 @@ class TestNERDetector: """Test detecting person names.""" spans, _ = ner_detector.detect("John Smith sent the message.") - person_spans = [s for s in spans if s.category == DynamicCategory.PERSON] + [s for s in spans if s.category == DynamicCategory.PERSON] # NER might or might not detect "John Smith" depending on model # This is more of an integration test @@ -435,7 +439,7 @@ class TestNERDetector: """Test detecting money amounts.""" spans, _ = ner_detector.detect("The total is $500.00") - money_spans = [s for s in spans if s.category == DynamicCategory.MONEY] + [s for s in spans if s.category == DynamicCategory.MONEY] # May or may not detect depending on spaCy model @@ -445,7 +449,10 @@ class TestSemanticDetector: @pytest.fixture def semantic_detector(self): """Create detector with semantic enabled.""" - from headroom.cache.dynamic_detector import SemanticDetector, _SENTENCE_TRANSFORMERS_AVAILABLE + from headroom.cache.dynamic_detector import ( + _SENTENCE_TRANSFORMERS_AVAILABLE, + SemanticDetector, + ) if not _SENTENCE_TRANSFORMERS_AVAILABLE: pytest.skip("sentence-transformers not installed") diff --git a/tests/test_cache/test_google.py b/tests/test_cache/test_google.py index 0e9e5489b..2114c92db 100644 --- a/tests/test_cache/test_google.py +++ b/tests/test_cache/test_google.py @@ -1,14 +1,16 @@ """Tests for GoogleCacheOptimizer.""" -import pytest from datetime import datetime, timedelta -from headroom.cache import GoogleCacheOptimizer, CacheConfig, OptimizationContext + +import pytest + +from headroom.cache import CacheConfig, GoogleCacheOptimizer, OptimizationContext from headroom.cache.base import CacheStrategy from headroom.cache.google import ( - GOOGLE_MIN_CACHE_TOKENS, GOOGLE_CACHE_DISCOUNT, - CachedContentInfo, + GOOGLE_MIN_CACHE_TOKENS, CacheabilityAnalysis, + CachedContentInfo, ) diff --git a/tests/test_cache/test_openai.py b/tests/test_cache/test_openai.py index 7e26b646e..24559d502 100644 --- a/tests/test_cache/test_openai.py +++ b/tests/test_cache/test_openai.py @@ -1,7 +1,8 @@ """Tests for OpenAICacheOptimizer.""" import pytest -from headroom.cache import OpenAICacheOptimizer, CacheConfig, OptimizationContext + +from headroom.cache import CacheConfig, OpenAICacheOptimizer, OptimizationContext from headroom.cache.base import CacheStrategy @@ -94,7 +95,7 @@ class TestOpenAICacheOptimizer: ] # First call - result1 = optimizer.optimize(messages, context) + optimizer.optimize(messages, context) # Second call with same messages result2 = optimizer.optimize(messages, context) @@ -114,7 +115,7 @@ class TestOpenAICacheOptimizer: {"role": "user", "content": "Hello!"}, ] - result1 = optimizer.optimize(messages1, context) + optimizer.optimize(messages1, context) result2 = optimizer.optimize(messages2, context) # Second call should detect prefix change diff --git a/tests/test_cache/test_registry.py b/tests/test_cache/test_registry.py index c383e325a..f0af96a74 100644 --- a/tests/test_cache/test_registry.py +++ b/tests/test_cache/test_registry.py @@ -1,14 +1,15 @@ """Tests for CacheOptimizerRegistry.""" import pytest + from headroom.cache import ( - CacheOptimizerRegistry, AnthropicCacheOptimizer, - OpenAICacheOptimizer, - GoogleCacheOptimizer, CacheConfig, + CacheOptimizerRegistry, + GoogleCacheOptimizer, + OpenAICacheOptimizer, ) -from headroom.cache.base import BaseCacheOptimizer, CacheStrategy, CacheResult, OptimizationContext +from headroom.cache.base import BaseCacheOptimizer, CacheResult, CacheStrategy class MockOptimizer(BaseCacheOptimizer): diff --git a/tests/test_cache/test_semantic.py b/tests/test_cache/test_semantic.py index 3cda666bd..dc8b11d91 100644 --- a/tests/test_cache/test_semantic.py +++ b/tests/test_cache/test_semantic.py @@ -1,14 +1,16 @@ """Tests for SemanticCache and SemanticCacheLayer.""" -import pytest import time + +import pytest + from headroom.cache import ( - SemanticCacheLayer, - SemanticCache, AnthropicCacheOptimizer, OptimizationContext, + SemanticCache, + SemanticCacheLayer, ) -from headroom.cache.semantic import SemanticCacheConfig, CacheEntry +from headroom.cache.semantic import SemanticCacheConfig class TestSemanticCacheConfig: diff --git a/tests/test_ccr.py b/tests/test_ccr.py index 46f4877c7..d11454a0c 100644 --- a/tests/test_ccr.py +++ b/tests/test_ccr.py @@ -10,20 +10,19 @@ These tests verify that: import json import time + import pytest + from headroom.cache.compression_store import ( CompressionStore, - CompressionEntry, - RetrievalEvent, get_compression_store, reset_compression_store, ) +from headroom.config import CCRConfig from headroom.transforms.smart_crusher import ( - SmartCrusher, SmartCrusherConfig, smart_crush_tool_output, ) -from headroom.config import CCRConfig class TestCompressionStore: @@ -52,7 +51,9 @@ class TestCompressionStore: compressed_item_count=10, ) - assert len(hash_key) == 24 # SHA256 truncated to 24 chars (96 bits for collision resistance) + assert ( + len(hash_key) == 24 + ) # SHA256 truncated to 24 chars (96 bits for collision resistance) entry = store.retrieve(hash_key) assert entry is not None @@ -246,10 +247,7 @@ class TestSmartCrusherCCRIntegration: def test_compression_caches_original(self): """SmartCrusher caches original content when compressing.""" - items = [ - {"id": i, "score": 100 - i, "data": f"item_{i}"} - for i in range(100) - ] + items = [{"id": i, "score": 100 - i, "data": f"item_{i}"} for i in range(100)] content = json.dumps(items) config = SmartCrusherConfig(max_items_after_crush=15) @@ -259,9 +257,7 @@ class TestSmartCrusherCCRIntegration: min_items_to_cache=10, ) - compressed_str, was_modified, _ = smart_crush_tool_output( - content, config, ccr_config - ) + compressed_str, was_modified, _ = smart_crush_tool_output(content, config, ccr_config) assert was_modified @@ -272,10 +268,7 @@ class TestSmartCrusherCCRIntegration: def test_retrieval_marker_injected(self): """CCR marker is injected when configured.""" - items = [ - {"id": i, "score": 100 - i, "data": f"item_{i}"} - for i in range(100) - ] + items = [{"id": i, "score": 100 - i, "data": f"item_{i}"} for i in range(100)] content = json.dumps(items) config = SmartCrusherConfig(max_items_after_crush=15) @@ -285,9 +278,7 @@ class TestSmartCrusherCCRIntegration: min_items_to_cache=10, ) - compressed_str, was_modified, _ = smart_crush_tool_output( - content, config, ccr_config - ) + compressed_str, was_modified, _ = smart_crush_tool_output(content, config, ccr_config) assert was_modified # Marker should be present @@ -314,18 +305,13 @@ class TestSmartCrusherCCRIntegration: def test_uncrushed_data_not_cached(self): """Data that doesn't get crushed is not cached.""" # DB results with unique IDs - shouldn't be crushed - items = [ - {"id": i, "name": f"User {i}", "email": f"user{i}@test.com"} - for i in range(30) - ] + items = [{"id": i, "name": f"User {i}", "email": f"user{i}@test.com"} for i in range(30)] content = json.dumps(items) config = SmartCrusherConfig(max_items_after_crush=10) ccr_config = CCRConfig(enabled=True, min_items_to_cache=10) - compressed_str, was_modified, _ = smart_crush_tool_output( - content, config, ccr_config - ) + compressed_str, was_modified, _ = smart_crush_tool_output(content, config, ccr_config) # If not modified, shouldn't be cached if not was_modified: @@ -336,8 +322,7 @@ class TestSmartCrusherCCRIntegration: def test_can_retrieve_after_compression(self): """Can retrieve original content after compression.""" items = [ - {"id": i, "score": 100 - i, "content": f"Document about topic {i}"} - for i in range(100) + {"id": i, "score": 100 - i, "content": f"Document about topic {i}"} for i in range(100) ] content = json.dumps(items) @@ -348,16 +333,15 @@ class TestSmartCrusherCCRIntegration: min_items_to_cache=10, ) - compressed_str, was_modified, _ = smart_crush_tool_output( - content, config, ccr_config - ) + compressed_str, was_modified, _ = smart_crush_tool_output(content, config, ccr_config) assert was_modified # Extract hash from marker # Marker format: [100 items compressed to 15. Retrieve more: hash=abc123...] import re - match = re.search(r'hash=([a-f0-9]+)', compressed_str) + + match = re.search(r"hash=([a-f0-9]+)", compressed_str) assert match is not None, f"No hash found in: {compressed_str}" hash_key = match.group(1) @@ -376,10 +360,7 @@ class TestSmartCrusherCCRIntegration: {"id": 1, "content": "Authentication error: invalid token"}, {"id": 2, "content": "Database connection successful"}, {"id": 3, "content": "User login completed"}, - ] + [ - {"id": i, "content": f"Generic log entry {i}"} - for i in range(4, 104) - ] + ] + [{"id": i, "content": f"Generic log entry {i}"} for i in range(4, 104)] content = json.dumps(items) config = SmartCrusherConfig(max_items_after_crush=15) @@ -389,15 +370,14 @@ class TestSmartCrusherCCRIntegration: min_items_to_cache=10, ) - compressed_str, was_modified, _ = smart_crush_tool_output( - content, config, ccr_config - ) + compressed_str, was_modified, _ = smart_crush_tool_output(content, config, ccr_config) assert was_modified # Extract hash import re - match = re.search(r'hash=([a-f0-9]+)', compressed_str) + + match = re.search(r"hash=([a-f0-9]+)", compressed_str) hash_key = match.group(1) # Search for authentication items @@ -424,10 +404,7 @@ class TestCCRConfig: def test_custom_marker_template(self): """Custom marker template is used.""" - items = [ - {"id": i, "score": 100 - i} - for i in range(100) - ] + items = [{"id": i, "score": 100 - i} for i in range(100)] content = json.dumps(items) config = SmartCrusherConfig(max_items_after_crush=15) @@ -438,9 +415,7 @@ class TestCCRConfig: marker_template="\n[CUSTOM: {original_count} -> {compressed_count}, key={hash}]", ) - compressed_str, was_modified, _ = smart_crush_tool_output( - content, config, ccr_config - ) + compressed_str, was_modified, _ = smart_crush_tool_output(content, config, ccr_config) if was_modified: assert "CUSTOM:" in compressed_str or "key=" in compressed_str @@ -595,10 +570,7 @@ class TestCCREdgeCases: """When CCR disabled, no caching occurs.""" reset_compression_store() - items = [ - {"id": i, "score": 100 - i} - for i in range(100) - ] + items = [{"id": i, "score": 100 - i} for i in range(100)] content = json.dumps(items) config = SmartCrusherConfig(max_items_after_crush=15) @@ -635,10 +607,7 @@ class TestCCREdgeCases: except Exception as e: errors.append(str(e)) - threads = [ - threading.Thread(target=store_and_retrieve, args=(i,)) - for i in range(20) - ] + threads = [threading.Thread(target=store_and_retrieve, args=(i,)) for i in range(20)] for t in threads: t.start() diff --git a/tests/test_ccr_feedback.py b/tests/test_ccr_feedback.py index 89821f8bb..96b53bd8b 100644 --- a/tests/test_ccr_feedback.py +++ b/tests/test_ccr_feedback.py @@ -1,11 +1,11 @@ """Tests for CCR feedback loop and pattern learning.""" import time + import pytest from headroom.cache.compression_feedback import ( CompressionFeedback, - CompressionHints, LocalToolPattern, get_compression_feedback, reset_compression_feedback, diff --git a/tests/test_ccr_tool_injection.py b/tests/test_ccr_tool_injection.py index 862d3be27..40c2558db 100644 --- a/tests/test_ccr_tool_injection.py +++ b/tests/test_ccr_tool_injection.py @@ -1,7 +1,6 @@ """Tests for CCR tool injection and MCP integration.""" import json -import pytest from headroom.ccr import ( CCR_TOOL_NAME, @@ -250,9 +249,7 @@ class TestCCRToolInjector: inject_tool=True, inject_system_instructions=True, ) - updated_messages, updated_tools, was_injected = injector.process_request( - messages, None - ) + updated_messages, updated_tools, was_injected = injector.process_request(messages, None) assert was_injected assert updated_tools is not None diff --git a/tests/test_config.py b/tests/test_config.py index 1e1c00ee3..3e95fc916 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -12,8 +12,6 @@ Tests all configuration dataclasses, enums, and utility classes: from dataclasses import fields from datetime import datetime -import pytest - from headroom.config import ( Block, CacheAlignerConfig, @@ -220,17 +218,13 @@ class TestHeadroomConfig: def test_get_context_limit_direct_match(self): """get_context_limit returns limit for exact model match.""" - config = HeadroomConfig( - model_context_limits={"gpt-4o": 128000, "claude-3-opus": 200000} - ) + config = HeadroomConfig(model_context_limits={"gpt-4o": 128000, "claude-3-opus": 200000}) assert config.get_context_limit("gpt-4o") == 128000 assert config.get_context_limit("claude-3-opus") == 200000 def test_get_context_limit_prefix_match(self): """get_context_limit returns limit for prefix match.""" - config = HeadroomConfig( - model_context_limits={"gpt-4": 128000, "claude-3": 200000} - ) + config = HeadroomConfig(model_context_limits={"gpt-4": 128000, "claude-3": 200000}) # Prefix matches assert config.get_context_limit("gpt-4-turbo") == 128000 assert config.get_context_limit("gpt-4o") == 128000 diff --git a/tests/test_critical_fixes.py b/tests/test_critical_fixes.py index 21c858382..aed2ae067 100644 --- a/tests/test_critical_fixes.py +++ b/tests/test_critical_fixes.py @@ -8,9 +8,8 @@ These tests verify the before/after behavior of critical bug fixes: 5. SmartCrusher integration with TOIN """ -import threading import time -from unittest.mock import MagicMock, patch +from unittest.mock import patch import pytest @@ -31,7 +30,12 @@ class TestTOINConfidenceMathFix: def test_confidence_user_boost_at_3_users(self): """With 3 users (min for network effect), boost should be meaningful.""" - from headroom.telemetry.toin import ToolIntelligenceNetwork, TOINConfig, reset_toin, ToolPattern + from headroom.telemetry.toin import ( + TOINConfig, + ToolIntelligenceNetwork, + ToolPattern, + reset_toin, + ) reset_toin() config = TOINConfig(min_users_for_network_effect=3) @@ -52,11 +56,18 @@ class TestTOINConfidenceMathFix: # BUG: With user_count * 0.01: boost = 0.03, total = 0.73 # After fix, confidence should be at least 0.75 - assert confidence >= 0.75, f"Confidence {confidence} too low for 3 users - user boost not meaningful" + assert confidence >= 0.75, ( + f"Confidence {confidence} too low for 3 users - user boost not meaningful" + ) def test_confidence_user_boost_at_10_users(self): """With 10 users, boost should hit or approach cap.""" - from headroom.telemetry.toin import ToolIntelligenceNetwork, TOINConfig, reset_toin, ToolPattern + from headroom.telemetry.toin import ( + TOINConfig, + ToolIntelligenceNetwork, + ToolPattern, + reset_toin, + ) reset_toin() config = TOINConfig(min_users_for_network_effect=3) @@ -92,8 +103,8 @@ class TestTOINDoubleCountFix: def test_user_count_no_double_counting_after_cap(self): """Same instance shouldn't be counted twice even after cap hit.""" - from headroom.telemetry.toin import ToolIntelligenceNetwork, TOINConfig, reset_toin from headroom.telemetry.models import ToolSignature + from headroom.telemetry.toin import TOINConfig, ToolIntelligenceNetwork, reset_toin reset_toin() toin = ToolIntelligenceNetwork(TOINConfig()) @@ -149,7 +160,10 @@ class TestCompressionFeedbackRaceCondition: def test_analyze_from_store_thread_safety(self): """Concurrent analyze_from_store and record_retrieval should not lose events.""" - from headroom.cache.compression_feedback import CompressionFeedback, reset_compression_feedback + from headroom.cache.compression_feedback import ( + CompressionFeedback, + reset_compression_feedback, + ) from headroom.cache.compression_store import CompressionStore, RetrievalEvent reset_compression_feedback() @@ -191,7 +205,10 @@ class TestCompressionFeedbackRaceCondition: def test_timestamp_filtering_inside_lock(self): """Verify that timestamp filtering happens atomically with update.""" - from headroom.cache.compression_feedback import CompressionFeedback, reset_compression_feedback + from headroom.cache.compression_feedback import ( + CompressionFeedback, + reset_compression_feedback, + ) from headroom.cache.compression_store import CompressionStore, RetrievalEvent reset_compression_feedback() @@ -204,21 +221,36 @@ class TestCompressionFeedbackRaceCondition: # Create mock store with events (correct API) mock_events = [ RetrievalEvent( - hash="h1", query=None, items_retrieved=5, total_items=50, - tool_name="tool_a", timestamp=99.0, retrieval_type="full", + hash="h1", + query=None, + items_retrieved=5, + total_items=50, + tool_name="tool_a", + timestamp=99.0, + retrieval_type="full", ), RetrievalEvent( - hash="h2", query=None, items_retrieved=5, total_items=50, - tool_name="tool_b", timestamp=101.0, retrieval_type="full", + hash="h2", + query=None, + items_retrieved=5, + total_items=50, + tool_name="tool_b", + timestamp=101.0, + retrieval_type="full", ), RetrievalEvent( - hash="h3", query="test", items_retrieved=5, total_items=50, - tool_name="tool_c", timestamp=102.0, retrieval_type="search", + hash="h3", + query="test", + items_retrieved=5, + total_items=50, + tool_name="tool_c", + timestamp=102.0, + retrieval_type="search", ), ] # Mock store.get_retrieval_events - with patch.object(store, 'get_retrieval_events', return_value=mock_events): + with patch.object(store, "get_retrieval_events", return_value=mock_events): feedback.analyze_from_store() # Only events with timestamp > 100.0 should be processed (h2, h3) @@ -242,7 +274,10 @@ class TestUnboundedStrategyDicts: def test_strategy_dicts_have_size_limits(self): """Strategy dicts should be bounded to prevent memory leaks.""" - from headroom.cache.compression_feedback import CompressionFeedback, reset_compression_feedback + from headroom.cache.compression_feedback import ( + CompressionFeedback, + reset_compression_feedback, + ) from headroom.cache.compression_store import CompressionStore reset_compression_feedback() @@ -280,9 +315,9 @@ class TestSmartCrusherTOINIntegration: def test_smart_crusher_records_to_toin(self): """SmartCrusher should record compression events to TOIN.""" - from headroom.transforms.smart_crusher import SmartCrusher, SmartCrusherConfig - from headroom.telemetry.toin import get_toin, reset_toin from headroom.telemetry.models import ToolSignature + from headroom.telemetry.toin import get_toin, reset_toin + from headroom.transforms.smart_crusher import SmartCrusher, SmartCrusherConfig reset_toin() @@ -311,13 +346,17 @@ class TestSmartCrusherTOINIntegration: # Get TOIN instance and check initial state toin = get_toin() - initial_pattern_count = len(toin._patterns) + len(toin._patterns) # Crush the array - result, info, markers = crusher._crush_array(items, query_context="test query", tool_name="test_tool") + result, info, markers = crusher._crush_array( + items, query_context="test query", tool_name="test_tool" + ) # Verify compression happened (not skipped) - assert "skip" not in info.lower(), f"Compression was skipped: {info}. Test needs crushable data." + assert "skip" not in info.lower(), ( + f"Compression was skipped: {info}. Test needs crushable data." + ) # Get the signature that would have been created sig = ToolSignature.from_items(items) @@ -342,11 +381,13 @@ class TestAllFixesIntegrated: def test_full_feedback_loop(self): """Test complete feedback loop: compress -> store -> retrieve -> learn.""" - from headroom.transforms.smart_crusher import SmartCrusher, SmartCrusherConfig - from headroom.cache.compression_store import get_compression_store, reset_compression_store - from headroom.cache.compression_feedback import get_compression_feedback, reset_compression_feedback - from headroom.telemetry.toin import get_toin, reset_toin + from headroom.cache.compression_feedback import ( + reset_compression_feedback, + ) + from headroom.cache.compression_store import reset_compression_store from headroom.telemetry.models import ToolSignature + from headroom.telemetry.toin import get_toin, reset_toin + from headroom.transforms.smart_crusher import SmartCrusher, SmartCrusherConfig # Reset all singletons reset_toin() @@ -382,7 +423,9 @@ class TestAllFixesIntegrated: ) # Verify compression happened (not skipped) - assert "skip" not in info.lower(), f"Compression was skipped: {info}. Test needs crushable data." + assert "skip" not in info.lower(), ( + f"Compression was skipped: {info}. Test needs crushable data." + ) # Step 2: Check TOIN was notified (after fix) toin = get_toin() diff --git a/tests/test_critical_gaps.py b/tests/test_critical_gaps.py index fb1a341f3..5df5c2556 100644 --- a/tests/test_critical_gaps.py +++ b/tests/test_critical_gaps.py @@ -4,7 +4,6 @@ These tests demonstrate bugs BEFORE the fix and verify they're fixed AFTER. Each test documents the specific issue being addressed. """ -import copy import hashlib import json import tempfile @@ -17,12 +16,10 @@ import pytest from headroom.cache.compression_feedback import ( CompressionFeedback, - LocalToolPattern, get_compression_feedback, reset_compression_feedback, ) from headroom.cache.compression_store import ( - CompressionEntry, CompressionStore, RetrievalEvent, get_compression_store, @@ -244,7 +241,11 @@ class TestUserCountMergeLogic: imported = ToolPattern(tool_signature_hash="test_hash") for i in range(3): # User 0 overlaps with existing - h = hashlib.sha256(f"existing_{i}".encode()).hexdigest()[:8] if i == 0 else hashlib.sha256(f"imported_{i}".encode()).hexdigest()[:8] + h = ( + hashlib.sha256(f"existing_{i}".encode()).hexdigest()[:8] + if i == 0 + else hashlib.sha256(f"imported_{i}".encode()).hexdigest()[:8] + ) imported._all_seen_instances.add(h) imported._seen_instance_hashes.append(h) imported.user_count += 1 @@ -460,7 +461,7 @@ class TestLockOrderingDeadlockRisk: deadlock_detected = threading.Event() def toin_writer(): - for i in range(50): + for _i in range(50): if deadlock_detected.is_set(): break try: @@ -491,7 +492,7 @@ class TestLockOrderingDeadlockRisk: time.sleep(0.001) def feedback_reader(): - for i in range(50): + for _i in range(50): if deadlock_detected.is_set(): break try: @@ -511,6 +512,7 @@ class TestLockOrderingDeadlockRisk: # Wait with timeout import concurrent.futures + done, not_done = concurrent.futures.wait(futures, timeout=10) if not_done: @@ -546,7 +548,7 @@ class TestHighPriorityFixes: hashes.append(h) # Store 6th entry - should evict oldest - h6 = store.store( + store.store( original='[{"id": 6}]', compressed='[{"id": 6}]', ) @@ -640,7 +642,9 @@ class TestCriticalFixesIntegration: sig = ToolSignature.from_items([{"id": 1, "score": 0.9, "name": "test"}]) # Simulate compression workflow - original = json.dumps([{"id": i, "score": 0.9 - i*0.01, "name": f"item_{i}"} for i in range(100)]) + original = json.dumps( + [{"id": i, "score": 0.9 - i * 0.01, "name": f"item_{i}"} for i in range(100)] + ) compressed = json.dumps([{"id": 0, "score": 0.9, "name": "item_0"}]) # 1. Record compression in feedback @@ -679,11 +683,11 @@ class TestCriticalFixesIntegration: assert entry.original_item_count == 100 # 5. Search within cached data - results = store.search(hash_key, "item_50") + store.search(hash_key, "item_50") # Should find the item even though it was compressed away # 6. Get recommendation from TOIN - hint = toin.get_recommendation(sig, "find item_50") + toin.get_recommendation(sig, "find item_50") # 7. Verify stats are consistent toin_stats = toin.get_stats() @@ -974,7 +978,6 @@ class TestCompressionFeedbackHighPriorityFixes: HIGH: Without timestamp tracking, events could be processed multiple times. """ - from .test_ccr import TestCompressionStore as CCRTests feedback = CompressionFeedback(analysis_interval=0) # Allow immediate re-analysis @@ -984,8 +987,6 @@ class TestCompressionFeedbackHighPriorityFixes: # Manually set last_event_timestamp to simulate processed events # This ensures we don't double-count - initial_retrievals = feedback._total_retrievals - # Call analyze multiple times - should not double-count for _ in range(3): feedback.analyze_from_store() @@ -1081,8 +1082,8 @@ class TestMediumPriorityTOINFixes: def test_query_pattern_frequency_tracking(self): """MEDIUM FIX #10: Query patterns should be ranked by frequency, not just recency.""" - from headroom.telemetry.toin import ToolIntelligenceNetwork, TOINConfig from headroom.telemetry.models import ToolSignature + from headroom.telemetry.toin import TOINConfig, ToolIntelligenceNetwork toin = ToolIntelligenceNetwork(TOINConfig(enabled=True)) sig = ToolSignature.from_items([{"id": 1, "status": "active"}]) @@ -1135,8 +1136,8 @@ class TestMediumPriorityTOINFixes: def test_common_queries_bounded(self): """Verify common_queries list is bounded.""" - from headroom.telemetry.toin import ToolIntelligenceNetwork, TOINConfig from headroom.telemetry.models import ToolSignature + from headroom.telemetry.toin import TOINConfig, ToolIntelligenceNetwork toin = ToolIntelligenceNetwork(TOINConfig(enabled=True)) sig = ToolSignature.from_items([{"id": 1}]) @@ -1176,7 +1177,7 @@ class TestLowPriorityFixes: hash_key = store.store( original='[{"id": 1}]', - compressed='[1]', + compressed="[1]", original_item_count=1, compressed_item_count=1, tool_name="test", @@ -1187,6 +1188,7 @@ class TestLowPriorityFixes: # Wait for expiry import time + time.sleep(1.1) # Entry is expired, exists() returns False but does NOT delete @@ -1215,8 +1217,8 @@ class TestLowPriorityFixes: def test_toin_metrics_callback(self): """LOW FIX #22: TOIN should emit metrics via callback.""" - from headroom.telemetry.toin import ToolIntelligenceNetwork, TOINConfig from headroom.telemetry.models import ToolSignature + from headroom.telemetry.toin import TOINConfig, ToolIntelligenceNetwork metrics_events = [] @@ -1259,16 +1261,17 @@ class TestMediumPriorityCompressionStoreFixes: def test_eviction_heap_order_correct(self): """MEDIUM FIX #16: Eviction heap should evict oldest entries first.""" - from headroom.cache.compression_store import CompressionStore import time + from headroom.cache.compression_store import CompressionStore + # Small store to trigger eviction store = CompressionStore(max_entries=3) # Store entries with small delays to ensure different timestamps hash1 = store.store( original='[{"id": 1}]', - compressed='[1]', + compressed="[1]", original_item_count=1, compressed_item_count=1, tool_name="tool1", @@ -1277,7 +1280,7 @@ class TestMediumPriorityCompressionStoreFixes: hash2 = store.store( original='[{"id": 2}]', - compressed='[2]', + compressed="[2]", original_item_count=1, compressed_item_count=1, tool_name="tool2", @@ -1286,7 +1289,7 @@ class TestMediumPriorityCompressionStoreFixes: hash3 = store.store( original='[{"id": 3}]', - compressed='[3]', + compressed="[3]", original_item_count=1, compressed_item_count=1, tool_name="tool3", @@ -1299,7 +1302,7 @@ class TestMediumPriorityCompressionStoreFixes: # Add a 4th entry to trigger eviction hash4 = store.store( original='[{"id": 4}]', - compressed='[4]', + compressed="[4]", original_item_count=1, compressed_item_count=1, tool_name="tool4", diff --git a/tests/test_crushability.py b/tests/test_crushability.py index 26eaefc6a..e67ac428b 100644 --- a/tests/test_crushability.py +++ b/tests/test_crushability.py @@ -14,13 +14,13 @@ Test scenarios: """ import json + import pytest + from headroom.transforms.smart_crusher import ( - SmartCrusher, - SmartCrusherConfig, - SmartAnalyzer, CompressionStrategy, - CrushabilityAnalysis, + SmartAnalyzer, + SmartCrusherConfig, smart_crush_tool_output, ) @@ -92,8 +92,7 @@ class TestCrushabilityDetection: # Should detect score field as importance signal assert analysis.crushability is not None assert analysis.crushability.crushable, ( - f"Search results should be crushable. " - f"Reason: {analysis.crushability.reason}" + f"Search results should be crushable. Reason: {analysis.crushability.reason}" ) assert analysis.crushability.has_score_field assert any("score" in s for s in analysis.crushability.signals_present) @@ -121,7 +120,10 @@ class TestCrushabilityDetection: assert analysis.crushability is not None assert analysis.crushability.crushable # Now uses structural_outliers instead of keyword-based error count - assert any("structural_outliers" in s or "outlier" in s.lower() for s in analysis.crushability.signals_present) + assert any( + "structural_outliers" in s or "outlier" in s.lower() + for s in analysis.crushability.signals_present + ) def test_time_series_with_anomalies_crushable(self, analyzer): """Time series with numeric anomalies SHOULD be crushed.""" @@ -130,11 +132,13 @@ class TestCrushabilityDetection: value = 100.0 # Normal value if i in [25, 50, 75]: # Anomaly points value = 999.0 - items.append({ - "id": i, - "timestamp": i, - "cpu_usage": value, - }) + items.append( + { + "id": i, + "timestamp": i, + "cpu_usage": value, + } + ) analysis = analyzer.analyze_array(items) @@ -150,8 +154,8 @@ class TestCrushabilityDetection: { "id": i, "status": "success", # Same for all - "code": 200, # Same for all - "message": "OK", # Same for all + "code": 200, # Same for all + "message": "OK", # Same for all } for i in range(100) ] @@ -162,7 +166,10 @@ class TestCrushabilityDetection: assert analysis.crushability is not None assert analysis.crushability.crushable # Can be "low_uniqueness" or "repetitive_content_with_ids" - assert "low_uniqueness" in analysis.crushability.reason or "repetitive" in analysis.crushability.reason + assert ( + "low_uniqueness" in analysis.crushability.reason + or "repetitive" in analysis.crushability.reason + ) def test_file_listing_not_crushable(self, analyzer): """File listing with unique paths should NOT be crushed.""" @@ -208,10 +215,7 @@ class TestCrushabilityEndToEnd: def test_db_results_preserved_completely(self): """DB results should be returned unchanged when not crushable.""" - items = [ - {"id": i, "name": f"User {i}", "email": f"user{i}@test.com"} - for i in range(30) - ] + items = [{"id": i, "name": f"User {i}", "email": f"user{i}@test.com"} for i in range(30)] content = json.dumps(items) config = SmartCrusherConfig(max_items_after_crush=10) @@ -222,8 +226,7 @@ class TestCrushabilityEndToEnd: result = json.loads(crushed) # If it was modified, all items should still be there assert len(result) == 30, ( - f"DB results should not lose items! " - f"Had 30, got {len(result)}. Info: {info}" + f"DB results should not lose items! Had 30, got {len(result)}. Info: {info}" ) def test_search_results_crushed_by_score(self): @@ -294,9 +297,7 @@ class TestCrushabilitySignals: for field_name, items in test_cases: analysis = analyzer.analyze_array(items) assert analysis.crushability is not None - assert analysis.crushability.has_id_field, ( - f"Should detect '{field_name}' as ID field" - ) + assert analysis.crushability.has_id_field, f"Should detect '{field_name}' as ID field" def test_detects_score_field_variations(self, analyzer): """Should detect various score field naming patterns.""" @@ -379,10 +380,7 @@ class TestCrushabilityEdgeCases: However, since ALL items are errors, they will ALL be preserved due to the preservation guarantee. The end result is the same - no data loss. """ - items = [ - {"id": i, "error": f"Error {i}", "status": "failed"} - for i in range(50) - ] + items = [{"id": i, "error": f"Error {i}", "status": "failed"} for i in range(50)] analysis = analyzer.analyze_array(items) assert analysis.crushability is not None diff --git a/tests/test_integrations/test_langchain.py b/tests/test_integrations/test_langchain.py index 59c1a934a..a0d127111 100644 --- a/tests/test_integrations/test_langchain.py +++ b/tests/test_integrations/test_langchain.py @@ -8,9 +8,10 @@ Tests cover: """ import json -import pytest from datetime import datetime -from unittest.mock import MagicMock, patch, PropertyMock +from unittest.mock import MagicMock, patch + +import pytest # Check if LangChain is available try: @@ -21,6 +22,7 @@ try: ToolMessage, ) from langchain_core.outputs import ChatGeneration, ChatResult + LANGCHAIN_AVAILABLE = True except ImportError: LANGCHAIN_AVAILABLE = False @@ -28,10 +30,7 @@ except ImportError: from headroom import HeadroomConfig, HeadroomMode # Skip all tests if LangChain not installed -pytestmark = pytest.mark.skipif( - not LANGCHAIN_AVAILABLE, - reason="LangChain not installed" -) +pytestmark = pytest.mark.skipif(not LANGCHAIN_AVAILABLE, reason="LangChain not installed") @pytest.fixture @@ -50,13 +49,15 @@ def mock_chat_model(): message=AIMessage(content="Hello! I'm a mock response."), ) ], - llm_output={"token_usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}}, + llm_output={ + "token_usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15} + }, ) mock._generate = MagicMock(side_effect=mock_generate) - mock._stream = MagicMock(return_value=iter([ - ChatGeneration(message=AIMessage(content="Streaming...")) - ])) + mock._stream = MagicMock( + return_value=iter([ChatGeneration(message=AIMessage(content="Streaming..."))]) + ) return mock @@ -74,8 +75,7 @@ def sample_messages(): def large_tool_output(): """Large tool output that should trigger compression.""" items = [ - {"id": i, "name": f"Item {i}", "value": i * 100, "status": "active"} - for i in range(100) + {"id": i, "name": f"Item {i}", "value": i * 100, "status": "active"} for i in range(100) ] return json.dumps(items) @@ -86,11 +86,13 @@ class TestLangchainAvailable: def test_returns_bool(self): """langchain_available returns boolean.""" from headroom.integrations.langchain import langchain_available + assert isinstance(langchain_available(), bool) def test_returns_true_when_installed(self): """Returns True when LangChain is installed.""" from headroom.integrations.langchain import langchain_available + assert langchain_available() is True @@ -161,9 +163,7 @@ class TestHeadroomChatModel: HumanMessage(content="Get the weather"), AIMessage( content="I'll check the weather.", - tool_calls=[ - {"id": "call_123", "name": "get_weather", "args": {"city": "Paris"}} - ], + tool_calls=[{"id": "call_123", "name": "get_weather", "args": {"city": "Paris"}}], ), ToolMessage(content='{"temp": 20}', tool_call_id="call_123"), ] @@ -207,7 +207,7 @@ class TestHeadroomChatModel: _ = model.pipeline # Force lazy init # Mock the pipeline apply method - with patch.object(model._pipeline, 'apply') as mock_apply: + with patch.object(model._pipeline, "apply") as mock_apply: mock_result = MagicMock() mock_result.messages = [ {"role": "system", "content": "You are helpful."}, @@ -218,7 +218,7 @@ class TestHeadroomChatModel: mock_result.transforms_applied = ["cache_aligner"] mock_apply.return_value = mock_result - result = model._generate(sample_messages) + model._generate(sample_messages) # Verify pipeline.apply was called mock_apply.assert_called_once() @@ -234,7 +234,7 @@ class TestHeadroomChatModel: model = HeadroomChatModel(mock_chat_model) # Add 150 fake metrics - for i in range(150): + for _i in range(150): model._metrics_history.append(MagicMock()) # Simulate a call that trims @@ -444,9 +444,10 @@ class TestHeadroomRunnable: def test_as_runnable(self): """Convert to LangChain Runnable.""" - from headroom.integrations.langchain import HeadroomRunnable from langchain_core.runnables import RunnableLambda + from headroom.integrations.langchain import HeadroomRunnable + runnable = HeadroomRunnable() lc_runnable = runnable.as_runnable() @@ -463,7 +464,7 @@ class TestHeadroomRunnable: runnable._provider = OpenAIProvider() _ = runnable.pipeline # Force lazy init - with patch.object(runnable._pipeline, 'apply') as mock_apply: + with patch.object(runnable._pipeline, "apply") as mock_apply: mock_result = MagicMock() mock_result.messages = [ {"role": "system", "content": "You are helpful."}, @@ -487,7 +488,7 @@ class TestOptimizeMessages: """Basic message optimization.""" from headroom.integrations import optimize_messages - with patch('headroom.integrations.langchain.TransformPipeline') as MockPipeline: + with patch("headroom.integrations.langchain.TransformPipeline") as MockPipeline: mock_instance = MagicMock() mock_result = MagicMock() mock_result.messages = [ @@ -512,7 +513,7 @@ class TestOptimizeMessages: config = HeadroomConfig(default_mode=HeadroomMode.AUDIT) - with patch('headroom.integrations.langchain.TransformPipeline') as MockPipeline: + with patch("headroom.integrations.langchain.TransformPipeline") as MockPipeline: mock_instance = MagicMock() mock_result = MagicMock() mock_result.messages = [] @@ -546,14 +547,22 @@ class TestOptimizeMessages: ToolMessage(content="Sunny", tool_call_id="1"), ] - with patch('headroom.integrations.langchain.TransformPipeline') as MockPipeline: + with patch("headroom.integrations.langchain.TransformPipeline") as MockPipeline: mock_instance = MagicMock() mock_result = MagicMock() mock_result.messages = [ {"role": "user", "content": "Get weather"}, - {"role": "assistant", "content": "Checking...", "tool_calls": [ - {"id": "1", "type": "function", "function": {"name": "weather", "arguments": "{}"}} - ]}, + { + "role": "assistant", + "content": "Checking...", + "tool_calls": [ + { + "id": "1", + "type": "function", + "function": {"name": "weather", "arguments": "{}"}, + } + ], + }, {"role": "tool", "tool_call_id": "1", "content": "Sunny"}, ] mock_result.tokens_before = 100 @@ -583,7 +592,9 @@ class TestIntegrationWithRealHeadroom: # Should return valid messages assert len(optimized) >= 1 - assert all(isinstance(m, (SystemMessage, HumanMessage, AIMessage, ToolMessage)) for m in optimized) + assert all( + isinstance(m, (SystemMessage, HumanMessage, AIMessage, ToolMessage)) for m in optimized + ) # Metrics should be populated assert "tokens_before" in metrics diff --git a/tests/test_integrations/test_langchain_evals.py b/tests/test_integrations/test_langchain_evals.py index 34879e8d9..8da5862bf 100644 --- a/tests/test_integrations/test_langchain_evals.py +++ b/tests/test_integrations/test_langchain_evals.py @@ -15,9 +15,9 @@ from datetime import datetime, timedelta import pytest -from headroom.config import SmartCrusherConfig, RelevanceScorerConfig -from headroom.transforms import SmartCrusher +from headroom.config import SmartCrusherConfig from headroom.providers import OpenAIProvider +from headroom.transforms import SmartCrusher # Test fixtures for realistic data @@ -44,18 +44,20 @@ def generate_log_entries(count: int, error_rate: float = 0.15) -> list[dict]: entries = [] levels = ["DEBUG", "INFO", "INFO", "INFO", "WARN"] # Base levels (no ERROR) - for i in range(count): + for _i in range(count): timestamp = datetime.now() - timedelta(minutes=random.randint(1, 1440)) # Force specific error rate if random.random() < error_rate: level = "ERROR" - message = random.choice([ - "Connection refused to db: timeout after 30s", - "Failed to process request: NullPointerException", - "Authentication failed for user: invalid token", - "Rate limit exceeded: 429 Too Many Requests", - ]) + message = random.choice( + [ + "Connection refused to db: timeout after 30s", + "Failed to process request: NullPointerException", + "Authentication failed for user: invalid token", + "Rate limit exceeded: 429 Too Many Requests", + ] + ) else: level = random.choice(levels) message = f"Processing request {random.randint(1000, 9999)}" @@ -107,13 +109,15 @@ def generate_search_results(count: int, query: str) -> list[dict]: snippet = f"This article discusses {query} in detail. {query} is important..." else: title = f"Unrelated Document {i}" - snippet = f"This document covers something else entirely. Not about your search." + snippet = "This document covers something else entirely. Not about your search." result = { "id": f"doc_{random.randint(10000, 99999)}", "title": title, "snippet": snippet, - "relevance_score": round(random.uniform(0.9, 1.0) if i < 5 else random.uniform(0.1, 0.5), 3), + "relevance_score": round( + random.uniform(0.9, 1.0) if i < 5 else random.uniform(0.1, 0.5), 3 + ), "url": f"https://docs.example.com/{i}", } results.append(result) @@ -158,7 +162,13 @@ class TestErrorPreservation: messages = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Find ERROR entries in the logs"}, - {"role": "assistant", "content": None, "tool_calls": [{"id": "call_1", "function": {"name": "search_logs", "arguments": "{}"}}]}, + { + "role": "assistant", + "content": None, + "tool_calls": [ + {"id": "call_1", "function": {"name": "search_logs", "arguments": "{}"}} + ], + }, {"role": "tool", "content": raw_output, "tool_call_id": "call_1"}, ] @@ -168,15 +178,17 @@ class TestErrorPreservation: # Extract JSON (handle potential markers) import re - json_match = re.search(r'(\{.*\})', compressed_output, re.DOTALL) + + json_match = re.search(r"(\{.*\})", compressed_output, re.DOTALL) compressed_data = json.loads(json_match.group(1) if json_match else compressed_output) # Count preserved errors compressed_errors = [e for e in compressed_data["entries"] if e["level"] == "ERROR"] # CRITICAL: 100% of errors must be preserved - assert len(compressed_errors) == len(original_errors), \ + assert len(compressed_errors) == len(original_errors), ( f"ERROR preservation failed: {len(compressed_errors)}/{len(original_errors)} preserved" + ) def test_errors_preserved_with_many_errors(self, smart_crusher, tokenizer): """Even with many errors (exceeding max_items), all must be preserved.""" @@ -188,7 +200,13 @@ class TestErrorPreservation: messages = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Find errors"}, - {"role": "assistant", "content": None, "tool_calls": [{"id": "call_1", "function": {"name": "search_logs", "arguments": "{}"}}]}, + { + "role": "assistant", + "content": None, + "tool_calls": [ + {"id": "call_1", "function": {"name": "search_logs", "arguments": "{}"}} + ], + }, {"role": "tool", "content": raw_output, "tool_call_id": "call_1"}, ] @@ -196,14 +214,16 @@ class TestErrorPreservation: compressed_output = result.messages[-1]["content"] import re - json_match = re.search(r'(\{.*\})', compressed_output, re.DOTALL) + + json_match = re.search(r"(\{.*\})", compressed_output, re.DOTALL) compressed_data = json.loads(json_match.group(1) if json_match else compressed_output) compressed_errors = [e for e in compressed_data["entries"] if e["level"] == "ERROR"] # Even with many errors, ALL must be preserved - assert len(compressed_errors) == len(original_errors), \ + assert len(compressed_errors) == len(original_errors), ( f"High-error-rate preservation failed: {len(compressed_errors)}/{len(original_errors)}" + ) class TestAnomalyPreservation: @@ -220,7 +240,13 @@ class TestAnomalyPreservation: messages = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Look for CPU spikes or high error rates"}, - {"role": "assistant", "content": None, "tool_calls": [{"id": "call_1", "function": {"name": "get_metrics", "arguments": "{}"}}]}, + { + "role": "assistant", + "content": None, + "tool_calls": [ + {"id": "call_1", "function": {"name": "get_metrics", "arguments": "{}"}} + ], + }, {"role": "tool", "content": raw_output, "tool_call_id": "call_1"}, ] @@ -228,15 +254,17 @@ class TestAnomalyPreservation: compressed_output = result.messages[-1]["content"] import re - json_match = re.search(r'(\{.*\})', compressed_output, re.DOTALL) + + json_match = re.search(r"(\{.*\})", compressed_output, re.DOTALL) compressed_data = json.loads(json_match.group(1) if json_match else compressed_output) compressed_anomalies = [m for m in compressed_data["metrics"] if m["cpu_percent"] > 70] # Most anomalies should be preserved (statistical detection may miss some edge cases) - preservation_rate = len(compressed_anomalies) / len(original_anomalies) if original_anomalies else 1.0 - assert preservation_rate >= 0.8, \ - f"Anomaly preservation too low: {preservation_rate:.1%}" + preservation_rate = ( + len(compressed_anomalies) / len(original_anomalies) if original_anomalies else 1.0 + ) + assert preservation_rate >= 0.8, f"Anomaly preservation too low: {preservation_rate:.1%}" class TestRelevancePreservation: @@ -257,7 +285,13 @@ class TestRelevancePreservation: messages = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": f"Find documentation about {query}"}, - {"role": "assistant", "content": None, "tool_calls": [{"id": "call_1", "function": {"name": "search_docs", "arguments": "{}"}}]}, + { + "role": "assistant", + "content": None, + "tool_calls": [ + {"id": "call_1", "function": {"name": "search_docs", "arguments": "{}"}} + ], + }, {"role": "tool", "content": raw_output, "tool_call_id": "call_1"}, ] @@ -265,17 +299,19 @@ class TestRelevancePreservation: compressed_output = result.messages[-1]["content"] import re - json_match = re.search(r'(\{.*\})', compressed_output, re.DOTALL) + + json_match = re.search(r"(\{.*\})", compressed_output, re.DOTALL) compressed_data = json.loads(json_match.group(1) if json_match else compressed_output) # At least some high-relevance results should be preserved # (BM25 may not catch all without exact keyword matches) - compressed_high_relevance = [r for r in compressed_data["results"] if r["relevance_score"] > 0.8] + compressed_high_relevance = [ + r for r in compressed_data["results"] if r["relevance_score"] > 0.8 + ] # With BM25, we should preserve at least 1 high-relevance result # Full embedding support would preserve more - assert len(compressed_high_relevance) >= 1, \ - f"No high-relevance results preserved" + assert len(compressed_high_relevance) >= 1, "No high-relevance results preserved" def test_exact_keyword_needle(self, smart_crusher, tokenizer): """A user with exact keyword match should be found.""" @@ -291,7 +327,13 @@ class TestRelevancePreservation: messages = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Find users with ERROR status"}, - {"role": "assistant", "content": None, "tool_calls": [{"id": "call_1", "function": {"name": "search_users", "arguments": "{}"}}]}, + { + "role": "assistant", + "content": None, + "tool_calls": [ + {"id": "call_1", "function": {"name": "search_users", "arguments": "{}"}} + ], + }, {"role": "tool", "content": raw_output, "tool_call_id": "call_1"}, ] @@ -299,8 +341,9 @@ class TestRelevancePreservation: compressed_output = result.messages[-1]["content"] # The ERROR user should be preserved (error keyword detection) - assert "ERROR_SUSPENDED" in compressed_output, \ + assert "ERROR_SUSPENDED" in compressed_output, ( "User with ERROR keyword not found in compressed results" + ) def test_first_last_items_always_preserved(self, smart_crusher, tokenizer): """First and last items should always be preserved for context.""" @@ -314,7 +357,13 @@ class TestRelevancePreservation: messages = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "List all users"}, - {"role": "assistant", "content": None, "tool_calls": [{"id": "call_1", "function": {"name": "search_users", "arguments": "{}"}}]}, + { + "role": "assistant", + "content": None, + "tool_calls": [ + {"id": "call_1", "function": {"name": "search_users", "arguments": "{}"}} + ], + }, {"role": "tool", "content": raw_output, "tool_call_id": "call_1"}, ] @@ -322,10 +371,8 @@ class TestRelevancePreservation: compressed_output = result.messages[-1]["content"] # First and last items should always be preserved - assert "FIRST_USER_MARKER" in compressed_output, \ - "First item not preserved" - assert "LAST_USER_MARKER" in compressed_output, \ - "Last item not preserved" + assert "FIRST_USER_MARKER" in compressed_output, "First item not preserved" + assert "LAST_USER_MARKER" in compressed_output, "Last item not preserved" class TestCompressionEfficiency: @@ -341,7 +388,13 @@ class TestCompressionEfficiency: messages = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Check the logs"}, - {"role": "assistant", "content": None, "tool_calls": [{"id": "call_1", "function": {"name": "search_logs", "arguments": "{}"}}]}, + { + "role": "assistant", + "content": None, + "tool_calls": [ + {"id": "call_1", "function": {"name": "search_logs", "arguments": "{}"}} + ], + }, {"role": "tool", "content": raw_output, "tool_call_id": "call_1"}, ] @@ -352,8 +405,7 @@ class TestCompressionEfficiency: compression_ratio = 1 - (compressed_tokens / original_tokens) # Should achieve at least 50% compression - assert compression_ratio >= 0.5, \ - f"Compression ratio too low: {compression_ratio:.1%}" + assert compression_ratio >= 0.5, f"Compression ratio too low: {compression_ratio:.1%}" def test_token_savings_reported(self, smart_crusher, tokenizer): """TransformResult should report accurate token savings.""" @@ -364,15 +416,22 @@ class TestCompressionEfficiency: messages = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Check the logs"}, - {"role": "assistant", "content": None, "tool_calls": [{"id": "call_1", "function": {"name": "search_logs", "arguments": "{}"}}]}, + { + "role": "assistant", + "content": None, + "tool_calls": [ + {"id": "call_1", "function": {"name": "search_logs", "arguments": "{}"}} + ], + }, {"role": "tool", "content": raw_output, "tool_call_id": "call_1"}, ] result = smart_crusher.apply(messages, tokenizer=tokenizer) # Token counts should be accurate - assert result.tokens_before > result.tokens_after, \ + assert result.tokens_before > result.tokens_after, ( f"No compression: {result.tokens_before} -> {result.tokens_after}" + ) tokens_saved = result.tokens_before - result.tokens_after assert tokens_saved > 0, "Should save tokens" @@ -390,7 +449,13 @@ class TestSchemaPreservation: messages = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Check the logs"}, - {"role": "assistant", "content": None, "tool_calls": [{"id": "call_1", "function": {"name": "search_logs", "arguments": "{}"}}]}, + { + "role": "assistant", + "content": None, + "tool_calls": [ + {"id": "call_1", "function": {"name": "search_logs", "arguments": "{}"}} + ], + }, {"role": "tool", "content": raw_output, "tool_call_id": "call_1"}, ] @@ -399,7 +464,8 @@ class TestSchemaPreservation: # Should be valid JSON import re - json_match = re.search(r'(\{.*\})', compressed_output, re.DOTALL) + + json_match = re.search(r"(\{.*\})", compressed_output, re.DOTALL) compressed_data = json.loads(json_match.group(1) if json_match else compressed_output) # Should have same top-level key @@ -409,8 +475,9 @@ class TestSchemaPreservation: if compressed_data["entries"]: first_entry = compressed_data["entries"][0] expected_fields = {"timestamp", "level", "service", "message", "trace_id"} - assert expected_fields.issubset(set(first_entry.keys())), \ + assert expected_fields.issubset(set(first_entry.keys())), ( f"Original fields missing: {expected_fields - set(first_entry.keys())}" + ) def test_no_summary_metadata(self, smart_crusher, tokenizer): """No summary or metadata fields should be added to output.""" @@ -421,7 +488,13 @@ class TestSchemaPreservation: messages = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Check the logs"}, - {"role": "assistant", "content": None, "tool_calls": [{"id": "call_1", "function": {"name": "search_logs", "arguments": "{}"}}]}, + { + "role": "assistant", + "content": None, + "tool_calls": [ + {"id": "call_1", "function": {"name": "search_logs", "arguments": "{}"}} + ], + }, {"role": "tool", "content": raw_output, "tool_call_id": "call_1"}, ] @@ -429,7 +502,8 @@ class TestSchemaPreservation: compressed_output = result.messages[-1]["content"] import re - json_match = re.search(r'(\{.*\})', compressed_output, re.DOTALL) + + json_match = re.search(r"(\{.*\})", compressed_output, re.DOTALL) compressed_data = json.loads(json_match.group(1) if json_match else compressed_output) # Should NOT have added metadata keys @@ -448,19 +522,27 @@ class TestEdgeCases: # Create entries that are ALL errors entries = [] for i in range(50): - entries.append({ - "timestamp": datetime.now().isoformat(), - "level": "ERROR", - "message": f"Error message {i}", - "service": "test", - }) + entries.append( + { + "timestamp": datetime.now().isoformat(), + "level": "ERROR", + "message": f"Error message {i}", + "service": "test", + } + ) raw_output = json.dumps({"entries": entries}, indent=2) messages = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Check errors"}, - {"role": "assistant", "content": None, "tool_calls": [{"id": "call_1", "function": {"name": "search_logs", "arguments": "{}"}}]}, + { + "role": "assistant", + "content": None, + "tool_calls": [ + {"id": "call_1", "function": {"name": "search_logs", "arguments": "{}"}} + ], + }, {"role": "tool", "content": raw_output, "tool_call_id": "call_1"}, ] @@ -468,12 +550,14 @@ class TestEdgeCases: compressed_output = result.messages[-1]["content"] import re - json_match = re.search(r'(\{.*\})', compressed_output, re.DOTALL) + + json_match = re.search(r"(\{.*\})", compressed_output, re.DOTALL) compressed_data = json.loads(json_match.group(1) if json_match else compressed_output) # ALL entries should be kept (they're all errors) - assert len(compressed_data["entries"]) == 50, \ + assert len(compressed_data["entries"]) == 50, ( f"Should keep all 50 error entries, got {len(compressed_data['entries'])}" + ) def test_small_input_no_compression(self, smart_crusher, tokenizer): """Small inputs below threshold should not be compressed.""" @@ -484,7 +568,13 @@ class TestEdgeCases: messages = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Check logs"}, - {"role": "assistant", "content": None, "tool_calls": [{"id": "call_1", "function": {"name": "search_logs", "arguments": "{}"}}]}, + { + "role": "assistant", + "content": None, + "tool_calls": [ + {"id": "call_1", "function": {"name": "search_logs", "arguments": "{}"}} + ], + }, {"role": "tool", "content": raw_output, "tool_call_id": "call_1"}, ] @@ -492,7 +582,8 @@ class TestEdgeCases: compressed_output = result.messages[-1]["content"] import re - json_match = re.search(r'(\{.*\})', compressed_output, re.DOTALL) + + json_match = re.search(r"(\{.*\})", compressed_output, re.DOTALL) compressed_data = json.loads(json_match.group(1) if json_match else compressed_output) # Should keep all entries (below min_items_to_analyze) diff --git a/tests/test_integrations/test_mcp.py b/tests/test_integrations/test_mcp.py index 61eaa368a..b3d85004b 100644 --- a/tests/test_integrations/test_mcp.py +++ b/tests/test_integrations/test_mcp.py @@ -5,27 +5,26 @@ while preserving 100% of critical data (errors, anomalies). """ import json -import pytest import random from datetime import datetime, timedelta +import pytest + from headroom.integrations.mcp import ( - HeadroomMCPCompressor, HeadroomMCPClientWrapper, + HeadroomMCPCompressor, MCPCompressionResult, MCPToolProfile, compress_tool_result, compress_tool_result_with_metrics, - DEFAULT_MCP_PROFILES, ) -from headroom.config import HeadroomConfig from headroom.providers import OpenAIProvider - # ============================================================================ # Test Fixtures # ============================================================================ + @pytest.fixture def mcp_compressor(): """Create MCP compressor with default settings.""" @@ -48,30 +47,36 @@ def generate_slack_messages(count: int, error_rate: float = 0.1) -> str: for i in range(count): is_error = random.random() < error_rate if is_error: - text = random.choice([ - "ERROR: Database connection failed at 2:30am", - "CRITICAL: API latency spike detected", - "Exception: NullPointerException in AuthService", - "FAILED: Build pipeline broke on main branch", - "BUG: Users can't login - investigating now", - ]) + text = random.choice( + [ + "ERROR: Database connection failed at 2:30am", + "CRITICAL: API latency spike detected", + "Exception: NullPointerException in AuthService", + "FAILED: Build pipeline broke on main branch", + "BUG: Users can't login - investigating now", + ] + ) else: - text = random.choice([ - "Reviewed the PR, looks good to merge", - "Updated the docs with new API endpoints", - "Meeting notes from standup attached", - "Can someone review my changes?", - "Deployed v2.3.1 to staging", - ]) + text = random.choice( + [ + "Reviewed the PR, looks good to merge", + "Updated the docs with new API endpoints", + "Meeting notes from standup attached", + "Can someone review my changes?", + "Deployed v2.3.1 to staging", + ] + ) - messages.append({ - "id": f"msg_{i}", - "channel": random.choice(channels), - "user": random.choice(users), - "text": text, - "timestamp": (datetime.now() - timedelta(hours=i)).isoformat(), - "reactions": random.randint(0, 10), - }) + messages.append( + { + "id": f"msg_{i}", + "channel": random.choice(channels), + "user": random.choice(users), + "text": text, + "timestamp": (datetime.now() - timedelta(hours=i)).isoformat(), + "reactions": random.randint(0, 10), + } + ) return json.dumps({"messages": messages, "total": count}) @@ -87,7 +92,9 @@ def generate_database_results(count: int, null_rate: float = 0.1) -> str: "id": i + 1, "user_id": f"user_{random.randint(1000, 9999)}", "email": f"user{i}@example.com", - "status": "ERROR: validation failed" if has_error else random.choice(["active", "inactive", "pending"]), + "status": "ERROR: validation failed" + if has_error + else random.choice(["active", "inactive", "pending"]), "created_at": (datetime.now() - timedelta(days=random.randint(1, 365))).isoformat(), "balance": None if has_null else round(random.uniform(0, 10000), 2), } @@ -98,37 +105,42 @@ def generate_database_results(count: int, null_rate: float = 0.1) -> str: def generate_log_entries(count: int, error_rate: float = 0.15) -> str: """Generate realistic log entries.""" - levels = ["DEBUG", "INFO", "WARN", "ERROR", "FATAL"] services = ["api-gateway", "auth-service", "payment-service", "user-service"] entries = [] for i in range(count): if random.random() < error_rate: level = random.choice(["ERROR", "FATAL"]) - message = random.choice([ - "Connection timeout to database", - "Failed to process payment: insufficient funds", - "Authentication failed for user", - "Memory limit exceeded", - "Unhandled exception in request handler", - ]) + message = random.choice( + [ + "Connection timeout to database", + "Failed to process payment: insufficient funds", + "Authentication failed for user", + "Memory limit exceeded", + "Unhandled exception in request handler", + ] + ) else: level = random.choice(["DEBUG", "INFO", "INFO", "INFO", "WARN"]) - message = random.choice([ - "Request processed successfully", - "Cache hit for user data", - "Starting health check", - "Connection pool recycled", - "Metrics exported", - ]) + message = random.choice( + [ + "Request processed successfully", + "Cache hit for user data", + "Starting health check", + "Connection pool recycled", + "Metrics exported", + ] + ) - entries.append({ - "timestamp": (datetime.now() - timedelta(minutes=i)).isoformat(), - "level": level, - "service": random.choice(services), - "message": message, - "trace_id": f"trace_{random.randint(100000, 999999)}", - }) + entries.append( + { + "timestamp": (datetime.now() - timedelta(minutes=i)).isoformat(), + "level": level, + "service": random.choice(services), + "message": message, + "trace_id": f"trace_{random.randint(100000, 999999)}", + } + ) return json.dumps({"entries": entries}) @@ -141,17 +153,23 @@ def generate_github_issues(count: int, bug_rate: float = 0.2) -> str: issues = [] for i in range(count): is_bug = random.random() < bug_rate - labels = random.sample(bug_labels, k=random.randint(1, 2)) if is_bug else random.sample(labels_pool, k=random.randint(0, 2)) + labels = ( + random.sample(bug_labels, k=random.randint(1, 2)) + if is_bug + else random.sample(labels_pool, k=random.randint(0, 2)) + ) - issues.append({ - "number": i + 1, - "title": f"{'BUG: ' if is_bug else ''}{random.choice(['Fix login flow', 'Update API docs', 'Add dark mode', 'Improve performance'])}", - "state": random.choice(["open", "closed"]), - "labels": labels, - "author": f"user{random.randint(1, 100)}", - "created_at": (datetime.now() - timedelta(days=random.randint(1, 30))).isoformat(), - "comments": random.randint(0, 20), - }) + issues.append( + { + "number": i + 1, + "title": f"{'BUG: ' if is_bug else ''}{random.choice(['Fix login flow', 'Update API docs', 'Add dark mode', 'Improve performance'])}", + "state": random.choice(["open", "closed"]), + "labels": labels, + "author": f"user{random.randint(1, 100)}", + "created_at": (datetime.now() - timedelta(days=random.randint(1, 30))).isoformat(), + "comments": random.randint(0, 20), + } + ) return json.dumps({"issues": issues, "total_count": count}) @@ -160,6 +178,7 @@ def generate_github_issues(count: int, bug_rate: float = 0.2) -> str: # Test Classes # ============================================================================ + class TestMCPToolProfiles: """Test tool profile matching.""" @@ -260,11 +279,14 @@ class TestMCPErrorPreservation: ) compressed_data = json.loads(result.compressed_content) - compressed_errors = [e for e in compressed_data["entries"] if e["level"] in ["ERROR", "FATAL"]] + compressed_errors = [ + e for e in compressed_data["entries"] if e["level"] in ["ERROR", "FATAL"] + ] # CRITICAL: 100% of errors must be preserved - assert len(compressed_errors) >= len(original_errors), \ + assert len(compressed_errors) >= len(original_errors), ( f"Lost errors: {len(original_errors)} -> {len(compressed_errors)}" + ) def test_slack_significant_compression_with_content(self, mcp_compressor): """Slack messages should compress while preserving error keywords in text.""" @@ -282,8 +304,11 @@ class TestMCPErrorPreservation: compressed_data = json.loads(result.compressed_content) # Should preserve some messages with error keywords (SmartCrusher detects these) - error_msgs = [m for m in compressed_data["messages"] - if any(kw in m["text"].lower() for kw in ["error", "failed", "exception"])] + error_msgs = [ + m + for m in compressed_data["messages"] + if any(kw in m["text"].lower() for kw in ["error", "failed", "exception"]) + ] assert len(error_msgs) > 0, "Should preserve some error messages" def test_database_error_status_preserved(self, mcp_compressor): @@ -292,8 +317,7 @@ class TestMCPErrorPreservation: content = generate_database_results(150, null_rate=0.15) data = json.loads(content) - original_errors = [r for r in data["rows"] - if "error" in str(r["status"]).lower()] + original_errors = [r for r in data["rows"] if "error" in str(r["status"]).lower()] result = mcp_compressor.compress( content=content, @@ -302,12 +326,14 @@ class TestMCPErrorPreservation: ) compressed_data = json.loads(result.compressed_content) - compressed_errors = [r for r in compressed_data["rows"] - if "error" in str(r["status"]).lower()] + compressed_errors = [ + r for r in compressed_data["rows"] if "error" in str(r["status"]).lower() + ] # Should preserve most error rows - assert len(compressed_errors) >= len(original_errors) * 0.8, \ + assert len(compressed_errors) >= len(original_errors) * 0.8, ( f"Lost too many errors: {len(original_errors)} -> {len(compressed_errors)}" + ) def test_github_bugs_partial_preservation(self, mcp_compressor): """GitHub bug issues should have partial preservation.""" @@ -322,8 +348,11 @@ class TestMCPErrorPreservation: compressed_data = json.loads(result.compressed_content) # Should preserve at least some bugs - compressed_bugs = [i for i in compressed_data["issues"] - if any(l in ["bug", "critical", "urgent", "blocker"] for l in i["labels"])] + compressed_bugs = [ + i + for i in compressed_data["issues"] + if any(label in ["bug", "critical", "urgent", "blocker"] for label in i["labels"]) + ] # At least 5 bugs should be preserved assert len(compressed_bugs) >= 5, "Should preserve at least 5 bug issues" @@ -364,6 +393,7 @@ class TestMCPClientWrapper: @pytest.fixture def mock_mcp_client(self): """Create a mock MCP client.""" + class MockMCPClient: async def call_tool(self, name: str, arguments: dict | None = None) -> str: if "slack" in name: @@ -372,6 +402,7 @@ class TestMCPClientWrapper: return generate_log_entries(150) else: return generate_database_results(80) + return MockMCPClient() @pytest.mark.asyncio @@ -421,8 +452,9 @@ class TestMCPCompressionRatio: content=content, tool_name="slack_search", ) - assert result.compression_ratio > 0.5, \ + assert result.compression_ratio > 0.5, ( f"Compression ratio too low: {result.compression_ratio:.2%}" + ) def test_significant_compression_logs(self, mcp_compressor): """Log entries should compress well (>50%).""" @@ -431,8 +463,9 @@ class TestMCPCompressionRatio: content=content, tool_name="search_logs", ) - assert result.compression_ratio > 0.5, \ + assert result.compression_ratio > 0.5, ( f"Compression ratio too low: {result.compression_ratio:.2%}" + ) def test_compression_efficiency_increases_with_size(self, mcp_compressor): """Larger outputs should compress more efficiently.""" diff --git a/tests/test_models.py b/tests/test_models.py index 24683a9f6..8d03172a2 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -3,11 +3,10 @@ from __future__ import annotations import pytest -from datetime import date from headroom.models import ( - ModelRegistry, ModelInfo, + ModelRegistry, get_model_info, list_models, register_model, diff --git a/tests/test_parser.py b/tests/test_parser.py index f1f0f6341..28686ee67 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -10,23 +10,23 @@ Tests all parsing and analysis functions: - get_message_content_text: Content extraction """ -import pytest from unittest.mock import Mock +import pytest + from headroom.parser import ( compute_hash, detect_waste_signals, + find_tool_units, + get_message_content_text, is_rag_content, parse_message_to_blocks, parse_messages, - find_tool_units, - get_message_content_text, ) -from headroom.config import Block, WasteSignals - # --- Fixtures --- + @pytest.fixture def mock_tokenizer(): """Mock tokenizer that returns predictable token counts.""" @@ -64,12 +64,9 @@ def tool_call_message(): { "id": "call_abc123", "type": "function", - "function": { - "name": "search_user", - "arguments": '{"user_id": "12345"}' - } + "function": {"name": "search_user", "arguments": '{"user_id": "12345"}'}, } - ] + ], } @@ -79,7 +76,7 @@ def tool_result_message(): return { "role": "tool", "tool_call_id": "call_abc123", - "content": '{"id": "12345", "name": "Alice", "email": "alice@example.com"}' + "content": '{"id": "12345", "name": "Alice", "email": "alice@example.com"}', } @@ -91,8 +88,8 @@ def multimodal_message(): "content": [ {"type": "text", "text": "Analyze this image:"}, {"type": "image", "source": {"type": "base64", "data": "..."}}, - {"type": "text", "text": "What do you see?"} - ] + {"type": "text", "text": "What do you see?"}, + ], } @@ -101,7 +98,7 @@ def rag_user_message(): """User message containing RAG content markers.""" return { "role": "user", - "content": "[Document 1] Here is the relevant context from our knowledge base. [Source: docs/manual.md]" + "content": "[Document 1] Here is the relevant context from our knowledge base. [Source: docs/manual.md]", } @@ -136,6 +133,7 @@ def json_bloat_text(): # --- TestComputeHash --- + class TestComputeHash: """Tests for compute_hash function.""" @@ -172,6 +170,7 @@ class TestComputeHash: # --- TestDetectWasteSignals --- + class TestDetectWasteSignals: """Tests for detect_waste_signals function.""" @@ -227,6 +226,7 @@ class TestDetectWasteSignals: # --- TestIsRagContent --- + class TestIsRagContent: """Tests for is_rag_content function.""" @@ -268,6 +268,7 @@ class TestIsRagContent: # --- TestParseMessageToBlocks --- + class TestParseMessageToBlocks: """Tests for parse_message_to_blocks function.""" @@ -337,12 +338,7 @@ class TestParseMessageToBlocks: msg = { "role": "assistant", "content": "Let me search for that.", - "tool_calls": [ - { - "id": "call_xyz", - "function": {"name": "search", "arguments": "{}"} - } - ] + "tool_calls": [{"id": "call_xyz", "function": {"name": "search", "arguments": "{}"}}], } blocks = parse_message_to_blocks(msg, 0, mock_tokenizer) kinds = [b.kind for b in blocks] @@ -369,6 +365,7 @@ class TestParseMessageToBlocks: # --- TestParseMessages --- + class TestParseMessages: """Tests for parse_messages function.""" @@ -412,6 +409,7 @@ class TestParseMessages: # --- TestFindToolUnits --- + class TestFindToolUnits: """Tests for find_tool_units function.""" @@ -433,8 +431,8 @@ class TestFindToolUnits: "content": None, "tool_calls": [ {"id": "call_1", "function": {"name": "search", "arguments": "{}"}}, - {"id": "call_2", "function": {"name": "fetch", "arguments": "{}"}} - ] + {"id": "call_2", "function": {"name": "fetch", "arguments": "{}"}}, + ], }, {"role": "tool", "tool_call_id": "call_1", "content": "result 1"}, {"role": "tool", "tool_call_id": "call_2", "content": "result 2"}, @@ -475,7 +473,7 @@ class TestFindToolUnits: "tool_calls": [ {"id": "call_a", "function": {"name": "first", "arguments": "{}"}}, {"id": "call_b", "function": {"name": "second", "arguments": "{}"}}, - ] + ], }, {"role": "tool", "tool_call_id": "call_b", "content": "second result"}, {"role": "tool", "tool_call_id": "call_a", "content": "first result"}, @@ -488,6 +486,7 @@ class TestFindToolUnits: # --- TestGetMessageContentText --- + class TestGetMessageContentText: """Tests for get_message_content_text function.""" @@ -505,7 +504,7 @@ class TestGetMessageContentText: {"type": "text", "text": "First part"}, {"type": "image", "source": {}}, {"type": "text", "text": "Second part"}, - ] + ], } text = get_message_content_text(msg) assert "First part" in text @@ -524,7 +523,7 @@ class TestGetMessageContentText: "content": [ {"type": "text", "text": "Dict text"}, "Plain string", - ] + ], } text = get_message_content_text(msg) assert "Dict text" in text @@ -543,7 +542,7 @@ class TestGetMessageContentText: "content": [ {"type": "image", "data": "..."}, {"type": "text", "text": "Only this"}, - ] + ], } text = get_message_content_text(msg) assert text == "Only this" @@ -557,6 +556,7 @@ class TestGetMessageContentText: # --- Additional fixtures for complex tests --- + @pytest.fixture def sample_messages(): """Basic conversation messages.""" @@ -580,17 +580,14 @@ def sample_messages_with_tools(): { "id": "call_123", "type": "function", - "function": { - "name": "search_user", - "arguments": '{"user_id": "12345"}' - } + "function": {"name": "search_user", "arguments": '{"user_id": "12345"}'}, } - ] + ], }, { "role": "tool", "tool_call_id": "call_123", - "content": '{"id": "12345", "name": "Alice", "email": "alice@example.com"}' + "content": '{"id": "12345", "name": "Alice", "email": "alice@example.com"}', }, {"role": "assistant", "content": "I found user Alice with ID 12345."}, ] diff --git a/tests/test_providers/test_anthropic.py b/tests/test_providers/test_anthropic.py index f97d36722..dd40a2857 100644 --- a/tests/test_providers/test_anthropic.py +++ b/tests/test_providers/test_anthropic.py @@ -1,10 +1,13 @@ """Tests for Anthropic provider.""" + import pytest + class TestAnthropicTokenCounting: @pytest.fixture def anthropic_provider(self): from headroom.providers.anthropic import AnthropicProvider + return AnthropicProvider() def test_count_text_fallback(self, anthropic_provider): @@ -19,10 +22,12 @@ class TestAnthropicTokenCounting: count = counter.count_messages(messages) assert count > 0 + class TestAnthropicModelLimits: @pytest.fixture def anthropic_provider(self): from headroom.providers.anthropic import AnthropicProvider + return AnthropicProvider() def test_get_context_limit_claude_sonnet(self, anthropic_provider): @@ -39,10 +44,12 @@ class TestAnthropicModelLimits: def test_supports_model_prefix(self, anthropic_provider): assert anthropic_provider.supports_model("claude-3-5-sonnet-latest") + class TestAnthropicCostEstimation: @pytest.fixture def anthropic_provider(self): from headroom.providers.anthropic import AnthropicProvider + return AnthropicProvider() def test_estimate_cost_basic(self, anthropic_provider): diff --git a/tests/test_providers/test_openai.py b/tests/test_providers/test_openai.py index 75c04dd38..58d7925d1 100644 --- a/tests/test_providers/test_openai.py +++ b/tests/test_providers/test_openai.py @@ -1,13 +1,12 @@ """Tests for OpenAI provider.""" + import pytest + from headroom.providers.openai import ( - OpenAIProvider, - OpenAITokenCounter, _get_encoding_name_for_model, - _check_pricing_staleness, - TIKTOKEN_AVAILABLE, ) + class TestOpenAITokenCounting: def test_count_text_empty(self, openai_tokenizer): assert openai_tokenizer.count_text("") == 0 @@ -32,13 +31,8 @@ class TestOpenAITokenCounting: {"role": "user", "content": "Search"}, { "role": "assistant", - "tool_calls": [ - { - "id": "call_1", - "function": {"name": "search", "arguments": "{}"} - } - ] - } + "tool_calls": [{"id": "call_1", "function": {"name": "search", "arguments": "{}"}}], + }, ] count = openai_tokenizer.count_messages(messages) assert count > 10 # Tool calls add overhead @@ -49,6 +43,7 @@ class TestOpenAITokenCounting: count = openai_tokenizer.count_message(msg) assert count >= 4 + class TestOpenAIModelLimits: def test_get_context_limit_gpt4o(self, openai_provider): assert openai_provider.get_context_limit("gpt-4o") == 128000 @@ -67,6 +62,7 @@ class TestOpenAIModelLimits: def test_supports_model_unknown(self, openai_provider): assert openai_provider.supports_model("claude-3") is False + class TestOpenAICostEstimation: def test_estimate_cost_input_only(self, openai_provider): cost = openai_provider.estimate_cost( @@ -103,6 +99,7 @@ class TestOpenAICostEstimation: ) assert cost is None + class TestEncodingSelection: def test_gpt4o_uses_o200k(self): assert _get_encoding_name_for_model("gpt-4o") == "o200k_base" diff --git a/tests/test_providers/test_universal.py b/tests/test_providers/test_universal.py index 9b1256ba2..d81a2d477 100644 --- a/tests/test_providers/test_universal.py +++ b/tests/test_providers/test_universal.py @@ -7,27 +7,28 @@ from __future__ import annotations import pytest +from headroom.providers import ( + GoogleProvider, + ModelCapabilities, + OpenAICompatibleProvider, + create_groq_provider, + create_lmstudio_provider, + create_ollama_provider, + create_together_provider, + create_vllm_provider, + is_litellm_available, +) + def _transformers_available() -> bool: """Check if transformers is available.""" try: import transformers # noqa: F401 + return True except ImportError: return False -from headroom.providers import ( - OpenAICompatibleProvider, - ModelCapabilities, - GoogleProvider, - create_ollama_provider, - create_together_provider, - create_groq_provider, - create_vllm_provider, - create_lmstudio_provider, - is_litellm_available, -) - class TestOpenAICompatibleProvider: """Tests for OpenAICompatibleProvider.""" @@ -58,7 +59,7 @@ class TestOpenAICompatibleProvider: @pytest.mark.skipif( not _transformers_available(), - reason="transformers not installed - needed for HuggingFace tokenizer" + reason="transformers not installed - needed for HuggingFace tokenizer", ) def test_get_token_counter(self): """Test getting token counter.""" diff --git a/tests/test_proxy_ccr.py b/tests/test_proxy_ccr.py index 023548384..d60b10896 100644 --- a/tests/test_proxy_ccr.py +++ b/tests/test_proxy_ccr.py @@ -4,14 +4,16 @@ These tests verify the /v1/retrieve endpoints work correctly. """ import json + import pytest # Skip if fastapi not available pytest.importorskip("fastapi") from fastapi.testclient import TestClient -from headroom.proxy.server import create_app, ProxyConfig -from headroom.cache.compression_store import reset_compression_store, get_compression_store + +from headroom.cache.compression_store import get_compression_store, reset_compression_store +from headroom.proxy.server import ProxyConfig, create_app @pytest.fixture @@ -106,8 +108,7 @@ class TestCCRRetrieveEndpoint: ) response = client.post( - "/v1/retrieve", - json={"hash": hash_key, "query": "Python programming"} + "/v1/retrieve", json={"hash": hash_key, "query": "Python programming"} ) assert response.status_code == 200 @@ -221,13 +222,16 @@ class TestCCRStatsEndpoint: def test_stats_tracks_retrievals(self, client): """Stats include recent retrieval events.""" import json as json_module + store = get_compression_store() # Use non-empty content so search actually logs - content = json_module.dumps([ - {"id": "1", "name": "test item", "value": 100}, - {"id": "2", "name": "another item", "value": 200}, - ]) + content = json_module.dumps( + [ + {"id": "1", "name": "test item", "value": 100}, + {"id": "2", "name": "another item", "value": 200}, + ] + ) hash_key = store.store( original=content, compressed=content, @@ -304,10 +308,7 @@ class TestCCREdgeCases: items = [{"id": 1, "text": "hello world"}] hash_key = store.store(original=json.dumps(items), compressed="[]") - response = client.post( - "/v1/retrieve", - json={"hash": hash_key, "query": "xyznonexistent"} - ) + response = client.post("/v1/retrieve", json={"hash": hash_key, "query": "xyznonexistent"}) assert response.status_code == 200 data = response.json() diff --git a/tests/test_quality_retention.py b/tests/test_quality_retention.py index ab72f4347..76e87a3b0 100644 --- a/tests/test_quality_retention.py +++ b/tests/test_quality_retention.py @@ -9,14 +9,16 @@ This is a FORMAL EVAL - any failure here is a CRITICAL BUG. """ import json + import pytest + +from headroom.providers.anthropic import AnthropicTokenCounter +from headroom.tokenizer import Tokenizer from headroom.transforms.smart_crusher import ( SmartCrusher, SmartCrusherConfig, smart_crush_tool_output, ) -from headroom.tokenizer import Tokenizer -from headroom.providers.anthropic import AnthropicTokenCounter class TestErrorRetention: @@ -31,12 +33,14 @@ class TestErrorRetention: error_indices = [] for i in range(1000): - items.append({ - "id": f"item_{i}", - "value": i, - "status": "ok", - "message": f"Normal operation {i}", - }) + items.append( + { + "id": f"item_{i}", + "value": i, + "status": "ok", + "message": f"Normal operation {i}", + } + ) # Insert errors at specific positions for idx in [10, 50, 100, 250, 500, 750, 999]: @@ -129,11 +133,13 @@ class TestAnomalyRetention: # Create items with normal values around mean=100, std=10 for i in range(1000): - items.append({ - "id": f"item_{i}", - "value": 100 + (i % 20) - 10, # Values 90-110 - "name": f"Normal item {i}", - }) + items.append( + { + "id": f"item_{i}", + "value": 100 + (i % 20) - 10, # Values 90-110 + "name": f"Normal item {i}", + } + ) # Insert anomalies (> 2 std = > 120 or < 80) for idx in [100, 300, 500, 700, 900]: @@ -171,10 +177,7 @@ class TestRelevanceRetention: def test_relevance_with_query_context(self): """Items matching query should be retained when context is provided.""" - items = [ - {"id": i, "content": f"Generic content about topic {i}"} - for i in range(100) - ] + items = [{"id": i, "content": f"Generic content about topic {i}"} for i in range(100)] # Insert a specific item that matches our query # Note: This also contains "error" keyword which will trigger error retention @@ -201,9 +204,7 @@ class TestRelevanceRetention: compressed = json.loads(tool_msg["content"].split("\n")[0]) # Remove marker targets = [x for x in compressed if x.get("is_target")] - assert len(targets) >= 1, ( - "Target item was dropped despite matching query context!" - ) + assert len(targets) >= 1, "Target item was dropped despite matching query context!" class TestFirstLastRetention: @@ -281,7 +282,7 @@ class TestCombinedRetention: # Count retained critical items errors_retained = sum(1 for x in compressed if x.get("error")) - anomalies_retained = sum(1 for x in compressed if x.get("value", 0) > 900000) + sum(1 for x in compressed if x.get("value", 0) > 900000) # All errors should be retained errors_original = sum(1 for x in items if x.get("error")) @@ -298,13 +299,15 @@ class TestCompressionRatio: # Create realistic large dataset items = [] for i in range(1000): - items.append({ - "id": f"doc_{i}", - "score": 0.5, - "title": f"Document {i} about various topics", - "snippet": "Lorem ipsum " * 20, - "metadata": {"source": "web", "date": "2024-01-01"}, - }) + items.append( + { + "id": f"doc_{i}", + "score": 0.5, + "title": f"Document {i} about various topics", + "snippet": "Lorem ipsum " * 20, + "metadata": {"source": "web", "date": "2024-01-01"}, + } + ) # Add some critical items items[100]["error"] = "Parse error" @@ -355,8 +358,7 @@ class TestEdgeCases: # All 50 errors should be retained (errors override max_items) assert len(compressed) == 50, ( - f"Some errors dropped when all items are errors! " - f"Expected 50, got {len(compressed)}" + f"Some errors dropped when all items are errors! Expected 50, got {len(compressed)}" ) def test_unicode_content(self): diff --git a/tests/test_storage/test_sqlite.py b/tests/test_storage/test_sqlite.py index 33d8eed58..373df5e48 100644 --- a/tests/test_storage/test_sqlite.py +++ b/tests/test_storage/test_sqlite.py @@ -33,9 +33,7 @@ class TestSQLiteStorageInit: cursor = conn.cursor() # Check that requests table exists - cursor.execute( - "SELECT name FROM sqlite_master WHERE type='table' AND name='requests'" - ) + cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='requests'") result = cursor.fetchone() assert result is not None assert result[0] == "requests" diff --git a/tests/test_telemetry.py b/tests/test_telemetry.py index e217852ed..4ad51cf3c 100644 --- a/tests/test_telemetry.py +++ b/tests/test_telemetry.py @@ -1,21 +1,19 @@ """Tests for telemetry module (data flywheel).""" -import time -import tempfile import os -import json +import tempfile + import pytest from headroom.telemetry import ( + AnonymizedToolStats, + FieldDistribution, + RetrievalStats, TelemetryCollector, TelemetryConfig, + ToolSignature, get_telemetry_collector, reset_telemetry_collector, - FieldDistribution, - ToolSignature, - CompressionEvent, - RetrievalStats, - AnonymizedToolStats, ) @@ -294,18 +292,27 @@ class TestTelemetryCollector: # Different strategies collector.record_compression( - items=items, original_count=100, compressed_count=10, - original_tokens=1000, compressed_tokens=100, + items=items, + original_count=100, + compressed_count=10, + original_tokens=1000, + compressed_tokens=100, strategy="top_n", ) collector.record_compression( - items=items, original_count=100, compressed_count=10, - original_tokens=1000, compressed_tokens=100, + items=items, + original_count=100, + compressed_count=10, + original_tokens=1000, + compressed_tokens=100, strategy="smart_sample", ) collector.record_compression( - items=items, original_count=100, compressed_count=10, - original_tokens=1000, compressed_tokens=100, + items=items, + original_count=100, + compressed_count=10, + original_tokens=1000, + compressed_tokens=100, strategy="top_n", ) @@ -324,8 +331,11 @@ class TestTelemetryCollector: items = [{"id": "1"}] for _ in range(5): # Less than 10 collector.record_compression( - items=items, original_count=100, compressed_count=10, - original_tokens=1000, compressed_tokens=100, + items=items, + original_count=100, + compressed_count=10, + original_tokens=1000, + compressed_tokens=100, strategy="top_n", ) @@ -343,8 +353,11 @@ class TestTelemetryCollector: items = [{"id": "1"}] for _ in range(10): collector.record_compression( - items=items, original_count=100, compressed_count=10, - original_tokens=1000, compressed_tokens=100, + items=items, + original_count=100, + compressed_count=10, + original_tokens=1000, + compressed_tokens=100, strategy="top_n", ) @@ -388,8 +401,11 @@ class TestTelemetryCollector: # Collector 1 records some compressions for _ in range(5): collector1.record_compression( - items=items, original_count=100, compressed_count=10, - original_tokens=1000, compressed_tokens=100, + items=items, + original_count=100, + compressed_count=10, + original_tokens=1000, + compressed_tokens=100, strategy="top_n", ) @@ -399,8 +415,11 @@ class TestTelemetryCollector: # Collector 2 records different compressions for _ in range(3): collector2.record_compression( - items=items, original_count=100, compressed_count=10, - original_tokens=1000, compressed_tokens=100, + items=items, + original_count=100, + compressed_count=10, + original_tokens=1000, + compressed_tokens=100, strategy="smart_sample", ) @@ -420,8 +439,11 @@ class TestTelemetryCollector: items = [{"id": "1"}] collector.record_compression( - items=items, original_count=100, compressed_count=10, - original_tokens=1000, compressed_tokens=100, + items=items, + original_count=100, + compressed_count=10, + original_tokens=1000, + compressed_tokens=100, strategy="top_n", ) @@ -468,8 +490,11 @@ class TestTelemetryCollector: # Record more than max events for i in range(10): collector.record_compression( - items=items, original_count=100 + i, compressed_count=10, - original_tokens=1000, compressed_tokens=100, + items=items, + original_count=100 + i, + compressed_count=10, + original_tokens=1000, + compressed_tokens=100, strategy="top_n", ) @@ -493,8 +518,11 @@ class TestTelemetryPersistence: items = [{"id": "1", "name": "test"}] for _ in range(3): collector.record_compression( - items=items, original_count=100, compressed_count=10, - original_tokens=1000, compressed_tokens=100, + items=items, + original_count=100, + compressed_count=10, + original_tokens=1000, + compressed_tokens=100, strategy="top_n", ) @@ -525,8 +553,11 @@ class TestGlobalTelemetryCollector: collector1 = get_telemetry_collector() items = [{"id": "1"}] collector1.record_compression( - items=items, original_count=100, compressed_count=10, - original_tokens=1000, compressed_tokens=100, + items=items, + original_count=100, + compressed_count=10, + original_tokens=1000, + compressed_tokens=100, strategy="top_n", ) @@ -658,6 +689,7 @@ class TestAnonymizedToolStats: # Make a deep copy to compare after import copy + original_data = copy.deepcopy(data) stats = AnonymizedToolStats.from_dict(data) diff --git a/tests/test_toin.py b/tests/test_toin.py index dfda596a7..58e4f46a8 100644 --- a/tests/test_toin.py +++ b/tests/test_toin.py @@ -3,15 +3,15 @@ import os import tempfile import time -import json + import pytest from headroom.telemetry import ( - ToolSignature, - ToolIntelligenceNetwork, - ToolPattern, CompressionHint, TOINConfig, + ToolIntelligenceNetwork, + ToolPattern, + ToolSignature, get_toin, reset_toin, ) diff --git a/tests/test_toin_fixes.py b/tests/test_toin_fixes.py index 4228709a3..8cb758177 100644 --- a/tests/test_toin_fixes.py +++ b/tests/test_toin_fixes.py @@ -11,32 +11,27 @@ This file tests all the fixes made to the TOIN implementation: """ import json -import pytest import tempfile from pathlib import Path -from unittest.mock import patch, MagicMock -from headroom.telemetry.toin import ( - ToolIntelligenceNetwork, - TOINConfig, - ToolPattern, - CompressionHint, - get_toin, - reset_toin, -) -from headroom.telemetry import ToolSignature +import pytest + from headroom.cache.compression_feedback import ( - CompressionFeedback, - LocalToolPattern as FeedbackToolPattern, get_compression_feedback, reset_compression_feedback, ) from headroom.cache.compression_store import ( - CompressionStore, RetrievalEvent, get_compression_store, reset_compression_store, ) +from headroom.telemetry import ToolSignature +from headroom.telemetry.toin import ( + TOINConfig, + ToolIntelligenceNetwork, + get_toin, + reset_toin, +) @pytest.fixture @@ -45,10 +40,12 @@ def fresh_toin(): reset_toin() with tempfile.TemporaryDirectory() as tmpdir: storage_path = str(Path(tmpdir) / "toin_test.json") - toin = get_toin(TOINConfig( - storage_path=storage_path, - auto_save_interval=0, - )) + toin = get_toin( + TOINConfig( + storage_path=storage_path, + auto_save_interval=0, + ) + ) yield toin reset_toin() @@ -420,7 +417,7 @@ class TestFieldRetrievalFrequencyWeighting: # field_b should come before field_c which should come before field_a b_idx = hint.preserve_fields.index(field_b) if field_b in hint.preserve_fields else -1 c_idx = hint.preserve_fields.index(field_c) if field_c in hint.preserve_fields else -1 - a_idx = hint.preserve_fields.index(field_a) if field_a in hint.preserve_fields else -1 + hint.preserve_fields.index(field_a) if field_a in hint.preserve_fields else -1 if b_idx >= 0 and c_idx >= 0: assert b_idx < c_idx, "Higher frequency field should come first" @@ -679,7 +676,7 @@ class TestIntegration: ) # Retrieve triggers feedback - entry = fresh_store.retrieve(hash_key, query="test query") + fresh_store.retrieve(hash_key, query="test query") # Verify feedback received the strategy pattern = fresh_feedback._tool_patterns.get("test_tool") diff --git a/tests/test_toin_integration.py b/tests/test_toin_integration.py index 3b974f26d..77fb0f12c 100644 --- a/tests/test_toin_integration.py +++ b/tests/test_toin_integration.py @@ -9,27 +9,27 @@ Tests the complete flow: """ import json -import pytest import tempfile from pathlib import Path +import pytest + from headroom.cache.compression_store import ( - CompressionStore, get_compression_store, reset_compression_store, ) -from headroom.transforms.smart_crusher import ( - SmartCrusher, - SmartCrusherConfig, -) from headroom.config import CCRConfig from headroom.telemetry import ToolSignature from headroom.telemetry.toin import ( - ToolIntelligenceNetwork, TOINConfig, + ToolIntelligenceNetwork, get_toin, reset_toin, ) +from headroom.transforms.smart_crusher import ( + SmartCrusher, + SmartCrusherConfig, +) @pytest.fixture @@ -38,10 +38,12 @@ def fresh_toin(): reset_toin() with tempfile.TemporaryDirectory() as tmpdir: storage_path = str(Path(tmpdir) / "toin.json") - toin = get_toin(TOINConfig( - storage_path=storage_path, - auto_save_interval=0, # No auto-persist during tests - )) + toin = get_toin( + TOINConfig( + storage_path=storage_path, + auto_save_interval=0, # No auto-persist during tests + ) + ) yield toin reset_toin() @@ -346,7 +348,9 @@ class TestStoreToTOINHash: # Get the stored hash entries = list(fresh_store._store.values()) - assert len(entries) >= 1, f"Should have stored entry. Modified: {was_modified}, Info: {info}" + assert len(entries) >= 1, ( + f"Should have stored entry. Modified: {was_modified}, Info: {info}" + ) stored_hash = entries[0].tool_signature_hash # Verify it matches ToolSignature diff --git a/tests/test_tokenizers.py b/tests/test_tokenizers.py index f955632e3..99de6d096 100644 --- a/tests/test_tokenizers.py +++ b/tests/test_tokenizers.py @@ -5,17 +5,17 @@ from __future__ import annotations import pytest from headroom.tokenizers import ( - TokenizerRegistry, - get_tokenizer, - register_tokenizer, - list_supported_models, - TiktokenCounter, - EstimatingTokenCounter, - CharacterCounter, - TokenCounter, BaseTokenizer, - is_mistral_tokenizer_available, + CharacterCounter, + EstimatingTokenCounter, + TiktokenCounter, + TokenCounter, + TokenizerRegistry, get_mistral_tokenizer, + get_tokenizer, + is_mistral_tokenizer_available, + list_supported_models, + register_tokenizer, ) @@ -66,14 +66,16 @@ class TestTiktokenCounter: {"role": "user", "content": "Search for Python"}, { "role": "assistant", - "tool_calls": [{ - "id": "call_123", - "type": "function", - "function": { - "name": "search", - "arguments": '{"query": "Python"}', - }, - }], + "tool_calls": [ + { + "id": "call_123", + "type": "function", + "function": { + "name": "search", + "arguments": '{"query": "Python"}', + }, + } + ], }, { "role": "tool", diff --git a/tests/test_transforms/test_cache_aligner.py b/tests/test_transforms/test_cache_aligner.py index 5157dc1cd..bed03acff 100644 --- a/tests/test_transforms/test_cache_aligner.py +++ b/tests/test_transforms/test_cache_aligner.py @@ -6,7 +6,6 @@ from headroom import OpenAIProvider, Tokenizer from headroom.config import CacheAlignerConfig, CachePrefixMetrics from headroom.transforms import CacheAligner - # Create a shared provider for tests _provider = OpenAIProvider() @@ -79,11 +78,7 @@ def system_prompt_with_multiple_dates(): @pytest.fixture def system_prompt_no_dates(): """System prompt without any date patterns.""" - return ( - "You are a helpful assistant. " - "Help users with their questions. " - "Be concise and accurate." - ) + return "You are a helpful assistant. Help users with their questions. Be concise and accurate." @pytest.fixture @@ -192,12 +187,7 @@ class TestDateExtraction: r"Build #\d+", # Build number ] - system_prompt = ( - "You are an assistant.\n" - "Version 1.2.3\n" - "Build #456\n" - "Help users." - ) + system_prompt = "You are an assistant.\nVersion 1.2.3\nBuild #456\nHelp users." messages = [ {"role": "system", "content": system_prompt}, @@ -323,7 +313,7 @@ class TestWhitespaceNormalization: date_patterns=[r"Line \d"], ) aligner = CacheAligner(config) - result = aligner.apply(messages, tokenizer) + aligner.apply(messages, tokenizer) # When normalization is disabled, CRLF should be preserved # (though dates are still extracted and reinserted) @@ -666,7 +656,10 @@ class TestApply: assert result.tokens_before > 0 assert result.tokens_after > 0 # Token count may change due to dynamic context addition - assert result.tokens_before != result.tokens_after or result.tokens_before == result.tokens_after + assert ( + result.tokens_before != result.tokens_after + or result.tokens_before == result.tokens_after + ) def test_apply_deep_copies_messages(self, tokenizer): """Test that apply does not modify original messages.""" diff --git a/tests/test_transforms/test_rolling_window.py b/tests/test_transforms/test_rolling_window.py index 1cb196cc1..8627716ef 100644 --- a/tests/test_transforms/test_rolling_window.py +++ b/tests/test_transforms/test_rolling_window.py @@ -1,13 +1,10 @@ """Tests for rolling window transform.""" -import json - import pytest -from headroom import OpenAIProvider, Tokenizer, RollingWindowConfig -from headroom.transforms import RollingWindow +from headroom import OpenAIProvider, RollingWindowConfig, Tokenizer from headroom.parser import find_tool_units - +from headroom.transforms import RollingWindow # Create a shared provider for tests _provider = OpenAIProvider() @@ -21,15 +18,22 @@ def get_tokenizer(model: str = "gpt-4o") -> Tokenizer: # Fixtures for realistic message scenarios + @pytest.fixture def messages_with_system(): """Messages with a system prompt.""" return [ - {"role": "system", "content": "You are a helpful assistant. You help users with their tasks."}, + { + "role": "system", + "content": "You are a helpful assistant. You help users with their tasks.", + }, {"role": "user", "content": "Hello, can you help me?"}, {"role": "assistant", "content": "Of course! What do you need help with?"}, {"role": "user", "content": "I need to analyze some data."}, - {"role": "assistant", "content": "I'd be happy to help analyze your data. What kind of data do you have?"}, + { + "role": "assistant", + "content": "I'd be happy to help analyze your data. What kind of data do you have?", + }, ] @@ -46,19 +50,19 @@ def messages_with_tool_calls(): { "id": "call_abc123", "type": "function", - "function": { - "name": "get_user", - "arguments": '{"user_id": "12345"}' - } + "function": {"name": "get_user", "arguments": '{"user_id": "12345"}'}, } - ] + ], }, { "role": "tool", "tool_call_id": "call_abc123", - "content": '{"id": "12345", "name": "Alice", "email": "alice@example.com", "status": "active"}' + "content": '{"id": "12345", "name": "Alice", "email": "alice@example.com", "status": "active"}', + }, + { + "role": "assistant", + "content": "I found the user. Alice (ID: 12345) is an active user with email alice@example.com.", }, - {"role": "assistant", "content": "I found the user. Alice (ID: 12345) is an active user with email alice@example.com."}, {"role": "user", "content": "Can you also find user 67890?"}, { "role": "assistant", @@ -67,17 +71,14 @@ def messages_with_tool_calls(): { "id": "call_def456", "type": "function", - "function": { - "name": "get_user", - "arguments": '{"user_id": "67890"}' - } + "function": {"name": "get_user", "arguments": '{"user_id": "67890"}'}, } - ] + ], }, { "role": "tool", "tool_call_id": "call_def456", - "content": '{"id": "67890", "name": "Bob", "email": "bob@example.com", "status": "inactive"}' + "content": '{"id": "67890", "name": "Bob", "email": "bob@example.com", "status": "inactive"}', }, {"role": "assistant", "content": "Found Bob (ID: 67890). This user is currently inactive."}, {"role": "user", "content": "Thanks for the help!"}, @@ -98,31 +99,17 @@ def messages_multiple_tool_calls(): { "id": "call_multi_1", "type": "function", - "function": { - "name": "search_user", - "arguments": '{"name": "Alice"}' - } + "function": {"name": "search_user", "arguments": '{"name": "Alice"}'}, }, { "id": "call_multi_2", "type": "function", - "function": { - "name": "search_user", - "arguments": '{"name": "Bob"}' - } - } - ] - }, - { - "role": "tool", - "tool_call_id": "call_multi_1", - "content": '{"id": "1", "name": "Alice"}' - }, - { - "role": "tool", - "tool_call_id": "call_multi_2", - "content": '{"id": "2", "name": "Bob"}' + "function": {"name": "search_user", "arguments": '{"name": "Bob"}'}, + }, + ], }, + {"role": "tool", "tool_call_id": "call_multi_1", "content": '{"id": "1", "name": "Alice"}'}, + {"role": "tool", "tool_call_id": "call_multi_2", "content": '{"id": "2", "name": "Bob"}'}, {"role": "assistant", "content": "I found both users."}, ] @@ -135,8 +122,12 @@ def long_conversation(): ] # Add 20 turns of conversation for i in range(20): - messages.append({"role": "user", "content": f"This is user message number {i}. " * 10}) # ~50 tokens each - messages.append({"role": "assistant", "content": f"This is assistant response number {i}. " * 10}) + messages.append( + {"role": "user", "content": f"This is user message number {i}. " * 10} + ) # ~50 tokens each + messages.append( + {"role": "assistant", "content": f"This is assistant response number {i}. " * 10} + ) return messages @@ -221,20 +212,20 @@ class TestRollingWindowProtection: { "id": "call_protected", "type": "function", - "function": {"name": "get_user", "arguments": '{"id": "999"}'} + "function": {"name": "get_user", "arguments": '{"id": "999"}'}, } - ] + ], }, { "role": "tool", "tool_call_id": "call_protected", - "content": '{"id": "999", "name": "Protected User"}' + "content": '{"id": "999", "name": "Protected User"}', }, {"role": "user", "content": "Thanks!"}, {"role": "assistant", "content": "You're welcome!"}, ] - result = window.apply( + window.apply( messages, tokenizer, model_limit=500, @@ -252,8 +243,13 @@ class TestRollingWindowProtection: for tc in msg.get("tool_calls", []): tc_id = tc.get("id") for j, other_msg in enumerate(messages): - if other_msg.get("role") == "tool" and other_msg.get("tool_call_id") == tc_id: - assert j in protected_indices, f"Tool response at {j} should be protected" + if ( + other_msg.get("role") == "tool" + and other_msg.get("tool_call_id") == tc_id + ): + assert j in protected_indices, ( + f"Tool response at {j} should be protected" + ) class TestDropPriority: @@ -268,7 +264,7 @@ class TestDropPriority: output_buffer_tokens=0, ) window = RollingWindow(config) - tokenizer = get_tokenizer() + get_tokenizer() # Check drop candidates ordering protected = window._get_protected_indices(messages_with_tool_calls) @@ -344,7 +340,8 @@ class TestDropPriority: tool_call_ids = {tc.get("id") for tc in msg.get("tool_calls", [])} # Find matching tool responses tool_responses = [ - m for m in result.messages + m + for m in result.messages if m.get("role") == "tool" and m.get("tool_call_id") in tool_call_ids ] # All tool calls should have their responses @@ -555,7 +552,9 @@ class TestMarkers: # All system messages should come before the marker for i in range(marker_idx): - assert result.messages[i].get("role") == "system" or " 0 - @pytest.mark.skip(reason="Bug in source: apply_rolling_window calls Tokenizer() without token_counter") + @pytest.mark.skip( + reason="Bug in source: apply_rolling_window calls Tokenizer() without token_counter" + ) def test_convenience_function_with_config(self, long_conversation): """The convenience function should accept a config.""" from headroom.transforms.rolling_window import apply_rolling_window diff --git a/tests/test_transforms/test_smart_crusher.py b/tests/test_transforms/test_smart_crusher.py index 7eaa085f4..238690e7d 100644 --- a/tests/test_transforms/test_smart_crusher.py +++ b/tests/test_transforms/test_smart_crusher.py @@ -8,7 +8,6 @@ Comprehensive tests covering: """ import json -from unittest.mock import Mock import pytest @@ -18,16 +17,13 @@ from headroom import ( SmartCrusherConfig, Tokenizer, ) -from headroom.relevance import BM25Scorer, RelevanceScore, RelevanceScorer +from headroom.relevance import RelevanceScore, RelevanceScorer from headroom.transforms.smart_crusher import ( - ArrayAnalysis, CompressionStrategy, - FieldStats, SmartAnalyzer, SmartCrusher, ) - # ============================================================================= # Test Fixtures # ============================================================================= @@ -84,11 +80,13 @@ def generate_time_series_data(n: int = 20, with_spike: bool = False) -> list[dic value = 100.0 + (i * 0.5) # Slight upward trend if with_spike and i == n // 2: value = 500.0 # Spike in the middle - data.append({ - "timestamp": f"2025-01-{(i % 28) + 1:02d}T12:00:00Z", - "value": value, - "metric": "cpu_usage", - }) + data.append( + { + "timestamp": f"2025-01-{(i % 28) + 1:02d}T12:00:00Z", + "value": value, + "metric": "cpu_usage", + } + ) return data @@ -103,11 +101,13 @@ def generate_log_data(n: int = 20, with_errors: bool = False) -> list[dict]: message = f"Connection failed: timeout after 30s (attempt {i})" else: message = f"Processing request {i} successfully" - data.append({ - "level": level, - "message": message, - "timestamp": f"2025-01-06T{12 + (i // 60):02d}:{i % 60:02d}:00Z", - }) + data.append( + { + "level": level, + "message": message, + "timestamp": f"2025-01-06T{12 + (i // 60):02d}:{i % 60:02d}:00Z", + } + ) return data @@ -235,11 +235,13 @@ class TestSmartAnalyzer: value = 100.0 + (i * 2.0) # Steady increase with variance if i == 20: value = 999.0 # Anomaly provides importance signal - items.append({ - "timestamp": f"2025-01-{(i % 28) + 1:02d}T12:00:00Z", - "value": value, - "metric": "cpu_usage", - }) + items.append( + { + "timestamp": f"2025-01-{(i % 28) + 1:02d}T12:00:00Z", + "value": value, + "metric": "cpu_usage", + } + ) result = analyzer.analyze_array(items) @@ -272,11 +274,13 @@ class TestSmartAnalyzer: value = 100.0 + (i * 0.5) # Values around 100-110 else: value = 300.0 + ((i - 20) * 0.5) # Values around 300-310 (jump) - items.append({ - "timestamp": f"2025-01-{(i % 28) + 1:02d}T12:00:00Z", - "value": value, - "metric": "cpu_usage", - }) + items.append( + { + "timestamp": f"2025-01-{(i % 28) + 1:02d}T12:00:00Z", + "value": value, + "metric": "cpu_usage", + } + ) result = analyzer.analyze_array(items) @@ -436,10 +440,12 @@ class TestSmartCrusher: # Add anomaly to provide importance signal for crushing if i == 25: value = 999.0 # Extreme anomaly - items.append({ - "timestamp": f"2025-01-{(i % 28) + 1:02d}T12:00:00Z", - "value": value, - }) + items.append( + { + "timestamp": f"2025-01-{(i % 28) + 1:02d}T12:00:00Z", + "value": value, + } + ) messages = [ {"role": "system", "content": "You are helpful."}, @@ -786,10 +792,7 @@ class TestRelevanceScoring: def test_relevance_keeps_matching_items(self, tokenizer): """Items matching user query should be preserved.""" # Create items where one matches the user query - items = [ - {"id": i, "name": f"User {i}", "email": f"user{i}@example.com"} - for i in range(30) - ] + items = [{"id": i, "name": f"User {i}", "email": f"user{i}@example.com"} for i in range(30)] # Add special user Alice items[15] = {"id": 15, "name": "Alice", "email": "alice@example.com"} @@ -1074,10 +1077,7 @@ class TestEdgeCases: signals, so it will be SKIPPED (not crushed) - which is correct behavior. The test verifies that null values don't crash the analyzer. """ - items = [ - {"id": i, "value": None if i % 2 == 0 else i * 10} - for i in range(20) - ] + items = [{"id": i, "value": None if i % 2 == 0 else i * 10} for i in range(20)] messages = [ {"role": "system", "content": "You are helpful."}, @@ -1112,7 +1112,11 @@ class TestEdgeCases: messages = [ {"role": "system", "content": "You are helpful."}, - {"role": "tool", "tool_call_id": "call_1", "content": json.dumps(items, ensure_ascii=False)}, + { + "role": "tool", + "tool_call_id": "call_1", + "content": json.dumps(items, ensure_ascii=False), + }, ] config = SmartCrusherConfig( @@ -1319,12 +1323,14 @@ class TestSmartCrusherIntegration: # Spike at index 30 if i == 30: cpu = 95.0 - items.append({ - "timestamp": f"2025-01-06T{10 + (i // 60):02d}:{i % 60:02d}:00Z", - "cpu_percent": cpu, - "memory_percent": 60.0, - "host": "server-01", - }) + items.append( + { + "timestamp": f"2025-01-06T{10 + (i // 60):02d}:{i % 60:02d}:00Z", + "cpu_percent": cpu, + "memory_percent": 60.0, + "host": "server-01", + } + ) messages = [ {"role": "system", "content": "You are a monitoring assistant."}, diff --git a/tests/test_transforms/test_tool_crusher.py b/tests/test_transforms/test_tool_crusher.py index 987b964dd..d7e98a106 100644 --- a/tests/test_transforms/test_tool_crusher.py +++ b/tests/test_transforms/test_tool_crusher.py @@ -2,12 +2,9 @@ import json -import pytest - from headroom import OpenAIProvider, Tokenizer, ToolCrusherConfig from headroom.transforms import ToolCrusher - # Create a shared provider for tests _provider = OpenAIProvider() @@ -120,7 +117,7 @@ class TestToolCrusher: def test_digest_marker_added(self): """Digest marker should be added to crushed content.""" - large_data = {"items": [i for i in range(100)]} + large_data = {"items": list(range(100))} messages = [ {"role": "system", "content": "You are helpful."}, diff --git a/uv.lock b/uv.lock new file mode 100644 index 000000000..bda7cd134 --- /dev/null +++ b/uv.lock @@ -0,0 +1,2259 @@ +version = 1 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version < '3.11'", +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/19320396573/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19320396572/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303 }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/14505990414/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://pypi.netflix.net/packages/14505990413/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anthropic" +version = "0.75.0" +source = { registry = "https://pypi.netflix.net/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "docstring-parser" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://pypi.netflix.net/packages/19384807028/anthropic-0.75.0.tar.gz", hash = "sha256:e8607422f4ab616db2ea5baacc215dd5f028da99ce2f022e33c7c535b29f3dfb", size = 439565 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19384807027/anthropic-0.75.0-py3-none-any.whl", hash = "sha256:ea8317271b6c15d80225a9f3c670152746e88805a7a61e14d4a374577164965b", size = 388164 }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.netflix.net/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://pypi.netflix.net/packages/19560988049/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19560988048/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592 }, +] + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/18876064296/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893 } +wheels = [ + { url = "https://pypi.netflix.net/packages/18876064295/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313 }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/19551676291/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19551676290/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/19200249251/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19200237212/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709 }, + { url = "https://pypi.netflix.net/packages/19200237213/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814 }, + { url = "https://pypi.netflix.net/packages/19200237214/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467 }, + { url = "https://pypi.netflix.net/packages/19200237215/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280 }, + { url = "https://pypi.netflix.net/packages/19200237216/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454 }, + { url = "https://pypi.netflix.net/packages/19200237217/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609 }, + { url = "https://pypi.netflix.net/packages/19200237218/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849 }, + { url = "https://pypi.netflix.net/packages/19200237219/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586 }, + { url = "https://pypi.netflix.net/packages/19200237220/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290 }, + { url = "https://pypi.netflix.net/packages/19200237221/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663 }, + { url = "https://pypi.netflix.net/packages/19200237222/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964 }, + { url = "https://pypi.netflix.net/packages/19200237223/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064 }, + { url = "https://pypi.netflix.net/packages/19200237224/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015 }, + { url = "https://pypi.netflix.net/packages/19200237225/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792 }, + { url = "https://pypi.netflix.net/packages/19200237226/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198 }, + { url = "https://pypi.netflix.net/packages/19200238505/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262 }, + { url = "https://pypi.netflix.net/packages/19200238506/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988 }, + { url = "https://pypi.netflix.net/packages/19200238507/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324 }, + { url = "https://pypi.netflix.net/packages/19200238508/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742 }, + { url = "https://pypi.netflix.net/packages/19200238509/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863 }, + { url = "https://pypi.netflix.net/packages/19200238510/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837 }, + { url = "https://pypi.netflix.net/packages/19200238511/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550 }, + { url = "https://pypi.netflix.net/packages/19200238512/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162 }, + { url = "https://pypi.netflix.net/packages/19200238513/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019 }, + { url = "https://pypi.netflix.net/packages/19200238514/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310 }, + { url = "https://pypi.netflix.net/packages/19200238515/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022 }, + { url = "https://pypi.netflix.net/packages/19200238516/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383 }, + { url = "https://pypi.netflix.net/packages/19200239807/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098 }, + { url = "https://pypi.netflix.net/packages/19200239808/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991 }, + { url = "https://pypi.netflix.net/packages/19200239809/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456 }, + { url = "https://pypi.netflix.net/packages/19200239810/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978 }, + { url = "https://pypi.netflix.net/packages/19200239811/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969 }, + { url = "https://pypi.netflix.net/packages/19200239812/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425 }, + { url = "https://pypi.netflix.net/packages/19200239813/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162 }, + { url = "https://pypi.netflix.net/packages/19200239814/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558 }, + { url = "https://pypi.netflix.net/packages/19200239815/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497 }, + { url = "https://pypi.netflix.net/packages/19200239816/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240 }, + { url = "https://pypi.netflix.net/packages/19200239817/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471 }, + { url = "https://pypi.netflix.net/packages/19200239818/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864 }, + { url = "https://pypi.netflix.net/packages/19200241121/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647 }, + { url = "https://pypi.netflix.net/packages/19200241122/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110 }, + { url = "https://pypi.netflix.net/packages/19200241123/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839 }, + { url = "https://pypi.netflix.net/packages/19200241124/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667 }, + { url = "https://pypi.netflix.net/packages/19200241125/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535 }, + { url = "https://pypi.netflix.net/packages/19200241126/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816 }, + { url = "https://pypi.netflix.net/packages/19200241127/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694 }, + { url = "https://pypi.netflix.net/packages/19200241128/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131 }, + { url = "https://pypi.netflix.net/packages/19200241129/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390 }, + { url = "https://pypi.netflix.net/packages/19200241130/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091 }, + { url = "https://pypi.netflix.net/packages/19200241131/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936 }, + { url = "https://pypi.netflix.net/packages/19200242445/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180 }, + { url = "https://pypi.netflix.net/packages/19200242446/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346 }, + { url = "https://pypi.netflix.net/packages/19200242447/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874 }, + { url = "https://pypi.netflix.net/packages/19200242448/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076 }, + { url = "https://pypi.netflix.net/packages/19200242449/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601 }, + { url = "https://pypi.netflix.net/packages/19200242450/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376 }, + { url = "https://pypi.netflix.net/packages/19200242451/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825 }, + { url = "https://pypi.netflix.net/packages/19200242452/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583 }, + { url = "https://pypi.netflix.net/packages/19200242453/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366 }, + { url = "https://pypi.netflix.net/packages/19200242454/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300 }, + { url = "https://pypi.netflix.net/packages/19200242455/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465 }, + { url = "https://pypi.netflix.net/packages/19200242456/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404 }, + { url = "https://pypi.netflix.net/packages/19200243782/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092 }, + { url = "https://pypi.netflix.net/packages/19200243783/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408 }, + { url = "https://pypi.netflix.net/packages/19200243784/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746 }, + { url = "https://pypi.netflix.net/packages/19200243785/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889 }, + { url = "https://pypi.netflix.net/packages/19200243786/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641 }, + { url = "https://pypi.netflix.net/packages/19200243787/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779 }, + { url = "https://pypi.netflix.net/packages/19200243788/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035 }, + { url = "https://pypi.netflix.net/packages/19200243789/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542 }, + { url = "https://pypi.netflix.net/packages/19200243790/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524 }, + { url = "https://pypi.netflix.net/packages/19200243791/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395 }, + { url = "https://pypi.netflix.net/packages/19200243792/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680 }, + { url = "https://pypi.netflix.net/packages/19200245129/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045 }, + { url = "https://pypi.netflix.net/packages/19200245130/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687 }, + { url = "https://pypi.netflix.net/packages/19200245131/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014 }, + { url = "https://pypi.netflix.net/packages/19200245132/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044 }, + { url = "https://pypi.netflix.net/packages/19200245133/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940 }, + { url = "https://pypi.netflix.net/packages/19200245134/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104 }, + { url = "https://pypi.netflix.net/packages/19200245135/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743 }, + { url = "https://pypi.netflix.net/packages/19200249250/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402 }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.netflix.net/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://pypi.netflix.net/packages/19343417910/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19343417909/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/988988/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://pypi.netflix.net/packages/988987/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "coverage" +version = "7.13.1" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/19531301260/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19531229535/coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147", size = 218633 }, + { url = "https://pypi.netflix.net/packages/19531229536/coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d", size = 219147 }, + { url = "https://pypi.netflix.net/packages/19531229537/coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0", size = 245894 }, + { url = "https://pypi.netflix.net/packages/19531229538/coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90", size = 247721 }, + { url = "https://pypi.netflix.net/packages/19531229539/coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d", size = 249585 }, + { url = "https://pypi.netflix.net/packages/19531229540/coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b", size = 246597 }, + { url = "https://pypi.netflix.net/packages/19531229541/coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6", size = 247626 }, + { url = "https://pypi.netflix.net/packages/19531236660/coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e", size = 245629 }, + { url = "https://pypi.netflix.net/packages/19531236661/coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae", size = 245901 }, + { url = "https://pypi.netflix.net/packages/19531236662/coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29", size = 246505 }, + { url = "https://pypi.netflix.net/packages/19531236663/coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f", size = 221257 }, + { url = "https://pypi.netflix.net/packages/19531236664/coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1", size = 222191 }, + { url = "https://pypi.netflix.net/packages/19531236665/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88", size = 218755 }, + { url = "https://pypi.netflix.net/packages/19531236666/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3", size = 219257 }, + { url = "https://pypi.netflix.net/packages/19531236667/coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9", size = 249657 }, + { url = "https://pypi.netflix.net/packages/19531245794/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee", size = 251581 }, + { url = "https://pypi.netflix.net/packages/19531245795/coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf", size = 253691 }, + { url = "https://pypi.netflix.net/packages/19531245796/coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3", size = 249799 }, + { url = "https://pypi.netflix.net/packages/19531245797/coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef", size = 251389 }, + { url = "https://pypi.netflix.net/packages/19531245798/coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851", size = 249450 }, + { url = "https://pypi.netflix.net/packages/19531245799/coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb", size = 249170 }, + { url = "https://pypi.netflix.net/packages/19531245800/coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba", size = 250081 }, + { url = "https://pypi.netflix.net/packages/19531245801/coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19", size = 221281 }, + { url = "https://pypi.netflix.net/packages/19531245802/coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a", size = 222215 }, + { url = "https://pypi.netflix.net/packages/19531245803/coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c", size = 220886 }, + { url = "https://pypi.netflix.net/packages/19531245804/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927 }, + { url = "https://pypi.netflix.net/packages/19531245805/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288 }, + { url = "https://pypi.netflix.net/packages/19531245806/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786 }, + { url = "https://pypi.netflix.net/packages/19531245807/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543 }, + { url = "https://pypi.netflix.net/packages/19531245808/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635 }, + { url = "https://pypi.netflix.net/packages/19531245809/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202 }, + { url = "https://pypi.netflix.net/packages/19531245810/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566 }, + { url = "https://pypi.netflix.net/packages/19531245811/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711 }, + { url = "https://pypi.netflix.net/packages/19531258156/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278 }, + { url = "https://pypi.netflix.net/packages/19531258157/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154 }, + { url = "https://pypi.netflix.net/packages/19531258158/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487 }, + { url = "https://pypi.netflix.net/packages/19531258159/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299 }, + { url = "https://pypi.netflix.net/packages/19531258160/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941 }, + { url = "https://pypi.netflix.net/packages/19531258161/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78", size = 218951 }, + { url = "https://pypi.netflix.net/packages/19531258162/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b", size = 219325 }, + { url = "https://pypi.netflix.net/packages/19531258163/coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd", size = 250309 }, + { url = "https://pypi.netflix.net/packages/19531258164/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992", size = 252907 }, + { url = "https://pypi.netflix.net/packages/19531265318/coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4", size = 254148 }, + { url = "https://pypi.netflix.net/packages/19531265319/coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a", size = 250515 }, + { url = "https://pypi.netflix.net/packages/19531265320/coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766", size = 252292 }, + { url = "https://pypi.netflix.net/packages/19531265321/coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4", size = 250242 }, + { url = "https://pypi.netflix.net/packages/19531265322/coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398", size = 250068 }, + { url = "https://pypi.netflix.net/packages/19531265323/coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784", size = 251846 }, + { url = "https://pypi.netflix.net/packages/19531265324/coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461", size = 221512 }, + { url = "https://pypi.netflix.net/packages/19531265325/coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500", size = 222321 }, + { url = "https://pypi.netflix.net/packages/19531265326/coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9", size = 220949 }, + { url = "https://pypi.netflix.net/packages/19531272489/coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc", size = 219643 }, + { url = "https://pypi.netflix.net/packages/19531272490/coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a", size = 219997 }, + { url = "https://pypi.netflix.net/packages/19531272491/coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4", size = 261296 }, + { url = "https://pypi.netflix.net/packages/19531272492/coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6", size = 263363 }, + { url = "https://pypi.netflix.net/packages/19531272493/coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1", size = 265783 }, + { url = "https://pypi.netflix.net/packages/19531272494/coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd", size = 260508 }, + { url = "https://pypi.netflix.net/packages/19531272495/coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c", size = 263357 }, + { url = "https://pypi.netflix.net/packages/19531272496/coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0", size = 260978 }, + { url = "https://pypi.netflix.net/packages/19531279667/coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e", size = 259877 }, + { url = "https://pypi.netflix.net/packages/19531279668/coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53", size = 262069 }, + { url = "https://pypi.netflix.net/packages/19531279669/coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842", size = 222184 }, + { url = "https://pypi.netflix.net/packages/19531279670/coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2", size = 223250 }, + { url = "https://pypi.netflix.net/packages/19531279671/coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09", size = 221521 }, + { url = "https://pypi.netflix.net/packages/19531279672/coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894", size = 218996 }, + { url = "https://pypi.netflix.net/packages/19531279673/coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a", size = 219326 }, + { url = "https://pypi.netflix.net/packages/19531279674/coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f", size = 250374 }, + { url = "https://pypi.netflix.net/packages/19531286853/coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909", size = 252882 }, + { url = "https://pypi.netflix.net/packages/19531286854/coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4", size = 254218 }, + { url = "https://pypi.netflix.net/packages/19531286855/coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75", size = 250391 }, + { url = "https://pypi.netflix.net/packages/19531286856/coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9", size = 252239 }, + { url = "https://pypi.netflix.net/packages/19531286857/coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465", size = 250196 }, + { url = "https://pypi.netflix.net/packages/19531286858/coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864", size = 250008 }, + { url = "https://pypi.netflix.net/packages/19531286859/coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9", size = 251671 }, + { url = "https://pypi.netflix.net/packages/19531286860/coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5", size = 221777 }, + { url = "https://pypi.netflix.net/packages/19531286861/coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a", size = 222592 }, + { url = "https://pypi.netflix.net/packages/19531286862/coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0", size = 221169 }, + { url = "https://pypi.netflix.net/packages/19531294051/coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a", size = 219730 }, + { url = "https://pypi.netflix.net/packages/19531294052/coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6", size = 220001 }, + { url = "https://pypi.netflix.net/packages/19531294053/coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673", size = 261370 }, + { url = "https://pypi.netflix.net/packages/19531294054/coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5", size = 263485 }, + { url = "https://pypi.netflix.net/packages/19531294055/coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d", size = 265890 }, + { url = "https://pypi.netflix.net/packages/19531294056/coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8", size = 260445 }, + { url = "https://pypi.netflix.net/packages/19531294057/coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486", size = 263357 }, + { url = "https://pypi.netflix.net/packages/19531301253/coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564", size = 260959 }, + { url = "https://pypi.netflix.net/packages/19531301254/coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7", size = 259792 }, + { url = "https://pypi.netflix.net/packages/19531301255/coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416", size = 262123 }, + { url = "https://pypi.netflix.net/packages/19531301256/coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f", size = 222562 }, + { url = "https://pypi.netflix.net/packages/19531301257/coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79", size = 223670 }, + { url = "https://pypi.netflix.net/packages/19531301258/coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4", size = 221707 }, + { url = "https://pypi.netflix.net/packages/19531301259/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722 }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/4292630514/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 } +wheels = [ + { url = "https://pypi.netflix.net/packages/4292630513/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 }, +] + +[[package]] +name = "docstring-parser" +version = "0.17.0" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/18890285851/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442 } +wheels = [ + { url = "https://pypi.netflix.net/packages/18890285850/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.netflix.net/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://pypi.netflix.net/packages/19374036393/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19374036392/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740 }, +] + +[[package]] +name = "fastapi" +version = "0.128.0" +source = { registry = "https://pypi.netflix.net/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://pypi.netflix.net/packages/19528748899/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19528748898/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094 }, +] + +[[package]] +name = "filelock" +version = "3.20.2" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/19547009507/filelock-3.20.2.tar.gz", hash = "sha256:a2241ff4ddde2a7cebddf78e39832509cb045d18ec1a09d7248d6bfc6bfbbe64", size = 19510 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19547009506/filelock-3.20.2-py3-none-any.whl", hash = "sha256:fbba7237d6ea277175a32c54bb71ef814a8546d8601269e1bfc388de333974e8", size = 16697 }, +] + +[[package]] +name = "fsspec" +version = "2025.12.0" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/19417522985/fsspec-2025.12.0.tar.gz", hash = "sha256:c505de011584597b1060ff778bb664c1bc022e87921b0e4f10cc9c44f9635973", size = 309748 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19417522984/fsspec-2025.12.0-py3-none-any.whl", hash = "sha256:8bf1fe301b7d8acfa6e8571e3b1c3d158f909666642431cc78a1b7b4dbc5ec5b", size = 201422 }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/18633116060/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } +wheels = [ + { url = "https://pypi.netflix.net/packages/18633116059/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, +] + +[[package]] +name = "headroom" +version = "0.2.0" +source = { editable = "." } +dependencies = [ + { name = "pydantic" }, + { name = "tiktoken" }, +] + +[package.optional-dependencies] +all = [ + { name = "fastapi" }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.netflix.net/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.0", source = { registry = "https://pypi.netflix.net/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sentence-transformers" }, + { name = "uvicorn" }, +] +dev = [ + { name = "anthropic" }, + { name = "mypy" }, + { name = "openai" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "ruff" }, +] +proxy = [ + { name = "fastapi" }, + { name = "httpx" }, + { name = "uvicorn" }, +] +relevance = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.netflix.net/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.0", source = { registry = "https://pypi.netflix.net/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sentence-transformers" }, +] +reports = [ + { name = "jinja2" }, +] + +[package.metadata] +requires-dist = [ + { name = "anthropic", marker = "extra == 'dev'", specifier = ">=0.18.0" }, + { name = "fastapi", marker = "extra == 'proxy'", specifier = ">=0.100.0" }, + { name = "headroom", extras = ["relevance", "proxy", "reports"], marker = "extra == 'all'" }, + { name = "httpx", marker = "extra == 'proxy'", specifier = ">=0.24.0" }, + { name = "jinja2", marker = "extra == 'reports'", specifier = ">=3.0.0" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.0.0" }, + { name = "numpy", marker = "extra == 'relevance'", specifier = ">=1.24.0" }, + { name = "openai", marker = "extra == 'dev'", specifier = ">=1.0.0" }, + { name = "pydantic", specifier = ">=2.0.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21.0" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" }, + { name = "sentence-transformers", marker = "extra == 'relevance'", specifier = ">=2.2.0" }, + { name = "tiktoken", specifier = ">=0.5.0" }, + { name = "uvicorn", marker = "extra == 'proxy'", specifier = ">=0.23.0" }, +] + +[[package]] +name = "hf-xet" +version = "1.2.0" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/19242501973/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19242499803/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", size = 2861870 }, + { url = "https://pypi.netflix.net/packages/19242499804/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", size = 2717584 }, + { url = "https://pypi.netflix.net/packages/19242499805/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", size = 3315004 }, + { url = "https://pypi.netflix.net/packages/19242499806/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", size = 3222636 }, + { url = "https://pypi.netflix.net/packages/19242499807/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448 }, + { url = "https://pypi.netflix.net/packages/19242501958/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401 }, + { url = "https://pypi.netflix.net/packages/19242501959/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", size = 2900866 }, + { url = "https://pypi.netflix.net/packages/19242499808/hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e", size = 2861861 }, + { url = "https://pypi.netflix.net/packages/19242499809/hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8", size = 2717699 }, + { url = "https://pypi.netflix.net/packages/19242499810/hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0", size = 3314885 }, + { url = "https://pypi.netflix.net/packages/19242499811/hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090", size = 3221550 }, + { url = "https://pypi.netflix.net/packages/19242501964/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a", size = 3408010 }, + { url = "https://pypi.netflix.net/packages/19242501965/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f", size = 3503264 }, + { url = "https://pypi.netflix.net/packages/19242506317/hf_xet-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc", size = 2901071 }, + { url = "https://pypi.netflix.net/packages/19242499812/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099 }, + { url = "https://pypi.netflix.net/packages/19242499813/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178 }, + { url = "https://pypi.netflix.net/packages/19242499814/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214 }, + { url = "https://pypi.netflix.net/packages/19242499815/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054 }, + { url = "https://pypi.netflix.net/packages/19242501970/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812 }, + { url = "https://pypi.netflix.net/packages/19242501971/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920 }, + { url = "https://pypi.netflix.net/packages/19242501972/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735 }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.netflix.net/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://pypi.netflix.net/packages/18635511306/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 } +wheels = [ + { url = "https://pypi.netflix.net/packages/18635511305/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.netflix.net/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://pypi.netflix.net/packages/17718101182/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +wheels = [ + { url = "https://pypi.netflix.net/packages/17718101181/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, +] + +[[package]] +name = "huggingface-hub" +version = "0.36.0" +source = { registry = "https://pypi.netflix.net/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://pypi.netflix.net/packages/19236406989/huggingface_hub-0.36.0.tar.gz", hash = "sha256:47b3f0e2539c39bf5cde015d63b72ec49baff67b6931c3d97f3f84532e2b8d25", size = 463358 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19236406988/huggingface_hub-0.36.0-py3-none-any.whl", hash = "sha256:7bcc9ad17d5b3f07b57c78e79d527102d08313caa278a641993acddcb894548d", size = 566094 }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/19194514269/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19194514268/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008 }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/19216517185/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19216517184/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.netflix.net/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://pypi.netflix.net/packages/18507965198/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } +wheels = [ + { url = "https://pypi.netflix.net/packages/18507965197/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, +] + +[[package]] +name = "jiter" +version = "0.12.0" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/19316163111/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19316149676/jiter-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:e7acbaba9703d5de82a2c98ae6a0f59ab9770ab5af5fa35e43a303aee962cf65", size = 316652 }, + { url = "https://pypi.netflix.net/packages/19316149677/jiter-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:364f1a7294c91281260364222f535bc427f56d4de1d8ffd718162d21fbbd602e", size = 319829 }, + { url = "https://pypi.netflix.net/packages/19316149678/jiter-0.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85ee4d25805d4fb23f0a5167a962ef8e002dbfb29c0989378488e32cf2744b62", size = 350568 }, + { url = "https://pypi.netflix.net/packages/19316149679/jiter-0.12.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:796f466b7942107eb889c08433b6e31b9a7ed31daceaecf8af1be26fb26c0ca8", size = 369052 }, + { url = "https://pypi.netflix.net/packages/19316149680/jiter-0.12.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35506cb71f47dba416694e67af996bbdefb8e3608f1f78799c2e1f9058b01ceb", size = 481585 }, + { url = "https://pypi.netflix.net/packages/19316149681/jiter-0.12.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:726c764a90c9218ec9e4f99a33d6bf5ec169163f2ca0fc21b654e88c2abc0abc", size = 380541 }, + { url = "https://pypi.netflix.net/packages/19316149682/jiter-0.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa47810c5565274810b726b0dc86d18dce5fd17b190ebdc3890851d7b2a0e74", size = 364423 }, + { url = "https://pypi.netflix.net/packages/19316150875/jiter-0.12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8ec0259d3f26c62aed4d73b198c53e316ae11f0f69c8fbe6682c6dcfa0fcce2", size = 389958 }, + { url = "https://pypi.netflix.net/packages/19316150876/jiter-0.12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:79307d74ea83465b0152fa23e5e297149506435535282f979f18b9033c0bb025", size = 522084 }, + { url = "https://pypi.netflix.net/packages/19316150877/jiter-0.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cf6e6dd18927121fec86739f1a8906944703941d000f0639f3eb6281cc601dca", size = 513054 }, + { url = "https://pypi.netflix.net/packages/19316150878/jiter-0.12.0-cp310-cp310-win32.whl", hash = "sha256:b6ae2aec8217327d872cbfb2c1694489057b9433afce447955763e6ab015b4c4", size = 206368 }, + { url = "https://pypi.netflix.net/packages/19316150879/jiter-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:c7f49ce90a71e44f7e1aa9e7ec415b9686bbc6a5961e57eab511015e6759bc11", size = 204847 }, + { url = "https://pypi.netflix.net/packages/19316150880/jiter-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8f8a7e317190b2c2d60eb2e8aa835270b008139562d70fe732e1c0020ec53c9", size = 316435 }, + { url = "https://pypi.netflix.net/packages/19316150881/jiter-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2218228a077e784c6c8f1a8e5d6b8cb1dea62ce25811c356364848554b2056cd", size = 320548 }, + { url = "https://pypi.netflix.net/packages/19316150882/jiter-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9354ccaa2982bf2188fd5f57f79f800ef622ec67beb8329903abf6b10da7d423", size = 351915 }, + { url = "https://pypi.netflix.net/packages/19316150883/jiter-0.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2607185ea89b4af9a604d4c7ec40e45d3ad03ee66998b031134bc510232bb7", size = 368966 }, + { url = "https://pypi.netflix.net/packages/19316152057/jiter-0.12.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a585a5e42d25f2e71db5f10b171f5e5ea641d3aa44f7df745aa965606111cc2", size = 482047 }, + { url = "https://pypi.netflix.net/packages/19316152058/jiter-0.12.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd9e21d34edff5a663c631f850edcb786719c960ce887a5661e9c828a53a95d9", size = 380835 }, + { url = "https://pypi.netflix.net/packages/19316152059/jiter-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a612534770470686cd5431478dc5a1b660eceb410abade6b1b74e320ca98de6", size = 364587 }, + { url = "https://pypi.netflix.net/packages/19316152060/jiter-0.12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3985aea37d40a908f887b34d05111e0aae822943796ebf8338877fee2ab67725", size = 390492 }, + { url = "https://pypi.netflix.net/packages/19316152061/jiter-0.12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b1207af186495f48f72529f8d86671903c8c10127cac6381b11dddc4aaa52df6", size = 522046 }, + { url = "https://pypi.netflix.net/packages/19316152062/jiter-0.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef2fb241de583934c9915a33120ecc06d94aa3381a134570f59eed784e87001e", size = 513392 }, + { url = "https://pypi.netflix.net/packages/19316152063/jiter-0.12.0-cp311-cp311-win32.whl", hash = "sha256:453b6035672fecce8007465896a25b28a6b59cfe8fbc974b2563a92f5a92a67c", size = 206096 }, + { url = "https://pypi.netflix.net/packages/19316152064/jiter-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:ca264b9603973c2ad9435c71a8ec8b49f8f715ab5ba421c85a51cde9887e421f", size = 204899 }, + { url = "https://pypi.netflix.net/packages/19316153246/jiter-0.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:cb00ef392e7d684f2754598c02c409f376ddcef857aae796d559e6cacc2d78a5", size = 188070 }, + { url = "https://pypi.netflix.net/packages/19316153247/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449 }, + { url = "https://pypi.netflix.net/packages/19316153248/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855 }, + { url = "https://pypi.netflix.net/packages/19316153249/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171 }, + { url = "https://pypi.netflix.net/packages/19316153250/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590 }, + { url = "https://pypi.netflix.net/packages/19316153251/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462 }, + { url = "https://pypi.netflix.net/packages/19316153252/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983 }, + { url = "https://pypi.netflix.net/packages/19316153253/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328 }, + { url = "https://pypi.netflix.net/packages/19316153254/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740 }, + { url = "https://pypi.netflix.net/packages/19316154445/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875 }, + { url = "https://pypi.netflix.net/packages/19316154446/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457 }, + { url = "https://pypi.netflix.net/packages/19316154447/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546 }, + { url = "https://pypi.netflix.net/packages/19316154448/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196 }, + { url = "https://pypi.netflix.net/packages/19316154449/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100 }, + { url = "https://pypi.netflix.net/packages/19316154450/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658 }, + { url = "https://pypi.netflix.net/packages/19316154451/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605 }, + { url = "https://pypi.netflix.net/packages/19316154452/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803 }, + { url = "https://pypi.netflix.net/packages/19316154453/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120 }, + { url = "https://pypi.netflix.net/packages/19316154454/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918 }, + { url = "https://pypi.netflix.net/packages/19316155655/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008 }, + { url = "https://pypi.netflix.net/packages/19316155656/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785 }, + { url = "https://pypi.netflix.net/packages/19316155657/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108 }, + { url = "https://pypi.netflix.net/packages/19316155658/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937 }, + { url = "https://pypi.netflix.net/packages/19316155659/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853 }, + { url = "https://pypi.netflix.net/packages/19316155660/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699 }, + { url = "https://pypi.netflix.net/packages/19316155661/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258 }, + { url = "https://pypi.netflix.net/packages/19316155662/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503 }, + { url = "https://pypi.netflix.net/packages/19316155663/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965 }, + { url = "https://pypi.netflix.net/packages/19316155664/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831 }, + { url = "https://pypi.netflix.net/packages/19316156875/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272 }, + { url = "https://pypi.netflix.net/packages/19316156876/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604 }, + { url = "https://pypi.netflix.net/packages/19316156877/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628 }, + { url = "https://pypi.netflix.net/packages/19316156878/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478 }, + { url = "https://pypi.netflix.net/packages/19316156879/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706 }, + { url = "https://pypi.netflix.net/packages/19316156880/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894 }, + { url = "https://pypi.netflix.net/packages/19316156881/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714 }, + { url = "https://pypi.netflix.net/packages/19316156882/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989 }, + { url = "https://pypi.netflix.net/packages/19316156883/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615 }, + { url = "https://pypi.netflix.net/packages/19316158103/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745 }, + { url = "https://pypi.netflix.net/packages/19316158104/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502 }, + { url = "https://pypi.netflix.net/packages/19316158105/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845 }, + { url = "https://pypi.netflix.net/packages/19316158106/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701 }, + { url = "https://pypi.netflix.net/packages/19316158107/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029 }, + { url = "https://pypi.netflix.net/packages/19316158108/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960 }, + { url = "https://pypi.netflix.net/packages/19316158109/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529 }, + { url = "https://pypi.netflix.net/packages/19316158110/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974 }, + { url = "https://pypi.netflix.net/packages/19316158111/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932 }, + { url = "https://pypi.netflix.net/packages/19316158112/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243 }, + { url = "https://pypi.netflix.net/packages/19316159342/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315 }, + { url = "https://pypi.netflix.net/packages/19316159343/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714 }, + { url = "https://pypi.netflix.net/packages/19316159344/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168 }, + { url = "https://pypi.netflix.net/packages/19316159345/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893 }, + { url = "https://pypi.netflix.net/packages/19316159346/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828 }, + { url = "https://pypi.netflix.net/packages/19316159347/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009 }, + { url = "https://pypi.netflix.net/packages/19316159348/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110 }, + { url = "https://pypi.netflix.net/packages/19316159349/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223 }, + { url = "https://pypi.netflix.net/packages/19316160587/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564 }, + { url = "https://pypi.netflix.net/packages/19316161847/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:4739a4657179ebf08f85914ce50332495811004cc1747852e8b2041ed2aab9b8", size = 311144 }, + { url = "https://pypi.netflix.net/packages/19316161848/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:41da8def934bf7bec16cb24bd33c0ca62126d2d45d81d17b864bd5ad721393c3", size = 305877 }, + { url = "https://pypi.netflix.net/packages/19316161849/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c44ee814f499c082e69872d426b624987dbc5943ab06e9bbaa4f81989fdb79e", size = 340419 }, + { url = "https://pypi.netflix.net/packages/19316161850/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2097de91cf03eaa27b3cbdb969addf83f0179c6afc41bbc4513705e013c65d", size = 347212 }, + { url = "https://pypi.netflix.net/packages/19316161851/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974 }, + { url = "https://pypi.netflix.net/packages/19316161852/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233 }, + { url = "https://pypi.netflix.net/packages/19316163109/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537 }, + { url = "https://pypi.netflix.net/packages/19316163110/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110 }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/19479936085/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19479936084/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071 }, +] + +[[package]] +name = "librt" +version = "0.7.7" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/19544948393/librt-0.7.7.tar.gz", hash = "sha256:81d957b069fed1890953c3b9c3895c7689960f233eea9a1d9607f71ce7f00b2c", size = 145910 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19544940501/librt-0.7.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4836c5645f40fbdc275e5670819bde5ab5f2e882290d304e3c6ddab1576a6d0", size = 54709 }, + { url = "https://pypi.netflix.net/packages/19544940502/librt-0.7.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ae8aec43117a645a31e5f60e9e3a0797492e747823b9bda6972d521b436b4e8", size = 56658 }, + { url = "https://pypi.netflix.net/packages/19544940503/librt-0.7.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:aea05f701ccd2a76b34f0daf47ca5068176ff553510b614770c90d76ac88df06", size = 161026 }, + { url = "https://pypi.netflix.net/packages/19544940504/librt-0.7.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b16ccaeff0ed4355dfb76fe1ea7a5d6d03b5ad27f295f77ee0557bc20a72495", size = 169529 }, + { url = "https://pypi.netflix.net/packages/19544940505/librt-0.7.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c48c7e150c095d5e3cea7452347ba26094be905d6099d24f9319a8b475fcd3e0", size = 183271 }, + { url = "https://pypi.netflix.net/packages/19544940506/librt-0.7.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4dcee2f921a8632636d1c37f1bbdb8841d15666d119aa61e5399c5268e7ce02e", size = 179039 }, + { url = "https://pypi.netflix.net/packages/19544940507/librt-0.7.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:14ef0f4ac3728ffd85bfc58e2f2f48fb4ef4fa871876f13a73a7381d10a9f77c", size = 173505 }, + { url = "https://pypi.netflix.net/packages/19544940508/librt-0.7.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e4ab69fa37f8090f2d971a5d2bc606c7401170dbdae083c393d6cbf439cb45b8", size = 193570 }, + { url = "https://pypi.netflix.net/packages/19544940509/librt-0.7.7-cp310-cp310-win32.whl", hash = "sha256:4bf3cc46d553693382d2abf5f5bd493d71bb0f50a7c0beab18aa13a5545c8900", size = 42600 }, + { url = "https://pypi.netflix.net/packages/19544940510/librt-0.7.7-cp310-cp310-win_amd64.whl", hash = "sha256:f0c8fe5aeadd8a0e5b0598f8a6ee3533135ca50fd3f20f130f9d72baf5c6ac58", size = 48977 }, + { url = "https://pypi.netflix.net/packages/19544940511/librt-0.7.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a487b71fbf8a9edb72a8c7a456dda0184642d99cd007bc819c0b7ab93676a8ee", size = 54709 }, + { url = "https://pypi.netflix.net/packages/19544940512/librt-0.7.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f4d4efb218264ecf0f8516196c9e2d1a0679d9fb3bb15df1155a35220062eba8", size = 56663 }, + { url = "https://pypi.netflix.net/packages/19544940513/librt-0.7.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b8bb331aad734b059c4b450cd0a225652f16889e286b2345af5e2c3c625c3d85", size = 161705 }, + { url = "https://pypi.netflix.net/packages/19544940514/librt-0.7.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:467dbd7443bda08338fc8ad701ed38cef48194017554f4c798b0a237904b3f99", size = 171029 }, + { url = "https://pypi.netflix.net/packages/19544940515/librt-0.7.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50d1d1ee813d2d1a3baf2873634ba506b263032418d16287c92ec1cc9c1a00cb", size = 184704 }, + { url = "https://pypi.netflix.net/packages/19544940516/librt-0.7.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c7e5070cf3ec92d98f57574da0224f8c73faf1ddd6d8afa0b8c9f6e86997bc74", size = 180719 }, + { url = "https://pypi.netflix.net/packages/19544940517/librt-0.7.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bdb9f3d865b2dafe7f9ad7f30ef563c80d0ddd2fdc8cc9b8e4f242f475e34d75", size = 174537 }, + { url = "https://pypi.netflix.net/packages/19544940518/librt-0.7.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8185c8497d45164e256376f9da5aed2bb26ff636c798c9dabe313b90e9f25b28", size = 195238 }, + { url = "https://pypi.netflix.net/packages/19544942058/librt-0.7.7-cp311-cp311-win32.whl", hash = "sha256:44d63ce643f34a903f09ff7ca355aae019a3730c7afd6a3c037d569beeb5d151", size = 42939 }, + { url = "https://pypi.netflix.net/packages/19544942059/librt-0.7.7-cp311-cp311-win_amd64.whl", hash = "sha256:7d13cc340b3b82134f8038a2bfe7137093693dcad8ba5773da18f95ad6b77a8a", size = 49240 }, + { url = "https://pypi.netflix.net/packages/19544942060/librt-0.7.7-cp311-cp311-win_arm64.whl", hash = "sha256:983de36b5a83fe9222f4f7dcd071f9b1ac6f3f17c0af0238dadfb8229588f890", size = 42613 }, + { url = "https://pypi.netflix.net/packages/19544942061/librt-0.7.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a85a1fc4ed11ea0eb0a632459ce004a2d14afc085a50ae3463cd3dfe1ce43fc", size = 55687 }, + { url = "https://pypi.netflix.net/packages/19544942062/librt-0.7.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c87654e29a35938baead1c4559858f346f4a2a7588574a14d784f300ffba0efd", size = 57136 }, + { url = "https://pypi.netflix.net/packages/19544942063/librt-0.7.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c9faaebb1c6212c20afd8043cd6ed9de0a47d77f91a6b5b48f4e46ed470703fe", size = 165320 }, + { url = "https://pypi.netflix.net/packages/19544942064/librt-0.7.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1908c3e5a5ef86b23391448b47759298f87f997c3bd153a770828f58c2bb4630", size = 174216 }, + { url = "https://pypi.netflix.net/packages/19544942065/librt-0.7.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dbc4900e95a98fc0729523be9d93a8fedebb026f32ed9ffc08acd82e3e181503", size = 189005 }, + { url = "https://pypi.netflix.net/packages/19544942066/librt-0.7.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a7ea4e1fbd253e5c68ea0fe63d08577f9d288a73f17d82f652ebc61fa48d878d", size = 183961 }, + { url = "https://pypi.netflix.net/packages/19544942067/librt-0.7.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ef7699b7a5a244b1119f85c5bbc13f152cd38240cbb2baa19b769433bae98e50", size = 177610 }, + { url = "https://pypi.netflix.net/packages/19544942068/librt-0.7.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:955c62571de0b181d9e9e0a0303c8bc90d47670a5eff54cf71bf5da61d1899cf", size = 199272 }, + { url = "https://pypi.netflix.net/packages/19544942069/librt-0.7.7-cp312-cp312-win32.whl", hash = "sha256:1bcd79be209313b270b0e1a51c67ae1af28adad0e0c7e84c3ad4b5cb57aaa75b", size = 43189 }, + { url = "https://pypi.netflix.net/packages/19544942070/librt-0.7.7-cp312-cp312-win_amd64.whl", hash = "sha256:4353ee891a1834567e0302d4bd5e60f531912179578c36f3d0430f8c5e16b456", size = 49462 }, + { url = "https://pypi.netflix.net/packages/19544943623/librt-0.7.7-cp312-cp312-win_arm64.whl", hash = "sha256:a76f1d679beccccdf8c1958e732a1dfcd6e749f8821ee59d7bec009ac308c029", size = 42828 }, + { url = "https://pypi.netflix.net/packages/19544943624/librt-0.7.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f4a0b0a3c86ba9193a8e23bb18f100d647bf192390ae195d84dfa0a10fb6244", size = 55746 }, + { url = "https://pypi.netflix.net/packages/19544943625/librt-0.7.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5335890fea9f9e6c4fdf8683061b9ccdcbe47c6dc03ab8e9b68c10acf78be78d", size = 57174 }, + { url = "https://pypi.netflix.net/packages/19544943626/librt-0.7.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b4346b1225be26def3ccc6c965751c74868f0578cbcba293c8ae9168483d811", size = 165834 }, + { url = "https://pypi.netflix.net/packages/19544943627/librt-0.7.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a10b8eebdaca6e9fdbaf88b5aefc0e324b763a5f40b1266532590d5afb268a4c", size = 174819 }, + { url = "https://pypi.netflix.net/packages/19544943628/librt-0.7.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:067be973d90d9e319e6eb4ee2a9b9307f0ecd648b8a9002fa237289a4a07a9e7", size = 189607 }, + { url = "https://pypi.netflix.net/packages/19544943629/librt-0.7.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:23d2299ed007812cccc1ecef018db7d922733382561230de1f3954db28433977", size = 184586 }, + { url = "https://pypi.netflix.net/packages/19544943630/librt-0.7.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6b6f8ea465524aa4c7420c7cc4ca7d46fe00981de8debc67b1cc2e9957bb5b9d", size = 178251 }, + { url = "https://pypi.netflix.net/packages/19544943631/librt-0.7.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8df32a99cc46eb0ee90afd9ada113ae2cafe7e8d673686cf03ec53e49635439", size = 199853 }, + { url = "https://pypi.netflix.net/packages/19544943632/librt-0.7.7-cp313-cp313-win32.whl", hash = "sha256:86f86b3b785487c7760247bcdac0b11aa8bf13245a13ed05206286135877564b", size = 43247 }, + { url = "https://pypi.netflix.net/packages/19544943633/librt-0.7.7-cp313-cp313-win_amd64.whl", hash = "sha256:4862cb2c702b1f905c0503b72d9d4daf65a7fdf5a9e84560e563471e57a56949", size = 49419 }, + { url = "https://pypi.netflix.net/packages/19544945197/librt-0.7.7-cp313-cp313-win_arm64.whl", hash = "sha256:0996c83b1cb43c00e8c87835a284f9057bc647abd42b5871e5f941d30010c832", size = 42828 }, + { url = "https://pypi.netflix.net/packages/19544945198/librt-0.7.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:23daa1ab0512bafdd677eb1bfc9611d8ffbe2e328895671e64cb34166bc1b8c8", size = 55188 }, + { url = "https://pypi.netflix.net/packages/19544945199/librt-0.7.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:558a9e5a6f3cc1e20b3168fb1dc802d0d8fa40731f6e9932dcc52bbcfbd37111", size = 56895 }, + { url = "https://pypi.netflix.net/packages/19544945200/librt-0.7.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2567cb48dc03e5b246927ab35cbb343376e24501260a9b5e30b8e255dca0d1d2", size = 163724 }, + { url = "https://pypi.netflix.net/packages/19544945201/librt-0.7.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6066c638cdf85ff92fc6f932d2d73c93a0e03492cdfa8778e6d58c489a3d7259", size = 172470 }, + { url = "https://pypi.netflix.net/packages/19544945202/librt-0.7.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a609849aca463074c17de9cda173c276eb8fee9e441053529e7b9e249dc8b8ee", size = 186806 }, + { url = "https://pypi.netflix.net/packages/19544945203/librt-0.7.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:add4e0a000858fe9bb39ed55f31085506a5c38363e6eb4a1e5943a10c2bfc3d1", size = 181809 }, + { url = "https://pypi.netflix.net/packages/19544945204/librt-0.7.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a3bfe73a32bd0bdb9a87d586b05a23c0a1729205d79df66dee65bb2e40d671ba", size = 175597 }, + { url = "https://pypi.netflix.net/packages/19544945205/librt-0.7.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0ecce0544d3db91a40f8b57ae26928c02130a997b540f908cefd4d279d6c5848", size = 196506 }, + { url = "https://pypi.netflix.net/packages/19544945206/librt-0.7.7-cp314-cp314-win32.whl", hash = "sha256:8f7a74cf3a80f0c3b0ec75b0c650b2f0a894a2cec57ef75f6f72c1e82cdac61d", size = 39747 }, + { url = "https://pypi.netflix.net/packages/19544945207/librt-0.7.7-cp314-cp314-win_amd64.whl", hash = "sha256:3d1fe2e8df3268dd6734dba33ededae72ad5c3a859b9577bc00b715759c5aaab", size = 45971 }, + { url = "https://pypi.netflix.net/packages/19544945208/librt-0.7.7-cp314-cp314-win_arm64.whl", hash = "sha256:2987cf827011907d3dfd109f1be0d61e173d68b1270107bb0e89f2fca7f2ed6b", size = 39075 }, + { url = "https://pypi.netflix.net/packages/19544945209/librt-0.7.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8e92c8de62b40bfce91d5e12c6e8b15434da268979b1af1a6589463549d491e6", size = 57368 }, + { url = "https://pypi.netflix.net/packages/19544946786/librt-0.7.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f683dcd49e2494a7535e30f779aa1ad6e3732a019d80abe1309ea91ccd3230e3", size = 59238 }, + { url = "https://pypi.netflix.net/packages/19544946787/librt-0.7.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b15e5d17812d4d629ff576699954f74e2cc24a02a4fc401882dd94f81daba45", size = 183870 }, + { url = "https://pypi.netflix.net/packages/19544946788/librt-0.7.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c084841b879c4d9b9fa34e5d5263994f21aea7fd9c6add29194dbb41a6210536", size = 194608 }, + { url = "https://pypi.netflix.net/packages/19544946789/librt-0.7.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c8fb9966f84737115513fecbaf257f9553d067a7dd45a69c2c7e5339e6a8dc", size = 206776 }, + { url = "https://pypi.netflix.net/packages/19544946790/librt-0.7.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5fb1ecb2c35362eab2dbd354fd1efa5a8440d3e73a68be11921042a0edc0ff", size = 203206 }, + { url = "https://pypi.netflix.net/packages/19544946791/librt-0.7.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d1454899909d63cc9199a89fcc4f81bdd9004aef577d4ffc022e600c412d57f3", size = 196697 }, + { url = "https://pypi.netflix.net/packages/19544946792/librt-0.7.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7ef28f2e7a016b29792fe0a2dd04dec75725b32a1264e390c366103f834a9c3a", size = 217193 }, + { url = "https://pypi.netflix.net/packages/19544946793/librt-0.7.7-cp314-cp314t-win32.whl", hash = "sha256:5e419e0db70991b6ba037b70c1d5bbe92b20ddf82f31ad01d77a347ed9781398", size = 40277 }, + { url = "https://pypi.netflix.net/packages/19544946794/librt-0.7.7-cp314-cp314t-win_amd64.whl", hash = "sha256:d6b7d93657332c817b8d674ef6bf1ab7796b4f7ce05e420fd45bd258a72ac804", size = 46765 }, + { url = "https://pypi.netflix.net/packages/19544946795/librt-0.7.7-cp314-cp314t-win_arm64.whl", hash = "sha256:142c2cd91794b79fd0ce113bd658993b7ede0fe93057668c2f98a45ca00b7e91", size = 39724 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/19138582262/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19138577022/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631 }, + { url = "https://pypi.netflix.net/packages/19138577023/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057 }, + { url = "https://pypi.netflix.net/packages/19138577024/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050 }, + { url = "https://pypi.netflix.net/packages/19138577025/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681 }, + { url = "https://pypi.netflix.net/packages/19138577026/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705 }, + { url = "https://pypi.netflix.net/packages/19138577027/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524 }, + { url = "https://pypi.netflix.net/packages/19138577028/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282 }, + { url = "https://pypi.netflix.net/packages/19138577029/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745 }, + { url = "https://pypi.netflix.net/packages/19138577030/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571 }, + { url = "https://pypi.netflix.net/packages/19138577031/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056 }, + { url = "https://pypi.netflix.net/packages/19138577032/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932 }, + { url = "https://pypi.netflix.net/packages/19138577033/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631 }, + { url = "https://pypi.netflix.net/packages/19138577034/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058 }, + { url = "https://pypi.netflix.net/packages/19138577891/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287 }, + { url = "https://pypi.netflix.net/packages/19138577892/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940 }, + { url = "https://pypi.netflix.net/packages/19138577893/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887 }, + { url = "https://pypi.netflix.net/packages/19138577894/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692 }, + { url = "https://pypi.netflix.net/packages/19138577895/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471 }, + { url = "https://pypi.netflix.net/packages/19138577896/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923 }, + { url = "https://pypi.netflix.net/packages/19138577897/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572 }, + { url = "https://pypi.netflix.net/packages/19138577898/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077 }, + { url = "https://pypi.netflix.net/packages/19138577899/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876 }, + { url = "https://pypi.netflix.net/packages/19138577900/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615 }, + { url = "https://pypi.netflix.net/packages/19138577901/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020 }, + { url = "https://pypi.netflix.net/packages/19138577902/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332 }, + { url = "https://pypi.netflix.net/packages/19138577903/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947 }, + { url = "https://pypi.netflix.net/packages/19138577904/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962 }, + { url = "https://pypi.netflix.net/packages/19138578744/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760 }, + { url = "https://pypi.netflix.net/packages/19138578745/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529 }, + { url = "https://pypi.netflix.net/packages/19138578746/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015 }, + { url = "https://pypi.netflix.net/packages/19138578747/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540 }, + { url = "https://pypi.netflix.net/packages/19138578748/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105 }, + { url = "https://pypi.netflix.net/packages/19138578749/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906 }, + { url = "https://pypi.netflix.net/packages/19138578750/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622 }, + { url = "https://pypi.netflix.net/packages/19138578751/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029 }, + { url = "https://pypi.netflix.net/packages/19138578752/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374 }, + { url = "https://pypi.netflix.net/packages/19138578753/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980 }, + { url = "https://pypi.netflix.net/packages/19138578754/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990 }, + { url = "https://pypi.netflix.net/packages/19138578755/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784 }, + { url = "https://pypi.netflix.net/packages/19138578756/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588 }, + { url = "https://pypi.netflix.net/packages/19138579600/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041 }, + { url = "https://pypi.netflix.net/packages/19138579601/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543 }, + { url = "https://pypi.netflix.net/packages/19138579602/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113 }, + { url = "https://pypi.netflix.net/packages/19138579603/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911 }, + { url = "https://pypi.netflix.net/packages/19138579604/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658 }, + { url = "https://pypi.netflix.net/packages/19138579605/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066 }, + { url = "https://pypi.netflix.net/packages/19138579606/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639 }, + { url = "https://pypi.netflix.net/packages/19138579607/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569 }, + { url = "https://pypi.netflix.net/packages/19138579608/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284 }, + { url = "https://pypi.netflix.net/packages/19138579609/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801 }, + { url = "https://pypi.netflix.net/packages/19138579610/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769 }, + { url = "https://pypi.netflix.net/packages/19138579611/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642 }, + { url = "https://pypi.netflix.net/packages/19138579612/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612 }, + { url = "https://pypi.netflix.net/packages/19138579613/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200 }, + { url = "https://pypi.netflix.net/packages/19138580471/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973 }, + { url = "https://pypi.netflix.net/packages/19138580472/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619 }, + { url = "https://pypi.netflix.net/packages/19138580473/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029 }, + { url = "https://pypi.netflix.net/packages/19138580474/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408 }, + { url = "https://pypi.netflix.net/packages/19138580475/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005 }, + { url = "https://pypi.netflix.net/packages/19138580476/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048 }, + { url = "https://pypi.netflix.net/packages/19138580477/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821 }, + { url = "https://pypi.netflix.net/packages/19138580478/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606 }, + { url = "https://pypi.netflix.net/packages/19138580479/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043 }, + { url = "https://pypi.netflix.net/packages/19138580480/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747 }, + { url = "https://pypi.netflix.net/packages/19138580481/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341 }, + { url = "https://pypi.netflix.net/packages/19138580482/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073 }, + { url = "https://pypi.netflix.net/packages/19138580483/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661 }, + { url = "https://pypi.netflix.net/packages/19138580484/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069 }, + { url = "https://pypi.netflix.net/packages/19138581356/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670 }, + { url = "https://pypi.netflix.net/packages/19138581357/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598 }, + { url = "https://pypi.netflix.net/packages/19138581358/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261 }, + { url = "https://pypi.netflix.net/packages/19138581359/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835 }, + { url = "https://pypi.netflix.net/packages/19138581360/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733 }, + { url = "https://pypi.netflix.net/packages/19138581361/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672 }, + { url = "https://pypi.netflix.net/packages/19138581362/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819 }, + { url = "https://pypi.netflix.net/packages/19138581363/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426 }, + { url = "https://pypi.netflix.net/packages/19138581364/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146 }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/594478178/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106 } +wheels = [ + { url = "https://pypi.netflix.net/packages/594478177/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198 }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.netflix.net/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://pypi.netflix.net/packages/19479007228/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19479001851/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333 }, + { url = "https://pypi.netflix.net/packages/19478998168/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102 }, + { url = "https://pypi.netflix.net/packages/19479005390/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799 }, + { url = "https://pypi.netflix.net/packages/19478998169/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149 }, + { url = "https://pypi.netflix.net/packages/19478999921/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105 }, + { url = "https://pypi.netflix.net/packages/19478999922/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200 }, + { url = "https://pypi.netflix.net/packages/19479007197/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539 }, + { url = "https://pypi.netflix.net/packages/19479005394/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163 }, + { url = "https://pypi.netflix.net/packages/19478998170/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629 }, + { url = "https://pypi.netflix.net/packages/19479003622/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933 }, + { url = "https://pypi.netflix.net/packages/19478998171/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754 }, + { url = "https://pypi.netflix.net/packages/19479005398/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772 }, + { url = "https://pypi.netflix.net/packages/19479007203/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053 }, + { url = "https://pypi.netflix.net/packages/19479005399/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134 }, + { url = "https://pypi.netflix.net/packages/19478999925/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616 }, + { url = "https://pypi.netflix.net/packages/19479007206/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847 }, + { url = "https://pypi.netflix.net/packages/19479003625/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976 }, + { url = "https://pypi.netflix.net/packages/19479003626/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104 }, + { url = "https://pypi.netflix.net/packages/19478998172/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927 }, + { url = "https://pypi.netflix.net/packages/19479001860/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730 }, + { url = "https://pypi.netflix.net/packages/19479003629/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581 }, + { url = "https://pypi.netflix.net/packages/19478999927/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252 }, + { url = "https://pypi.netflix.net/packages/19479001862/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848 }, + { url = "https://pypi.netflix.net/packages/19479001863/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510 }, + { url = "https://pypi.netflix.net/packages/19479005409/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744 }, + { url = "https://pypi.netflix.net/packages/19478999928/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815 }, + { url = "https://pypi.netflix.net/packages/19479005411/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047 }, + { url = "https://pypi.netflix.net/packages/19479003634/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998 }, + { url = "https://pypi.netflix.net/packages/19479003635/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476 }, + { url = "https://pypi.netflix.net/packages/19479001865/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872 }, + { url = "https://pypi.netflix.net/packages/19479001869/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/18629119459/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343 } +wheels = [ + { url = "https://pypi.netflix.net/packages/18629119458/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 }, +] + +[[package]] +name = "networkx" +version = "3.4.2" +source = { registry = "https://pypi.netflix.net/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://pypi.netflix.net/packages/17089382755/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368 } +wheels = [ + { url = "https://pypi.netflix.net/packages/17089382754/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263 }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.netflix.net/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +sdist = { url = "https://pypi.netflix.net/packages/19441125159/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19441125158/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504 }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.netflix.net/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://pypi.netflix.net/packages/18694941122/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440 } +wheels = [ + { url = "https://pypi.netflix.net/packages/18694702540/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245 }, + { url = "https://pypi.netflix.net/packages/18694705588/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048 }, + { url = "https://pypi.netflix.net/packages/18694705589/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542 }, + { url = "https://pypi.netflix.net/packages/18694708639/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301 }, + { url = "https://pypi.netflix.net/packages/18694711690/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320 }, + { url = "https://pypi.netflix.net/packages/18694714742/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050 }, + { url = "https://pypi.netflix.net/packages/18694717795/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034 }, + { url = "https://pypi.netflix.net/packages/18694720849/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185 }, + { url = "https://pypi.netflix.net/packages/18694720850/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149 }, + { url = "https://pypi.netflix.net/packages/18694723906/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620 }, + { url = "https://pypi.netflix.net/packages/18694726963/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963 }, + { url = "https://pypi.netflix.net/packages/18694730021/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743 }, + { url = "https://pypi.netflix.net/packages/18694730022/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616 }, + { url = "https://pypi.netflix.net/packages/18694733082/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579 }, + { url = "https://pypi.netflix.net/packages/18694736143/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005 }, + { url = "https://pypi.netflix.net/packages/18694739205/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570 }, + { url = "https://pypi.netflix.net/packages/18694742268/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548 }, + { url = "https://pypi.netflix.net/packages/18694745332/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521 }, + { url = "https://pypi.netflix.net/packages/18694745333/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866 }, + { url = "https://pypi.netflix.net/packages/18694749296/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455 }, + { url = "https://pypi.netflix.net/packages/18694755870/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348 }, + { url = "https://pypi.netflix.net/packages/18694763990/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362 }, + { url = "https://pypi.netflix.net/packages/18694763991/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103 }, + { url = "https://pypi.netflix.net/packages/18694767133/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382 }, + { url = "https://pypi.netflix.net/packages/18694772602/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462 }, + { url = "https://pypi.netflix.net/packages/18694775674/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618 }, + { url = "https://pypi.netflix.net/packages/18694778747/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511 }, + { url = "https://pypi.netflix.net/packages/18694781821/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783 }, + { url = "https://pypi.netflix.net/packages/18694781822/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506 }, + { url = "https://pypi.netflix.net/packages/18694784898/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190 }, + { url = "https://pypi.netflix.net/packages/18694787975/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828 }, + { url = "https://pypi.netflix.net/packages/18694791053/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006 }, + { url = "https://pypi.netflix.net/packages/18694791054/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765 }, + { url = "https://pypi.netflix.net/packages/18694791055/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736 }, + { url = "https://pypi.netflix.net/packages/18694794136/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719 }, + { url = "https://pypi.netflix.net/packages/18694797218/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072 }, + { url = "https://pypi.netflix.net/packages/18694800301/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213 }, + { url = "https://pypi.netflix.net/packages/18694803385/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632 }, + { url = "https://pypi.netflix.net/packages/18694828954/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532 }, + { url = "https://pypi.netflix.net/packages/18694833586/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885 }, + { url = "https://pypi.netflix.net/packages/18694806470/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467 }, + { url = "https://pypi.netflix.net/packages/18694809556/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144 }, + { url = "https://pypi.netflix.net/packages/18694809557/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217 }, + { url = "https://pypi.netflix.net/packages/18694809558/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014 }, + { url = "https://pypi.netflix.net/packages/18694812647/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935 }, + { url = "https://pypi.netflix.net/packages/18694815737/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122 }, + { url = "https://pypi.netflix.net/packages/18694818828/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143 }, + { url = "https://pypi.netflix.net/packages/18694821920/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260 }, + { url = "https://pypi.netflix.net/packages/18694821921/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225 }, + { url = "https://pypi.netflix.net/packages/18694825015/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374 }, + { url = "https://pypi.netflix.net/packages/18694841917/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391 }, + { url = "https://pypi.netflix.net/packages/18694841918/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754 }, + { url = "https://pypi.netflix.net/packages/18694846571/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476 }, + { url = "https://pypi.netflix.net/packages/18694846572/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666 }, +] + +[[package]] +name = "numpy" +version = "2.4.0" +source = { registry = "https://pypi.netflix.net/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +sdist = { url = "https://pypi.netflix.net/packages/19506581406/numpy-2.4.0.tar.gz", hash = "sha256:6e504f7b16118198f138ef31ba24d985b124c2c469fe8467007cf30fd992f934", size = 20685720 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19506539363/numpy-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:316b2f2584682318539f0bcaca5a496ce9ca78c88066579ebd11fd06f8e4741e", size = 16940166 }, + { url = "https://pypi.netflix.net/packages/19506539364/numpy-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2718c1de8504121714234b6f8241d0019450353276c88b9453c9c3d92e101db", size = 12641781 }, + { url = "https://pypi.netflix.net/packages/19506539365/numpy-2.4.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:21555da4ec4a0c942520ead42c3b0dc9477441e085c42b0fbdd6a084869a6f6b", size = 5470247 }, + { url = "https://pypi.netflix.net/packages/19506539366/numpy-2.4.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:413aa561266a4be2d06cd2b9665e89d9f54c543f418773076a76adcf2af08bc7", size = 6799807 }, + { url = "https://pypi.netflix.net/packages/19506539367/numpy-2.4.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0feafc9e03128074689183031181fac0897ff169692d8492066e949041096548", size = 14701992 }, + { url = "https://pypi.netflix.net/packages/19506539368/numpy-2.4.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8fdfed3deaf1928fb7667d96e0567cdf58c2b370ea2ee7e586aa383ec2cb346", size = 16646871 }, + { url = "https://pypi.netflix.net/packages/19506539369/numpy-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e06a922a469cae9a57100864caf4f8a97a1026513793969f8ba5b63137a35d25", size = 16487190 }, + { url = "https://pypi.netflix.net/packages/19506539370/numpy-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:927ccf5cd17c48f801f4ed43a7e5673a2724bd2171460be3e3894e6e332ef83a", size = 18580762 }, + { url = "https://pypi.netflix.net/packages/19506542843/numpy-2.4.0-cp311-cp311-win32.whl", hash = "sha256:882567b7ae57c1b1a0250208cc21a7976d8cbcc49d5a322e607e6f09c9e0bd53", size = 6233359 }, + { url = "https://pypi.netflix.net/packages/19506542844/numpy-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:8b986403023c8f3bf8f487c2e6186afda156174d31c175f747d8934dfddf3479", size = 12601132 }, + { url = "https://pypi.netflix.net/packages/19506542845/numpy-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:3f3096405acc48887458bbf9f6814d43785ac7ba2a57ea6442b581dedbc60ce6", size = 10573977 }, + { url = "https://pypi.netflix.net/packages/19506542846/numpy-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a8b6bb8369abefb8bd1801b054ad50e02b3275c8614dc6e5b0373c305291037", size = 16653117 }, + { url = "https://pypi.netflix.net/packages/19506542847/numpy-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e284ca13d5a8367e43734148622caf0b261b275673823593e3e3634a6490f83", size = 12369711 }, + { url = "https://pypi.netflix.net/packages/19506542848/numpy-2.4.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:49ff32b09f5aa0cd30a20c2b39db3e669c845589f2b7fc910365210887e39344", size = 5198355 }, + { url = "https://pypi.netflix.net/packages/19506542849/numpy-2.4.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:36cbfb13c152b1c7c184ddac43765db8ad672567e7bafff2cc755a09917ed2e6", size = 6545298 }, + { url = "https://pypi.netflix.net/packages/19506546331/numpy-2.4.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35ddc8f4914466e6fc954c76527aa91aa763682a4f6d73249ef20b418fe6effb", size = 14398387 }, + { url = "https://pypi.netflix.net/packages/19506546332/numpy-2.4.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc578891de1db95b2a35001b695451767b580bb45753717498213c5ff3c41d63", size = 16363091 }, + { url = "https://pypi.netflix.net/packages/19506546333/numpy-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98e81648e0b36e325ab67e46b5400a7a6d4a22b8a7c8e8bbfe20e7db7906bf95", size = 16176394 }, + { url = "https://pypi.netflix.net/packages/19506546334/numpy-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d57b5046c120561ba8fa8e4030fbb8b822f3063910fa901ffadf16e2b7128ad6", size = 18287378 }, + { url = "https://pypi.netflix.net/packages/19506546335/numpy-2.4.0-cp312-cp312-win32.whl", hash = "sha256:92190db305a6f48734d3982f2c60fa30d6b5ee9bff10f2887b930d7b40119f4c", size = 5955432 }, + { url = "https://pypi.netflix.net/packages/19506546336/numpy-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:680060061adb2d74ce352628cb798cfdec399068aa7f07ba9fb818b2b3305f98", size = 12306201 }, + { url = "https://pypi.netflix.net/packages/19506546337/numpy-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:39699233bc72dd482da1415dcb06076e32f60eddc796a796c5fb6c5efce94667", size = 10308234 }, + { url = "https://pypi.netflix.net/packages/19506550961/numpy-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a152d86a3ae00ba5f47b3acf3b827509fd0b6cb7d3259665e63dafbad22a75ea", size = 16649088 }, + { url = "https://pypi.netflix.net/packages/19506550962/numpy-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39b19251dec4de8ff8496cd0806cbe27bf0684f765abb1f4809554de93785f2d", size = 12364065 }, + { url = "https://pypi.netflix.net/packages/19506550963/numpy-2.4.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:009bd0ea12d3c784b6639a8457537016ce5172109e585338e11334f6a7bb88ee", size = 5192640 }, + { url = "https://pypi.netflix.net/packages/19506550964/numpy-2.4.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5fe44e277225fd3dff6882d86d3d447205d43532c3627313d17e754fb3905a0e", size = 6541556 }, + { url = "https://pypi.netflix.net/packages/19506550965/numpy-2.4.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f935c4493eda9069851058fa0d9e39dbf6286be690066509305e52912714dbb2", size = 14396562 }, + { url = "https://pypi.netflix.net/packages/19506550966/numpy-2.4.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cfa5f29a695cb7438965e6c3e8d06e0416060cf0d709c1b1c1653a939bf5c2a", size = 16351719 }, + { url = "https://pypi.netflix.net/packages/19506550967/numpy-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba0cb30acd3ef11c94dc27fbfba68940652492bc107075e7ffe23057f9425681", size = 16176053 }, + { url = "https://pypi.netflix.net/packages/19506554461/numpy-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60e8c196cd82cbbd4f130b5290007e13e6de3eca79f0d4d38014769d96a7c475", size = 18277859 }, + { url = "https://pypi.netflix.net/packages/19506554462/numpy-2.4.0-cp313-cp313-win32.whl", hash = "sha256:5f48cb3e88fbc294dc90e215d86fbaf1c852c63dbdb6c3a3e63f45c4b57f7344", size = 5953849 }, + { url = "https://pypi.netflix.net/packages/19506554463/numpy-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:a899699294f28f7be8992853c0c60741f16ff199205e2e6cdca155762cbaa59d", size = 12302840 }, + { url = "https://pypi.netflix.net/packages/19506554464/numpy-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:9198f447e1dc5647d07c9a6bbe2063cc0132728cc7175b39dbc796da5b54920d", size = 10308509 }, + { url = "https://pypi.netflix.net/packages/19506554465/numpy-2.4.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74623f2ab5cc3f7c886add4f735d1031a1d2be4a4ae63c0546cfd74e7a31ddf6", size = 12491815 }, + { url = "https://pypi.netflix.net/packages/19506554466/numpy-2.4.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:0804a8e4ab070d1d35496e65ffd3cf8114c136a2b81f61dfab0de4b218aacfd5", size = 5320321 }, + { url = "https://pypi.netflix.net/packages/19506554467/numpy-2.4.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:02a2038eb27f9443a8b266a66911e926566b5a6ffd1a689b588f7f35b81e7dc3", size = 6641635 }, + { url = "https://pypi.netflix.net/packages/19506560730/numpy-2.4.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1889b3a3f47a7b5bee16bc25a2145bd7cb91897f815ce3499db64c7458b6d91d", size = 14456053 }, + { url = "https://pypi.netflix.net/packages/19506560731/numpy-2.4.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85eef4cb5625c47ee6425c58a3502555e10f45ee973da878ac8248ad58c136f3", size = 16401702 }, + { url = "https://pypi.netflix.net/packages/19506560732/numpy-2.4.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6dc8b7e2f4eb184b37655195f421836cfae6f58197b67e3ffc501f1333d993fa", size = 16242493 }, + { url = "https://pypi.netflix.net/packages/19506560733/numpy-2.4.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:44aba2f0cafd287871a495fb3163408b0bd25bbce135c6f621534a07f4f7875c", size = 18324222 }, + { url = "https://pypi.netflix.net/packages/19506560734/numpy-2.4.0-cp313-cp313t-win32.whl", hash = "sha256:20c115517513831860c573996e395707aa9fb691eb179200125c250e895fcd93", size = 6076216 }, + { url = "https://pypi.netflix.net/packages/19506560735/numpy-2.4.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b48e35f4ab6f6a7597c46e301126ceba4c44cd3280e3750f85db48b082624fa4", size = 12444263 }, + { url = "https://pypi.netflix.net/packages/19506564242/numpy-2.4.0-cp313-cp313t-win_arm64.whl", hash = "sha256:4d1cfce39e511069b11e67cd0bd78ceff31443b7c9e5c04db73c7a19f572967c", size = 10378265 }, + { url = "https://pypi.netflix.net/packages/19506564243/numpy-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c95eb6db2884917d86cde0b4d4cf31adf485c8ec36bf8696dd66fa70de96f36b", size = 16647476 }, + { url = "https://pypi.netflix.net/packages/19506564244/numpy-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:65167da969cd1ec3a1df31cb221ca3a19a8aaa25370ecb17d428415e93c1935e", size = 12374563 }, + { url = "https://pypi.netflix.net/packages/19506564245/numpy-2.4.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3de19cfecd1465d0dcf8a5b5ea8b3155b42ed0b639dba4b71e323d74f2a3be5e", size = 5203107 }, + { url = "https://pypi.netflix.net/packages/19506564246/numpy-2.4.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6c05483c3136ac4c91b4e81903cb53a8707d316f488124d0398499a4f8e8ef51", size = 6538067 }, + { url = "https://pypi.netflix.net/packages/19506564247/numpy-2.4.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36667db4d6c1cea79c8930ab72fadfb4060feb4bfe724141cd4bd064d2e5f8ce", size = 14411926 }, + { url = "https://pypi.netflix.net/packages/19506564248/numpy-2.4.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a818668b674047fd88c4cddada7ab8f1c298812783e8328e956b78dc4807f9f", size = 16354295 }, + { url = "https://pypi.netflix.net/packages/19506567762/numpy-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1ee32359fb7543b7b7bd0b2f46294db27e29e7bbdf70541e81b190836cd83ded", size = 16190242 }, + { url = "https://pypi.netflix.net/packages/19506567763/numpy-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e493962256a38f58283de033d8af176c5c91c084ea30f15834f7545451c42059", size = 18280875 }, + { url = "https://pypi.netflix.net/packages/19506567764/numpy-2.4.0-cp314-cp314-win32.whl", hash = "sha256:6bbaebf0d11567fa8926215ae731e1d58e6ec28a8a25235b8a47405d301332db", size = 6002530 }, + { url = "https://pypi.netflix.net/packages/19506567765/numpy-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d857f55e7fdf7c38ab96c4558c95b97d1c685be6b05c249f5fdafcbd6f9899e", size = 12435890 }, + { url = "https://pypi.netflix.net/packages/19506567766/numpy-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:bb50ce5fb202a26fd5404620e7ef820ad1ab3558b444cb0b55beb7ef66cd2d63", size = 10591892 }, + { url = "https://pypi.netflix.net/packages/19506567767/numpy-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:355354388cba60f2132df297e2d53053d4063f79077b67b481d21276d61fc4df", size = 12494312 }, + { url = "https://pypi.netflix.net/packages/19506567768/numpy-2.4.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:1d8f9fde5f6dc1b6fc34df8162f3b3079365468703fee7f31d4e0cc8c63baed9", size = 5322862 }, + { url = "https://pypi.netflix.net/packages/19506573812/numpy-2.4.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e0434aa22c821f44eeb4c650b81c7fbdd8c0122c6c4b5a576a76d5a35625ecd9", size = 6644986 }, + { url = "https://pypi.netflix.net/packages/19506573813/numpy-2.4.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40483b2f2d3ba7aad426443767ff5632ec3156ef09742b96913787d13c336471", size = 14457958 }, + { url = "https://pypi.netflix.net/packages/19506573814/numpy-2.4.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6a7664ddd9746e20b7325351fe1a8408d0a2bf9c63b5e898290ddc8f09544", size = 16398394 }, + { url = "https://pypi.netflix.net/packages/19506573815/numpy-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ecb0019d44f4cdb50b676c5d0cb4b1eae8e15d1ed3d3e6639f986fc92b2ec52c", size = 16241044 }, + { url = "https://pypi.netflix.net/packages/19506573816/numpy-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d0ffd9e2e4441c96a9c91ec1783285d80bf835b677853fc2770a89d50c1e48ac", size = 18321772 }, + { url = "https://pypi.netflix.net/packages/19506573817/numpy-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:77f0d13fa87036d7553bf81f0e1fe3ce68d14c9976c9851744e4d3e91127e95f", size = 6148320 }, + { url = "https://pypi.netflix.net/packages/19506573818/numpy-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b1f5b45829ac1848893f0ddf5cb326110604d6df96cdc255b0bf9edd154104d4", size = 12623460 }, + { url = "https://pypi.netflix.net/packages/19506577348/numpy-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:23a3e9d1a6f360267e8fbb38ba5db355a6a7e9be71d7fce7ab3125e88bb646c8", size = 10661799 }, + { url = "https://pypi.netflix.net/packages/19506577349/numpy-2.4.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b54c83f1c0c0f1d748dca0af516062b8829d53d1f0c402be24b4257a9c48ada6", size = 16819003 }, + { url = "https://pypi.netflix.net/packages/19506577350/numpy-2.4.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:aabb081ca0ec5d39591fc33018cd4b3f96e1a2dd6756282029986d00a785fba4", size = 12567105 }, + { url = "https://pypi.netflix.net/packages/19506577351/numpy-2.4.0-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:8eafe7c36c8430b7794edeab3087dec7bf31d634d92f2af9949434b9d1964cba", size = 5395590 }, + { url = "https://pypi.netflix.net/packages/19506577352/numpy-2.4.0-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2f585f52b2baf07ff3356158d9268ea095e221371f1074fadea2f42544d58b4d", size = 6709947 }, + { url = "https://pypi.netflix.net/packages/19506577353/numpy-2.4.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32ed06d0fe9cae27d8fb5f400c63ccee72370599c75e683a6358dd3a4fb50aaf", size = 14535119 }, + { url = "https://pypi.netflix.net/packages/19506581404/numpy-2.4.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:57c540ed8fb1f05cb997c6761cd56db72395b0d6985e90571ff660452ade4f98", size = 16475815 }, + { url = "https://pypi.netflix.net/packages/19506581405/numpy-2.4.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a39fb973a726e63223287adc6dafe444ce75af952d711e400f3bf2b36ef55a7b", size = 12489376 }, +] + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.8.4.1" +source = { registry = "https://pypi.netflix.net/simple" } +wheels = [ + { url = "https://pypi.netflix.net/packages/18511519906/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b86f6dd8935884615a0683b663891d43781b819ac4f2ba2b0c9604676af346d0", size = 590537124 }, + { url = "https://pypi.netflix.net/packages/18511525243/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921 }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.8.90" +source = { registry = "https://pypi.netflix.net/simple" } +wheels = [ + { url = "https://pypi.netflix.net/packages/18511515409/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4412396548808ddfed3f17a467b104ba7751e6b58678a4b840675c56d21cf7ed", size = 9533318 }, + { url = "https://pypi.netflix.net/packages/18511515410/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621 }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.8.93" +source = { registry = "https://pypi.netflix.net/simple" } +wheels = [ + { url = "https://pypi.netflix.net/packages/18511515611/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029 }, + { url = "https://pypi.netflix.net/packages/18511515561/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fc1fec1e1637854b4c0a65fb9a8346b51dd9ee69e61ebaccc82058441f15bce8", size = 43106076 }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.8.90" +source = { registry = "https://pypi.netflix.net/simple" } +wheels = [ + { url = "https://pypi.netflix.net/packages/18511514733/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:52bf7bbee900262ffefe5e9d5a2a69a30d97e2bc5bb6cc866688caa976966e3d", size = 965265 }, + { url = "https://pypi.netflix.net/packages/18511514734/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765 }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "9.10.2.21" +source = { registry = "https://pypi.netflix.net/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12" }, +] +wheels = [ + { url = "https://pypi.netflix.net/packages/18753584630/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:c9132cc3f8958447b4910a1720036d9eff5928cc3179b0a51fb6d167c6cc87d8", size = 705026878 }, + { url = "https://pypi.netflix.net/packages/18753585888/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467 }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.3.3.83" +source = { registry = "https://pypi.netflix.net/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://pypi.netflix.net/packages/18511525802/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:848ef7224d6305cdb2a4df928759dca7b1201874787083b6e7550dd6765ce69a", size = 193109211 }, + { url = "https://pypi.netflix.net/packages/18511525922/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695 }, +] + +[[package]] +name = "nvidia-cufile-cu12" +version = "1.13.1.3" +source = { registry = "https://pypi.netflix.net/simple" } +wheels = [ + { url = "https://pypi.netflix.net/packages/18631702234/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834 }, + { url = "https://pypi.netflix.net/packages/18631702235/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:4beb6d4cce47c1a0f1013d72e02b0994730359e17801d395bdcbf20cfb3bb00a", size = 1120705 }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.9.90" +source = { registry = "https://pypi.netflix.net/simple" } +wheels = [ + { url = "https://pypi.netflix.net/packages/18511526023/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:dfab99248034673b779bc6decafdc3404a8a6f502462201f2f31f11354204acd", size = 63625754 }, + { url = "https://pypi.netflix.net/packages/18511526024/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976 }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.7.3.90" +source = { registry = "https://pypi.netflix.net/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cusparse-cu12" }, + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://pypi.netflix.net/packages/18511526071/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:db9ed69dbef9715071232caa9b69c52ac7de3a95773c2db65bdba85916e4e5c0", size = 267229841 }, + { url = "https://pypi.netflix.net/packages/18511526119/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905 }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.5.8.93" +source = { registry = "https://pypi.netflix.net/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://pypi.netflix.net/packages/18511526166/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b6c161cb130be1a07a27ea6923df8141f3c295852f4b260c65f18f3e0a091dc", size = 288117129 }, + { url = "https://pypi.netflix.net/packages/18511526214/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466 }, +] + +[[package]] +name = "nvidia-cusparselt-cu12" +version = "0.7.1" +source = { registry = "https://pypi.netflix.net/simple" } +wheels = [ + { url = "https://pypi.netflix.net/packages/18490505433/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8878dce784d0fac90131b6817b607e803c36e629ba34dc5b433471382196b6a5", size = 283835557 }, + { url = "https://pypi.netflix.net/packages/18490505423/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691 }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.27.5" +source = { registry = "https://pypi.netflix.net/simple" } +wheels = [ + { url = "https://pypi.netflix.net/packages/18818906507/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:31432ad4d1fb1004eb0c56203dc9bc2178a1ba69d1d9e02d64a6938ab5e40e7a", size = 322400625 }, + { url = "https://pypi.netflix.net/packages/18818906537/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229 }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.8.93" +source = { registry = "https://pypi.netflix.net/simple" } +wheels = [ + { url = "https://pypi.netflix.net/packages/18511526262/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836 }, + { url = "https://pypi.netflix.net/packages/18511526263/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:adccd7161ace7261e01bb91e44e88da350895c270d23f744f0820c818b7229e7", size = 38386204 }, +] + +[[package]] +name = "nvidia-nvshmem-cu12" +version = "3.3.20" +source = { registry = "https://pypi.netflix.net/simple" } +wheels = [ + { url = "https://pypi.netflix.net/packages/18938635540/nvidia_nvshmem_cu12-3.3.20-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0b0b960da3842212758e4fa4696b94f129090b30e5122fea3c5345916545cff0", size = 124484616 }, + { url = "https://pypi.netflix.net/packages/18938635558/nvidia_nvshmem_cu12-3.3.20-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d00f26d3f9b2e3c3065be895e3059d6479ea5c638a3f38c9fec49b1b9dd7c1e5", size = 124657145 }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.8.90" +source = { registry = "https://pypi.netflix.net/simple" } +wheels = [ + { url = "https://pypi.netflix.net/packages/18511516001/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d7ad891da111ebafbf7e015d34879f7112832fc239ff0d7d776b6cb685274615", size = 91161 }, + { url = "https://pypi.netflix.net/packages/18511516049/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954 }, +] + +[[package]] +name = "openai" +version = "2.14.0" +source = { registry = "https://pypi.netflix.net/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://pypi.netflix.net/packages/19500516862/openai-2.14.0.tar.gz", hash = "sha256:419357bedde9402d23bf8f2ee372fca1985a73348debba94bddff06f19459952", size = 626938 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19500516861/openai-2.14.0-py3-none-any.whl", hash = "sha256:7ea40aca4ffc4c4a776e77679021b47eec1160e341f42ae086ba949c9dcc9183", size = 1067558 }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/18622031468/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } +wheels = [ + { url = "https://pypi.netflix.net/packages/18622031467/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, +] + +[[package]] +name = "pathspec" +version = "1.0.2" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/19570732230/pathspec-1.0.2.tar.gz", hash = "sha256:fa32b1eb775ed9ba8d599b22c5f906dc098113989da2c00bf8b210078ca7fb92", size = 130502 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19570732229/pathspec-1.0.2-py3-none-any.whl", hash = "sha256:62f8558917908d237d399b9b338ef455a814801a4688bc41074b25feefd93472", size = 54844 }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/18687957487/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } +wheels = [ + { url = "https://pypi.netflix.net/packages/18687957486/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.netflix.net/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://pypi.netflix.net/packages/19391909805/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19391909804/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580 }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.netflix.net/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://pypi.netflix.net/packages/19290297566/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19289578916/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298 }, + { url = "https://pypi.netflix.net/packages/19289578917/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475 }, + { url = "https://pypi.netflix.net/packages/19289578918/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815 }, + { url = "https://pypi.netflix.net/packages/19289578919/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567 }, + { url = "https://pypi.netflix.net/packages/19289578920/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442 }, + { url = "https://pypi.netflix.net/packages/19289578921/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956 }, + { url = "https://pypi.netflix.net/packages/19289578922/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253 }, + { url = "https://pypi.netflix.net/packages/19289578923/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050 }, + { url = "https://pypi.netflix.net/packages/19289578924/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178 }, + { url = "https://pypi.netflix.net/packages/19289591694/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833 }, + { url = "https://pypi.netflix.net/packages/19289591695/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156 }, + { url = "https://pypi.netflix.net/packages/19289591696/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378 }, + { url = "https://pypi.netflix.net/packages/19289591697/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622 }, + { url = "https://pypi.netflix.net/packages/19289591698/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873 }, + { url = "https://pypi.netflix.net/packages/19289591699/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826 }, + { url = "https://pypi.netflix.net/packages/19289591700/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869 }, + { url = "https://pypi.netflix.net/packages/19289591701/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890 }, + { url = "https://pypi.netflix.net/packages/19289591702/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740 }, + { url = "https://pypi.netflix.net/packages/19289611287/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021 }, + { url = "https://pypi.netflix.net/packages/19289611288/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378 }, + { url = "https://pypi.netflix.net/packages/19289611289/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761 }, + { url = "https://pypi.netflix.net/packages/19289611290/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303 }, + { url = "https://pypi.netflix.net/packages/19289611291/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355 }, + { url = "https://pypi.netflix.net/packages/19289611292/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875 }, + { url = "https://pypi.netflix.net/packages/19289611293/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549 }, + { url = "https://pypi.netflix.net/packages/19289611294/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305 }, + { url = "https://pypi.netflix.net/packages/19289623685/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902 }, + { url = "https://pypi.netflix.net/packages/19289623686/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990 }, + { url = "https://pypi.netflix.net/packages/19289623687/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003 }, + { url = "https://pypi.netflix.net/packages/19289623688/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200 }, + { url = "https://pypi.netflix.net/packages/19289623689/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578 }, + { url = "https://pypi.netflix.net/packages/19289623690/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504 }, + { url = "https://pypi.netflix.net/packages/19289623691/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816 }, + { url = "https://pypi.netflix.net/packages/19289623692/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366 }, + { url = "https://pypi.netflix.net/packages/19289652502/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698 }, + { url = "https://pypi.netflix.net/packages/19289652503/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603 }, + { url = "https://pypi.netflix.net/packages/19289652504/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591 }, + { url = "https://pypi.netflix.net/packages/19289652505/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068 }, + { url = "https://pypi.netflix.net/packages/19289652506/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908 }, + { url = "https://pypi.netflix.net/packages/19289652507/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145 }, + { url = "https://pypi.netflix.net/packages/19289652508/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179 }, + { url = "https://pypi.netflix.net/packages/19289652509/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403 }, + { url = "https://pypi.netflix.net/packages/19289652510/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206 }, + { url = "https://pypi.netflix.net/packages/19289652511/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307 }, + { url = "https://pypi.netflix.net/packages/19289652512/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258 }, + { url = "https://pypi.netflix.net/packages/19289652513/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917 }, + { url = "https://pypi.netflix.net/packages/19289652514/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186 }, + { url = "https://pypi.netflix.net/packages/19289652515/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164 }, + { url = "https://pypi.netflix.net/packages/19289652516/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146 }, + { url = "https://pypi.netflix.net/packages/19289665326/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788 }, + { url = "https://pypi.netflix.net/packages/19289665327/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133 }, + { url = "https://pypi.netflix.net/packages/19289665328/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852 }, + { url = "https://pypi.netflix.net/packages/19289665329/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679 }, + { url = "https://pypi.netflix.net/packages/19289665330/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766 }, + { url = "https://pypi.netflix.net/packages/19289665331/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005 }, + { url = "https://pypi.netflix.net/packages/19289665332/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622 }, + { url = "https://pypi.netflix.net/packages/19289665333/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725 }, + { url = "https://pypi.netflix.net/packages/19289682151/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040 }, + { url = "https://pypi.netflix.net/packages/19289682152/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691 }, + { url = "https://pypi.netflix.net/packages/19289682153/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897 }, + { url = "https://pypi.netflix.net/packages/19289682154/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302 }, + { url = "https://pypi.netflix.net/packages/19289682155/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877 }, + { url = "https://pypi.netflix.net/packages/19289690978/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680 }, + { url = "https://pypi.netflix.net/packages/19289690979/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960 }, + { url = "https://pypi.netflix.net/packages/19289690980/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102 }, + { url = "https://pypi.netflix.net/packages/19289690981/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039 }, + { url = "https://pypi.netflix.net/packages/19289690982/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126 }, + { url = "https://pypi.netflix.net/packages/19289690983/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489 }, + { url = "https://pypi.netflix.net/packages/19289690984/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288 }, + { url = "https://pypi.netflix.net/packages/19289690985/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255 }, + { url = "https://pypi.netflix.net/packages/19289703816/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760 }, + { url = "https://pypi.netflix.net/packages/19289703817/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092 }, + { url = "https://pypi.netflix.net/packages/19289703818/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385 }, + { url = "https://pypi.netflix.net/packages/19289703819/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832 }, + { url = "https://pypi.netflix.net/packages/19289703820/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585 }, + { url = "https://pypi.netflix.net/packages/19289703821/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078 }, + { url = "https://pypi.netflix.net/packages/19289703822/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914 }, + { url = "https://pypi.netflix.net/packages/19289703823/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560 }, + { url = "https://pypi.netflix.net/packages/19289716662/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244 }, + { url = "https://pypi.netflix.net/packages/19289716663/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955 }, + { url = "https://pypi.netflix.net/packages/19289716664/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906 }, + { url = "https://pypi.netflix.net/packages/19289716665/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607 }, + { url = "https://pypi.netflix.net/packages/19289716666/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769 }, + { url = "https://pypi.netflix.net/packages/19289755228/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441 }, + { url = "https://pypi.netflix.net/packages/19289755229/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291 }, + { url = "https://pypi.netflix.net/packages/19289755230/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632 }, + { url = "https://pypi.netflix.net/packages/19289755231/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905 }, + { url = "https://pypi.netflix.net/packages/19289755232/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495 }, + { url = "https://pypi.netflix.net/packages/19289775023/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388 }, + { url = "https://pypi.netflix.net/packages/19289775024/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879 }, + { url = "https://pypi.netflix.net/packages/19289775025/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017 }, + { url = "https://pypi.netflix.net/packages/19289775026/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351 }, + { url = "https://pypi.netflix.net/packages/19289775027/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363 }, + { url = "https://pypi.netflix.net/packages/19289775028/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615 }, + { url = "https://pypi.netflix.net/packages/19289775029/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369 }, + { url = "https://pypi.netflix.net/packages/19289775030/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218 }, + { url = "https://pypi.netflix.net/packages/19289775031/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951 }, + { url = "https://pypi.netflix.net/packages/19289775032/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428 }, + { url = "https://pypi.netflix.net/packages/19289775033/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009 }, + { url = "https://pypi.netflix.net/packages/19289798632/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980 }, + { url = "https://pypi.netflix.net/packages/19289798633/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865 }, + { url = "https://pypi.netflix.net/packages/19289798634/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256 }, + { url = "https://pypi.netflix.net/packages/19289798635/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762 }, + { url = "https://pypi.netflix.net/packages/19289824837/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141 }, + { url = "https://pypi.netflix.net/packages/19289824838/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317 }, + { url = "https://pypi.netflix.net/packages/19289824839/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992 }, + { url = "https://pypi.netflix.net/packages/19289824840/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302 }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/18805843371/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 } +wheels = [ + { url = "https://pypi.netflix.net/packages/18805843370/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.netflix.net/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://pypi.netflix.net/packages/19434856402/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19434856401/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801 }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.netflix.net/simple" } +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://pypi.netflix.net/packages/19318832271/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19318832270/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075 }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.netflix.net/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://pypi.netflix.net/packages/19062775858/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19062775857/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/19130520952/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19130514450/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227 }, + { url = "https://pypi.netflix.net/packages/19130514451/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019 }, + { url = "https://pypi.netflix.net/packages/19130514452/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646 }, + { url = "https://pypi.netflix.net/packages/19130514453/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793 }, + { url = "https://pypi.netflix.net/packages/19130514454/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293 }, + { url = "https://pypi.netflix.net/packages/19130514455/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872 }, + { url = "https://pypi.netflix.net/packages/19130514456/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828 }, + { url = "https://pypi.netflix.net/packages/19130514457/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415 }, + { url = "https://pypi.netflix.net/packages/19130515018/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561 }, + { url = "https://pypi.netflix.net/packages/19130515019/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826 }, + { url = "https://pypi.netflix.net/packages/19130515020/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577 }, + { url = "https://pypi.netflix.net/packages/19130515021/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556 }, + { url = "https://pypi.netflix.net/packages/19130515022/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114 }, + { url = "https://pypi.netflix.net/packages/19130515023/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638 }, + { url = "https://pypi.netflix.net/packages/19130515024/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463 }, + { url = "https://pypi.netflix.net/packages/19130515025/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986 }, + { url = "https://pypi.netflix.net/packages/19130515026/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543 }, + { url = "https://pypi.netflix.net/packages/19130515027/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763 }, + { url = "https://pypi.netflix.net/packages/19130515028/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063 }, + { url = "https://pypi.netflix.net/packages/19130515600/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973 }, + { url = "https://pypi.netflix.net/packages/19130515601/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116 }, + { url = "https://pypi.netflix.net/packages/19130515602/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011 }, + { url = "https://pypi.netflix.net/packages/19130515603/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870 }, + { url = "https://pypi.netflix.net/packages/19130515604/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089 }, + { url = "https://pypi.netflix.net/packages/19130515605/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181 }, + { url = "https://pypi.netflix.net/packages/19130515606/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658 }, + { url = "https://pypi.netflix.net/packages/19130515607/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003 }, + { url = "https://pypi.netflix.net/packages/19130515608/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344 }, + { url = "https://pypi.netflix.net/packages/19130515609/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669 }, + { url = "https://pypi.netflix.net/packages/19130515610/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252 }, + { url = "https://pypi.netflix.net/packages/19130516193/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081 }, + { url = "https://pypi.netflix.net/packages/19130516194/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159 }, + { url = "https://pypi.netflix.net/packages/19130516195/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626 }, + { url = "https://pypi.netflix.net/packages/19130516196/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613 }, + { url = "https://pypi.netflix.net/packages/19130516197/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115 }, + { url = "https://pypi.netflix.net/packages/19130516198/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427 }, + { url = "https://pypi.netflix.net/packages/19130516199/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090 }, + { url = "https://pypi.netflix.net/packages/19130516200/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246 }, + { url = "https://pypi.netflix.net/packages/19130516201/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814 }, + { url = "https://pypi.netflix.net/packages/19130516202/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809 }, + { url = "https://pypi.netflix.net/packages/19130516203/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454 }, + { url = "https://pypi.netflix.net/packages/19130516204/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355 }, + { url = "https://pypi.netflix.net/packages/19130519711/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175 }, + { url = "https://pypi.netflix.net/packages/19130519712/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228 }, + { url = "https://pypi.netflix.net/packages/19130519713/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194 }, + { url = "https://pypi.netflix.net/packages/19130520317/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429 }, + { url = "https://pypi.netflix.net/packages/19130520318/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912 }, + { url = "https://pypi.netflix.net/packages/19130519714/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108 }, + { url = "https://pypi.netflix.net/packages/19130519715/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641 }, + { url = "https://pypi.netflix.net/packages/19130519716/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901 }, + { url = "https://pypi.netflix.net/packages/19130519717/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132 }, + { url = "https://pypi.netflix.net/packages/19130519718/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261 }, + { url = "https://pypi.netflix.net/packages/19130519719/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272 }, + { url = "https://pypi.netflix.net/packages/19130520325/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923 }, + { url = "https://pypi.netflix.net/packages/19130520326/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062 }, + { url = "https://pypi.netflix.net/packages/19130520327/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341 }, +] + +[[package]] +name = "regex" +version = "2025.11.3" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/19286227790/regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", size = 414669 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19286102455/regex-2025.11.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2b441a4ae2c8049106e8b39973bfbddfb25a179dda2bdb99b0eeb60c40a6a3af", size = 488087 }, + { url = "https://pypi.netflix.net/packages/19286102456/regex-2025.11.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2fa2eed3f76677777345d2f81ee89f5de2f5745910e805f7af7386a920fa7313", size = 290544 }, + { url = "https://pypi.netflix.net/packages/19286102457/regex-2025.11.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d8b4a27eebd684319bdf473d39f1d79eed36bf2cd34bd4465cdb4618d82b3d56", size = 288408 }, + { url = "https://pypi.netflix.net/packages/19286102458/regex-2025.11.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cf77eac15bd264986c4a2c63353212c095b40f3affb2bc6b4ef80c4776c1a28", size = 781584 }, + { url = "https://pypi.netflix.net/packages/19286102459/regex-2025.11.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b7f9ee819f94c6abfa56ec7b1dbab586f41ebbdc0a57e6524bd5e7f487a878c7", size = 850733 }, + { url = "https://pypi.netflix.net/packages/19286102460/regex-2025.11.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:838441333bc90b829406d4a03cb4b8bf7656231b84358628b0406d803931ef32", size = 898691 }, + { url = "https://pypi.netflix.net/packages/19286102461/regex-2025.11.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfe6d3f0c9e3b7e8c0c694b24d25e677776f5ca26dce46fd6b0489f9c8339391", size = 791662 }, + { url = "https://pypi.netflix.net/packages/19286102462/regex-2025.11.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2ab815eb8a96379a27c3b6157fcb127c8f59c36f043c1678110cea492868f1d5", size = 782587 }, + { url = "https://pypi.netflix.net/packages/19286102463/regex-2025.11.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:728a9d2d173a65b62bdc380b7932dd8e74ed4295279a8fe1021204ce210803e7", size = 774709 }, + { url = "https://pypi.netflix.net/packages/19286102464/regex-2025.11.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:509dc827f89c15c66a0c216331260d777dd6c81e9a4e4f830e662b0bb296c313", size = 845773 }, + { url = "https://pypi.netflix.net/packages/19286102465/regex-2025.11.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:849202cd789e5f3cf5dcc7822c34b502181b4824a65ff20ce82da5524e45e8e9", size = 836164 }, + { url = "https://pypi.netflix.net/packages/19286102466/regex-2025.11.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b6f78f98741dcc89607c16b1e9426ee46ce4bf31ac5e6b0d40e81c89f3481ea5", size = 779832 }, + { url = "https://pypi.netflix.net/packages/19286102467/regex-2025.11.3-cp310-cp310-win32.whl", hash = "sha256:149eb0bba95231fb4f6d37c8f760ec9fa6fabf65bab555e128dde5f2475193ec", size = 265802 }, + { url = "https://pypi.netflix.net/packages/19286115976/regex-2025.11.3-cp310-cp310-win_amd64.whl", hash = "sha256:ee3a83ce492074c35a74cc76cf8235d49e77b757193a5365ff86e3f2f93db9fd", size = 277722 }, + { url = "https://pypi.netflix.net/packages/19286115977/regex-2025.11.3-cp310-cp310-win_arm64.whl", hash = "sha256:38af559ad934a7b35147716655d4a2f79fcef2d695ddfe06a06ba40ae631fa7e", size = 270289 }, + { url = "https://pypi.netflix.net/packages/19286115978/regex-2025.11.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eadade04221641516fa25139273505a1c19f9bf97589a05bc4cfcd8b4a618031", size = 488081 }, + { url = "https://pypi.netflix.net/packages/19286115979/regex-2025.11.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:feff9e54ec0dd3833d659257f5c3f5322a12eee58ffa360984b716f8b92983f4", size = 290554 }, + { url = "https://pypi.netflix.net/packages/19286115980/regex-2025.11.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3b30bc921d50365775c09a7ed446359e5c0179e9e2512beec4a60cbcef6ddd50", size = 288407 }, + { url = "https://pypi.netflix.net/packages/19286115981/regex-2025.11.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f99be08cfead2020c7ca6e396c13543baea32343b7a9a5780c462e323bd8872f", size = 793418 }, + { url = "https://pypi.netflix.net/packages/19286115982/regex-2025.11.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6dd329a1b61c0ee95ba95385fb0c07ea0d3fe1a21e1349fa2bec272636217118", size = 860448 }, + { url = "https://pypi.netflix.net/packages/19286120648/regex-2025.11.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c5238d32f3c5269d9e87be0cf096437b7622b6920f5eac4fd202468aaeb34d2", size = 907139 }, + { url = "https://pypi.netflix.net/packages/19286120649/regex-2025.11.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10483eefbfb0adb18ee9474498c9a32fcf4e594fbca0543bb94c48bac6183e2e", size = 800439 }, + { url = "https://pypi.netflix.net/packages/19286120650/regex-2025.11.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:78c2d02bb6e1da0720eedc0bad578049cad3f71050ef8cd065ecc87691bed2b0", size = 782965 }, + { url = "https://pypi.netflix.net/packages/19286120651/regex-2025.11.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e6b49cd2aad93a1790ce9cffb18964f6d3a4b0b3dbdbd5de094b65296fce6e58", size = 854398 }, + { url = "https://pypi.netflix.net/packages/19286120652/regex-2025.11.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:885b26aa3ee56433b630502dc3d36ba78d186a00cc535d3806e6bfd9ed3c70ab", size = 845897 }, + { url = "https://pypi.netflix.net/packages/19286120653/regex-2025.11.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ddd76a9f58e6a00f8772e72cff8ebcff78e022be95edf018766707c730593e1e", size = 788906 }, + { url = "https://pypi.netflix.net/packages/19286120654/regex-2025.11.3-cp311-cp311-win32.whl", hash = "sha256:3e816cc9aac1cd3cc9a4ec4d860f06d40f994b5c7b4d03b93345f44e08cc68bf", size = 265812 }, + { url = "https://pypi.netflix.net/packages/19286120655/regex-2025.11.3-cp311-cp311-win_amd64.whl", hash = "sha256:087511f5c8b7dfbe3a03f5d5ad0c2a33861b1fc387f21f6f60825a44865a385a", size = 277737 }, + { url = "https://pypi.netflix.net/packages/19286120656/regex-2025.11.3-cp311-cp311-win_arm64.whl", hash = "sha256:1ff0d190c7f68ae7769cd0313fe45820ba07ffebfddfaa89cc1eb70827ba0ddc", size = 270290 }, + { url = "https://pypi.netflix.net/packages/19286120657/regex-2025.11.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bc8ab71e2e31b16e40868a40a69007bc305e1109bd4658eb6cad007e0bf67c41", size = 489312 }, + { url = "https://pypi.netflix.net/packages/19286120658/regex-2025.11.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:22b29dda7e1f7062a52359fca6e58e548e28c6686f205e780b02ad8ef710de36", size = 291256 }, + { url = "https://pypi.netflix.net/packages/19286120659/regex-2025.11.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a91e4a29938bc1a082cc28fdea44be420bf2bebe2665343029723892eb073e1", size = 288921 }, + { url = "https://pypi.netflix.net/packages/19286140275/regex-2025.11.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08b884f4226602ad40c5d55f52bf91a9df30f513864e0054bad40c0e9cf1afb7", size = 798568 }, + { url = "https://pypi.netflix.net/packages/19286140276/regex-2025.11.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3e0b11b2b2433d1c39c7c7a30e3f3d0aeeea44c2a8d0bae28f6b95f639927a69", size = 864165 }, + { url = "https://pypi.netflix.net/packages/19286140277/regex-2025.11.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87eb52a81ef58c7ba4d45c3ca74e12aa4b4e77816f72ca25258a85b3ea96cb48", size = 912182 }, + { url = "https://pypi.netflix.net/packages/19286140278/regex-2025.11.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a12ab1f5c29b4e93db518f5e3872116b7e9b1646c9f9f426f777b50d44a09e8c", size = 803501 }, + { url = "https://pypi.netflix.net/packages/19286140279/regex-2025.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7521684c8c7c4f6e88e35ec89680ee1aa8358d3f09d27dfbdf62c446f5d4c695", size = 787842 }, + { url = "https://pypi.netflix.net/packages/19286140280/regex-2025.11.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7fe6e5440584e94cc4b3f5f4d98a25e29ca12dccf8873679a635638349831b98", size = 858519 }, + { url = "https://pypi.netflix.net/packages/19286140281/regex-2025.11.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8e026094aa12b43f4fd74576714e987803a315c76edb6b098b9809db5de58f74", size = 850611 }, + { url = "https://pypi.netflix.net/packages/19286140282/regex-2025.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:435bbad13e57eb5606a68443af62bed3556de2f46deb9f7d4237bc2f1c9fb3a0", size = 789759 }, + { url = "https://pypi.netflix.net/packages/19286140283/regex-2025.11.3-cp312-cp312-win32.whl", hash = "sha256:3839967cf4dc4b985e1570fd8d91078f0c519f30491c60f9ac42a8db039be204", size = 266194 }, + { url = "https://pypi.netflix.net/packages/19286140284/regex-2025.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:e721d1b46e25c481dc5ded6f4b3f66c897c58d2e8cfdf77bbced84339108b0b9", size = 277069 }, + { url = "https://pypi.netflix.net/packages/19286146972/regex-2025.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:64350685ff08b1d3a6fff33f45a9ca183dc1d58bbfe4981604e70ec9801bbc26", size = 270330 }, + { url = "https://pypi.netflix.net/packages/19286146973/regex-2025.11.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c1e448051717a334891f2b9a620fe36776ebf3dd8ec46a0b877c8ae69575feb4", size = 489081 }, + { url = "https://pypi.netflix.net/packages/19286146974/regex-2025.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b5aca4d5dfd7fbfbfbdaf44850fcc7709a01146a797536a8f84952e940cca76", size = 291123 }, + { url = "https://pypi.netflix.net/packages/19286146975/regex-2025.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:04d2765516395cf7dda331a244a3282c0f5ae96075f728629287dfa6f76ba70a", size = 288814 }, + { url = "https://pypi.netflix.net/packages/19286146976/regex-2025.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d9903ca42bfeec4cebedba8022a7c97ad2aab22e09573ce9976ba01b65e4361", size = 798592 }, + { url = "https://pypi.netflix.net/packages/19286146977/regex-2025.11.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:639431bdc89d6429f6721625e8129413980ccd62e9d3f496be618a41d205f160", size = 864122 }, + { url = "https://pypi.netflix.net/packages/19286146978/regex-2025.11.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f117efad42068f9715677c8523ed2be1518116d1c49b1dd17987716695181efe", size = 912272 }, + { url = "https://pypi.netflix.net/packages/19286146979/regex-2025.11.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4aecb6f461316adf9f1f0f6a4a1a3d79e045f9b71ec76055a791affa3b285850", size = 803497 }, + { url = "https://pypi.netflix.net/packages/19286153675/regex-2025.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b3a5f320136873cc5561098dfab677eea139521cb9a9e8db98b7e64aef44cbc", size = 787892 }, + { url = "https://pypi.netflix.net/packages/19286153676/regex-2025.11.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:75fa6f0056e7efb1f42a1c34e58be24072cb9e61a601340cc1196ae92326a4f9", size = 858462 }, + { url = "https://pypi.netflix.net/packages/19286153677/regex-2025.11.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:dbe6095001465294f13f1adcd3311e50dd84e5a71525f20a10bd16689c61ce0b", size = 850528 }, + { url = "https://pypi.netflix.net/packages/19286153678/regex-2025.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:454d9b4ae7881afbc25015b8627c16d88a597479b9dea82b8c6e7e2e07240dc7", size = 789866 }, + { url = "https://pypi.netflix.net/packages/19286153679/regex-2025.11.3-cp313-cp313-win32.whl", hash = "sha256:28ba4d69171fc6e9896337d4fc63a43660002b7da53fc15ac992abcf3410917c", size = 266189 }, + { url = "https://pypi.netflix.net/packages/19286153680/regex-2025.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:bac4200befe50c670c405dc33af26dad5a3b6b255dd6c000d92fe4629f9ed6a5", size = 277054 }, + { url = "https://pypi.netflix.net/packages/19286153681/regex-2025.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:2292cd5a90dab247f9abe892ac584cb24f0f54680c73fcb4a7493c66c2bf2467", size = 270325 }, + { url = "https://pypi.netflix.net/packages/19286160531/regex-2025.11.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1eb1ebf6822b756c723e09f5186473d93236c06c579d2cc0671a722d2ab14281", size = 491984 }, + { url = "https://pypi.netflix.net/packages/19286160532/regex-2025.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1e00ec2970aab10dc5db34af535f21fcf32b4a31d99e34963419636e2f85ae39", size = 292673 }, + { url = "https://pypi.netflix.net/packages/19286160533/regex-2025.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a4cb042b615245d5ff9b3794f56be4138b5adc35a4166014d31d1814744148c7", size = 291029 }, + { url = "https://pypi.netflix.net/packages/19286160534/regex-2025.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44f264d4bf02f3176467d90b294d59bf1db9fe53c141ff772f27a8b456b2a9ed", size = 807437 }, + { url = "https://pypi.netflix.net/packages/19286160535/regex-2025.11.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7be0277469bf3bd7a34a9c57c1b6a724532a0d235cd0dc4e7f4316f982c28b19", size = 873368 }, + { url = "https://pypi.netflix.net/packages/19286160536/regex-2025.11.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0d31e08426ff4b5b650f68839f5af51a92a5b51abd8554a60c2fbc7c71f25d0b", size = 914921 }, + { url = "https://pypi.netflix.net/packages/19286160537/regex-2025.11.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e43586ce5bd28f9f285a6e729466841368c4a0353f6fd08d4ce4630843d3648a", size = 812708 }, + { url = "https://pypi.netflix.net/packages/19286160538/regex-2025.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0f9397d561a4c16829d4e6ff75202c1c08b68a3bdbfe29dbfcdb31c9830907c6", size = 795472 }, + { url = "https://pypi.netflix.net/packages/19286160539/regex-2025.11.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:dd16e78eb18ffdb25ee33a0682d17912e8cc8a770e885aeee95020046128f1ce", size = 868341 }, + { url = "https://pypi.netflix.net/packages/19286168189/regex-2025.11.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:ffcca5b9efe948ba0661e9df0fa50d2bc4b097c70b9810212d6b62f05d83b2dd", size = 854666 }, + { url = "https://pypi.netflix.net/packages/19286168190/regex-2025.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c56b4d162ca2b43318ac671c65bd4d563e841a694ac70e1a976ac38fcf4ca1d2", size = 799473 }, + { url = "https://pypi.netflix.net/packages/19286168191/regex-2025.11.3-cp313-cp313t-win32.whl", hash = "sha256:9ddc42e68114e161e51e272f667d640f97e84a2b9ef14b7477c53aac20c2d59a", size = 268792 }, + { url = "https://pypi.netflix.net/packages/19286168192/regex-2025.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7a7c7fdf755032ffdd72c77e3d8096bdcb0eb92e89e17571a196f03d88b11b3c", size = 280214 }, + { url = "https://pypi.netflix.net/packages/19286168193/regex-2025.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:df9eb838c44f570283712e7cff14c16329a9f0fb19ca492d21d4b7528ee6821e", size = 271469 }, + { url = "https://pypi.netflix.net/packages/19286168194/regex-2025.11.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9697a52e57576c83139d7c6f213d64485d3df5bf84807c35fa409e6c970801c6", size = 489089 }, + { url = "https://pypi.netflix.net/packages/19286168195/regex-2025.11.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e18bc3f73bd41243c9b38a6d9f2366cd0e0137a9aebe2d8ff76c5b67d4c0a3f4", size = 291059 }, + { url = "https://pypi.netflix.net/packages/19286168196/regex-2025.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:61a08bcb0ec14ff4e0ed2044aad948d0659604f824cbd50b55e30b0ec6f09c73", size = 288900 }, + { url = "https://pypi.netflix.net/packages/19286183117/regex-2025.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9c30003b9347c24bcc210958c5d167b9e4f9be786cb380a7d32f14f9b84674f", size = 799010 }, + { url = "https://pypi.netflix.net/packages/19286183118/regex-2025.11.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4e1e592789704459900728d88d41a46fe3969b82ab62945560a31732ffc19a6d", size = 864893 }, + { url = "https://pypi.netflix.net/packages/19286183119/regex-2025.11.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6538241f45eb5a25aa575dbba1069ad786f68a4f2773a29a2bd3dd1f9de787be", size = 911522 }, + { url = "https://pypi.netflix.net/packages/19286183120/regex-2025.11.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce22519c989bb72a7e6b36a199384c53db7722fe669ba891da75907fe3587db", size = 803272 }, + { url = "https://pypi.netflix.net/packages/19286183121/regex-2025.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:66d559b21d3640203ab9075797a55165d79017520685fb407b9234d72ab63c62", size = 787958 }, + { url = "https://pypi.netflix.net/packages/19286183122/regex-2025.11.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:669dcfb2e38f9e8c69507bace46f4889e3abbfd9b0c29719202883c0a603598f", size = 859289 }, + { url = "https://pypi.netflix.net/packages/19286183123/regex-2025.11.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:32f74f35ff0f25a5021373ac61442edcb150731fbaa28286bbc8bb1582c89d02", size = 850026 }, + { url = "https://pypi.netflix.net/packages/19286183124/regex-2025.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e6c7a21dffba883234baefe91bc3388e629779582038f75d2a5be918e250f0ed", size = 789499 }, + { url = "https://pypi.netflix.net/packages/19286189859/regex-2025.11.3-cp314-cp314-win32.whl", hash = "sha256:795ea137b1d809eb6836b43748b12634291c0ed55ad50a7d72d21edf1cd565c4", size = 271604 }, + { url = "https://pypi.netflix.net/packages/19286189860/regex-2025.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f95fbaa0ee1610ec0fc6b26668e9917a582ba80c52cc6d9ada15e30aa9ab9ad", size = 280320 }, + { url = "https://pypi.netflix.net/packages/19286189861/regex-2025.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:dfec44d532be4c07088c3de2876130ff0fbeeacaa89a137decbbb5f665855a0f", size = 273372 }, + { url = "https://pypi.netflix.net/packages/19286189862/regex-2025.11.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ba0d8a5d7f04f73ee7d01d974d47c5834f8a1b0224390e4fe7c12a3a92a78ecc", size = 491985 }, + { url = "https://pypi.netflix.net/packages/19286189863/regex-2025.11.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:442d86cf1cfe4faabf97db7d901ef58347efd004934da045c745e7b5bd57ac49", size = 292669 }, + { url = "https://pypi.netflix.net/packages/19286189864/regex-2025.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fd0a5e563c756de210bb964789b5abe4f114dacae9104a47e1a649b910361536", size = 291030 }, + { url = "https://pypi.netflix.net/packages/19286189865/regex-2025.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf3490bcbb985a1ae97b2ce9ad1c0f06a852d5b19dde9b07bdf25bf224248c95", size = 807674 }, + { url = "https://pypi.netflix.net/packages/19286189866/regex-2025.11.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3809988f0a8b8c9dcc0f92478d6501fac7200b9ec56aecf0ec21f4a2ec4b6009", size = 873451 }, + { url = "https://pypi.netflix.net/packages/19286189867/regex-2025.11.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f4ff94e58e84aedb9c9fce66d4ef9f27a190285b451420f297c9a09f2b9abee9", size = 914980 }, + { url = "https://pypi.netflix.net/packages/19286196611/regex-2025.11.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eb542fd347ce61e1321b0a6b945d5701528dca0cd9759c2e3bb8bd57e47964d", size = 812852 }, + { url = "https://pypi.netflix.net/packages/19286196612/regex-2025.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2d5919075a1f2e413c00b056ea0c2f065b3f5fe83c3d07d325ab92dce51d6", size = 795566 }, + { url = "https://pypi.netflix.net/packages/19286196613/regex-2025.11.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3f8bf11a4827cc7ce5a53d4ef6cddd5ad25595d3c1435ef08f76825851343154", size = 868463 }, + { url = "https://pypi.netflix.net/packages/19286196614/regex-2025.11.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:22c12d837298651e5550ac1d964e4ff57c3f56965fc1812c90c9fb2028eaf267", size = 854694 }, + { url = "https://pypi.netflix.net/packages/19286196615/regex-2025.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ba394a3dda9ad41c7c780f60f6e4a70988741415ae96f6d1bf6c239cf01379", size = 799691 }, + { url = "https://pypi.netflix.net/packages/19286209338/regex-2025.11.3-cp314-cp314t-win32.whl", hash = "sha256:4bf146dca15cdd53224a1bf46d628bd7590e4a07fbb69e720d561aea43a32b38", size = 274583 }, + { url = "https://pypi.netflix.net/packages/19286209339/regex-2025.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:adad1a1bcf1c9e76346e091d22d23ac54ef28e1365117d99521631078dfec9de", size = 284286 }, + { url = "https://pypi.netflix.net/packages/19286209340/regex-2025.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c54f768482cef41e219720013cd05933b6f971d9562544d691c68699bf2b6801", size = 274741 }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.netflix.net/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://pypi.netflix.net/packages/18983953497/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517 } +wheels = [ + { url = "https://pypi.netflix.net/packages/18983953496/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738 }, +] + +[[package]] +name = "ruff" +version = "0.14.11" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/19573622725/ruff-0.14.11.tar.gz", hash = "sha256:f6dc463bfa5c07a59b1ff2c3b9767373e541346ea105503b4c0369c520a66958", size = 5993417 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19573630388/ruff-0.14.11-py3-none-linux_armv6l.whl", hash = "sha256:f6ff2d95cbd335841a7217bdfd9c1d2e44eac2c584197ab1385579d55ff8830e", size = 12951208 }, + { url = "https://pypi.netflix.net/packages/19573622711/ruff-0.14.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f6eb5c1c8033680f4172ea9c8d3706c156223010b8b97b05e82c59bdc774ee6", size = 13330075 }, + { url = "https://pypi.netflix.net/packages/19573622712/ruff-0.14.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2fc34cc896f90080fca01259f96c566f74069a04b25b6205d55379d12a6855e", size = 12257809 }, + { url = "https://pypi.netflix.net/packages/19573630391/ruff-0.14.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53386375001773ae812b43205d6064dae49ff0968774e6befe16a994fc233caa", size = 12678447 }, + { url = "https://pypi.netflix.net/packages/19573616216/ruff-0.14.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a697737dce1ca97a0a55b5ff0434ee7205943d4874d638fe3ae66166ff46edbe", size = 12758560 }, + { url = "https://pypi.netflix.net/packages/19573616217/ruff-0.14.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6845ca1da8ab81ab1dce755a32ad13f1db72e7fba27c486d5d90d65e04d17b8f", size = 13599296 }, + { url = "https://pypi.netflix.net/packages/19573622715/ruff-0.14.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e36ce2fd31b54065ec6f76cb08d60159e1b32bdf08507862e32f47e6dde8bcbf", size = 15048981 }, + { url = "https://pypi.netflix.net/packages/19573616218/ruff-0.14.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:590bcc0e2097ecf74e62a5c10a6b71f008ad82eb97b0a0079e85defe19fe74d9", size = 14633183 }, + { url = "https://pypi.netflix.net/packages/19573616219/ruff-0.14.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53fe71125fc158210d57fe4da26e622c9c294022988d08d9347ec1cf782adafe", size = 14050453 }, + { url = "https://pypi.netflix.net/packages/19573630397/ruff-0.14.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a35c9da08562f1598ded8470fcfef2afb5cf881996e6c0a502ceb61f4bc9c8a3", size = 13757889 }, + { url = "https://pypi.netflix.net/packages/19573622718/ruff-0.14.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0f3727189a52179393ecf92ec7057c2210203e6af2676f08d92140d3e1ee72c1", size = 13955832 }, + { url = "https://pypi.netflix.net/packages/19573616220/ruff-0.14.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:eb09f849bd37147a789b85995ff734a6c4a095bed5fd1608c4f56afc3634cde2", size = 12586522 }, + { url = "https://pypi.netflix.net/packages/19573616221/ruff-0.14.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c61782543c1231bf71041461c1f28c64b961d457d0f238ac388e2ab173d7ecb7", size = 12724637 }, + { url = "https://pypi.netflix.net/packages/19573616222/ruff-0.14.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82ff352ea68fb6766140381748e1f67f83c39860b6446966cff48a315c3e2491", size = 13145837 }, + { url = "https://pypi.netflix.net/packages/19573630402/ruff-0.14.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:728e56879df4ca5b62a9dde2dd0eb0edda2a55160c0ea28c4025f18c03f86984", size = 13850469 }, + { url = "https://pypi.netflix.net/packages/19573616223/ruff-0.14.11-py3-none-win32.whl", hash = "sha256:337c5dd11f16ee52ae217757d9b82a26400be7efac883e9e852646f1557ed841", size = 12851094 }, + { url = "https://pypi.netflix.net/packages/19573622723/ruff-0.14.11-py3-none-win_amd64.whl", hash = "sha256:f981cea63d08456b2c070e64b79cb62f951aa1305282974d4d5216e6e0178ae6", size = 14001379 }, + { url = "https://pypi.netflix.net/packages/19573616224/ruff-0.14.11-py3-none-win_arm64.whl", hash = "sha256:649fb6c9edd7f751db276ef42df1f3df41c38d67d199570ae2a7bd6cbc3590f0", size = 12938644 }, +] + +[[package]] +name = "safetensors" +version = "0.7.0" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/19360580766/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19360580744/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781 }, + { url = "https://pypi.netflix.net/packages/19360580745/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058 }, + { url = "https://pypi.netflix.net/packages/19360578602/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748 }, + { url = "https://pypi.netflix.net/packages/19360578603/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881 }, + { url = "https://pypi.netflix.net/packages/19360578604/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463 }, + { url = "https://pypi.netflix.net/packages/19360578605/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855 }, + { url = "https://pypi.netflix.net/packages/19360580750/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152 }, + { url = "https://pypi.netflix.net/packages/19360580751/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856 }, + { url = "https://pypi.netflix.net/packages/19360580752/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060 }, + { url = "https://pypi.netflix.net/packages/19360580753/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715 }, + { url = "https://pypi.netflix.net/packages/19360580754/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377 }, + { url = "https://pypi.netflix.net/packages/19360580755/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368 }, + { url = "https://pypi.netflix.net/packages/19360580756/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423 }, + { url = "https://pypi.netflix.net/packages/19360580757/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380 }, + { url = "https://pypi.netflix.net/packages/19360578606/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4729811a6640d019a4b7ba8638ee2fd21fa5ca8c7e7bdf0fed62068fcaac737", size = 492430 }, + { url = "https://pypi.netflix.net/packages/19360578607/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12f49080303fa6bb424b362149a12949dfbbf1e06811a88f2307276b0c131afd", size = 503977 }, + { url = "https://pypi.netflix.net/packages/19360578608/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0071bffba4150c2f46cae1432d31995d77acfd9f8db598b5d1a2ce67e8440ad2", size = 623890 }, + { url = "https://pypi.netflix.net/packages/19360578609/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:473b32699f4200e69801bf5abf93f1a4ecd432a70984df164fc22ccf39c4a6f3", size = 531885 }, +] + +[[package]] +name = "scikit-learn" +version = "1.7.2" +source = { registry = "https://pypi.netflix.net/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "joblib", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.netflix.net/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.netflix.net/simple" }, marker = "python_full_version < '3.11'" }, + { name = "threadpoolctl", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://pypi.netflix.net/packages/19062486281/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19062480630/scikit_learn-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b33579c10a3081d076ab403df4a4190da4f4432d443521674637677dc91e61f", size = 9336221 }, + { url = "https://pypi.netflix.net/packages/19062480631/scikit_learn-1.7.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:36749fb62b3d961b1ce4fedf08fa57a1986cd409eff2d783bca5d4b9b5fce51c", size = 8653834 }, + { url = "https://pypi.netflix.net/packages/19062480632/scikit_learn-1.7.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7a58814265dfc52b3295b1900cfb5701589d30a8bb026c7540f1e9d3499d5ec8", size = 9660938 }, + { url = "https://pypi.netflix.net/packages/19062480633/scikit_learn-1.7.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a847fea807e278f821a0406ca01e387f97653e284ecbd9750e3ee7c90347f18", size = 9477818 }, + { url = "https://pypi.netflix.net/packages/19062480634/scikit_learn-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:ca250e6836d10e6f402436d6463d6c0e4d8e0234cfb6a9a47835bd392b852ce5", size = 8886969 }, + { url = "https://pypi.netflix.net/packages/19062480635/scikit_learn-1.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7509693451651cd7361d30ce4e86a1347493554f172b1c72a39300fa2aea79e", size = 9331967 }, + { url = "https://pypi.netflix.net/packages/19062480636/scikit_learn-1.7.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:0486c8f827c2e7b64837c731c8feff72c0bd2b998067a8a9cbc10643c31f0fe1", size = 8648645 }, + { url = "https://pypi.netflix.net/packages/19062480637/scikit_learn-1.7.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89877e19a80c7b11a2891a27c21c4894fb18e2c2e077815bcade10d34287b20d", size = 9715424 }, + { url = "https://pypi.netflix.net/packages/19062482034/scikit_learn-1.7.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8da8bf89d4d79aaec192d2bda62f9b56ae4e5b4ef93b6a56b5de4977e375c1f1", size = 9509234 }, + { url = "https://pypi.netflix.net/packages/19062482035/scikit_learn-1.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:9b7ed8d58725030568523e937c43e56bc01cadb478fc43c042a9aca1dacb3ba1", size = 8894244 }, + { url = "https://pypi.netflix.net/packages/19062482036/scikit_learn-1.7.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8d91a97fa2b706943822398ab943cde71858a50245e31bc71dba62aab1d60a96", size = 9259818 }, + { url = "https://pypi.netflix.net/packages/19062482037/scikit_learn-1.7.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:acbc0f5fd2edd3432a22c69bed78e837c70cf896cd7993d71d51ba6708507476", size = 8636997 }, + { url = "https://pypi.netflix.net/packages/19062482038/scikit_learn-1.7.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e5bf3d930aee75a65478df91ac1225ff89cd28e9ac7bd1196853a9229b6adb0b", size = 9478381 }, + { url = "https://pypi.netflix.net/packages/19062482039/scikit_learn-1.7.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d6e9deed1a47aca9fe2f267ab8e8fe82ee20b4526b2c0cd9e135cea10feb44", size = 9300296 }, + { url = "https://pypi.netflix.net/packages/19062483442/scikit_learn-1.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:6088aa475f0785e01bcf8529f55280a3d7d298679f50c0bb70a2364a82d0b290", size = 8731256 }, + { url = "https://pypi.netflix.net/packages/19062483443/scikit_learn-1.7.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b7dacaa05e5d76759fb071558a8b5130f4845166d88654a0f9bdf3eb57851b7", size = 9212382 }, + { url = "https://pypi.netflix.net/packages/19062483444/scikit_learn-1.7.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:abebbd61ad9e1deed54cca45caea8ad5f79e1b93173dece40bb8e0c658dbe6fe", size = 8592042 }, + { url = "https://pypi.netflix.net/packages/19062483445/scikit_learn-1.7.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:502c18e39849c0ea1a5d681af1dbcf15f6cce601aebb657aabbfe84133c1907f", size = 9434180 }, + { url = "https://pypi.netflix.net/packages/19062483446/scikit_learn-1.7.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a4c328a71785382fe3fe676a9ecf2c86189249beff90bf85e22bdb7efaf9ae0", size = 9283660 }, + { url = "https://pypi.netflix.net/packages/19062483447/scikit_learn-1.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:63a9afd6f7b229aad94618c01c252ce9e6fa97918c5ca19c9a17a087d819440c", size = 8702057 }, + { url = "https://pypi.netflix.net/packages/19062484856/scikit_learn-1.7.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9acb6c5e867447b4e1390930e3944a005e2cb115922e693c08a323421a6966e8", size = 9558731 }, + { url = "https://pypi.netflix.net/packages/19062484857/scikit_learn-1.7.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:2a41e2a0ef45063e654152ec9d8bcfc39f7afce35b08902bfe290c2498a67a6a", size = 9038852 }, + { url = "https://pypi.netflix.net/packages/19062484858/scikit_learn-1.7.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98335fb98509b73385b3ab2bd0639b1f610541d3988ee675c670371d6a87aa7c", size = 9527094 }, + { url = "https://pypi.netflix.net/packages/19062484859/scikit_learn-1.7.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191e5550980d45449126e23ed1d5e9e24b2c68329ee1f691a3987476e115e09c", size = 9367436 }, + { url = "https://pypi.netflix.net/packages/19062484860/scikit_learn-1.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:57dc4deb1d3762c75d685507fbd0bc17160144b2f2ba4ccea5dc285ab0d0e973", size = 9275749 }, + { url = "https://pypi.netflix.net/packages/19062484861/scikit_learn-1.7.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fa8f63940e29c82d1e67a45d5297bdebbcb585f5a5a50c4914cc2e852ab77f33", size = 9208906 }, + { url = "https://pypi.netflix.net/packages/19062484862/scikit_learn-1.7.2-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:f95dc55b7902b91331fa4e5845dd5bde0580c9cd9612b1b2791b7e80c3d32615", size = 8627836 }, + { url = "https://pypi.netflix.net/packages/19062486278/scikit_learn-1.7.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9656e4a53e54578ad10a434dc1f993330568cfee176dff07112b8785fb413106", size = 9426236 }, + { url = "https://pypi.netflix.net/packages/19062486279/scikit_learn-1.7.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96dc05a854add0e50d3f47a1ef21a10a595016da5b007c7d9cd9d0bffd1fcc61", size = 9312593 }, + { url = "https://pypi.netflix.net/packages/19062486280/scikit_learn-1.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:bb24510ed3f9f61476181e4db51ce801e2ba37541def12dc9333b946fc7a9cf8", size = 8820007 }, +] + +[[package]] +name = "scikit-learn" +version = "1.8.0" +source = { registry = "https://pypi.netflix.net/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "joblib", marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.0", source = { registry = "https://pypi.netflix.net/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.16.3", source = { registry = "https://pypi.netflix.net/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "threadpoolctl", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://pypi.netflix.net/packages/19451429848/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19451423691/scikit_learn-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:146b4d36f800c013d267b29168813f7a03a43ecd2895d04861f1240b564421da", size = 8595835 }, + { url = "https://pypi.netflix.net/packages/19451423692/scikit_learn-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f984ca4b14914e6b4094c5d52a32ea16b49832c03bd17a110f004db3c223e8e1", size = 8080381 }, + { url = "https://pypi.netflix.net/packages/19451423693/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e30adb87f0cc81c7690a84f7932dd66be5bac57cfe16b91cb9151683a4a2d3b", size = 8799632 }, + { url = "https://pypi.netflix.net/packages/19451423694/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ada8121bcb4dac28d930febc791a69f7cb1673c8495e5eee274190b73a4559c1", size = 9103788 }, + { url = "https://pypi.netflix.net/packages/19451423695/scikit_learn-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:c57b1b610bd1f40ba43970e11ce62821c2e6569e4d74023db19c6b26f246cb3b", size = 8081706 }, + { url = "https://pypi.netflix.net/packages/19451423696/scikit_learn-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:2838551e011a64e3053ad7618dda9310175f7515f1742fa2d756f7c874c05961", size = 7688451 }, + { url = "https://pypi.netflix.net/packages/19451423697/scikit_learn-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fb63362b5a7ddab88e52b6dbb47dac3fd7dafeee740dc6c8d8a446ddedade8e", size = 8548242 }, + { url = "https://pypi.netflix.net/packages/19451423698/scikit_learn-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5025ce924beccb28298246e589c691fe1b8c1c96507e6d27d12c5fadd85bfd76", size = 8079075 }, + { url = "https://pypi.netflix.net/packages/19451423699/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4496bb2cf7a43ce1a2d7524a79e40bc5da45cf598dbf9545b7e8316ccba47bb4", size = 8660492 }, + { url = "https://pypi.netflix.net/packages/19451425165/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bcfe4d0d14aec44921545fd2af2338c7471de9cb701f1da4c9d85906ab847a", size = 8931904 }, + { url = "https://pypi.netflix.net/packages/19451425166/scikit_learn-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:35c007dedb2ffe38fe3ee7d201ebac4a2deccd2408e8621d53067733e3c74809", size = 8019359 }, + { url = "https://pypi.netflix.net/packages/19451425167/scikit_learn-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb", size = 7641898 }, + { url = "https://pypi.netflix.net/packages/19451425168/scikit_learn-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d6ae97234d5d7079dc0040990a6f7aeb97cb7fa7e8945f1999a429b23569e0a", size = 8513770 }, + { url = "https://pypi.netflix.net/packages/19451425169/scikit_learn-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:edec98c5e7c128328124a029bceb09eda2d526997780fef8d65e9a69eead963e", size = 8044458 }, + { url = "https://pypi.netflix.net/packages/19451425170/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74b66d8689d52ed04c271e1329f0c61635bcaf5b926db9b12d58914cdc01fe57", size = 8610341 }, + { url = "https://pypi.netflix.net/packages/19451425171/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fdf95767f989b0cfedb85f7ed8ca215d4be728031f56ff5a519ee1e3276dc2e", size = 8900022 }, + { url = "https://pypi.netflix.net/packages/19451425172/scikit_learn-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:2de443b9373b3b615aec1bb57f9baa6bb3a9bd093f1269ba95c17d870422b271", size = 7989409 }, + { url = "https://pypi.netflix.net/packages/19451426862/scikit_learn-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:eddde82a035681427cbedded4e6eff5e57fa59216c2e3e90b10b19ab1d0a65c3", size = 7619760 }, + { url = "https://pypi.netflix.net/packages/19451426863/scikit_learn-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7cc267b6108f0a1499a734167282c00c4ebf61328566b55ef262d48e9849c735", size = 8826045 }, + { url = "https://pypi.netflix.net/packages/19451426864/scikit_learn-1.8.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:fe1c011a640a9f0791146011dfd3c7d9669785f9fed2b2a5f9e207536cf5c2fd", size = 8420324 }, + { url = "https://pypi.netflix.net/packages/19451426865/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72358cce49465d140cc4e7792015bb1f0296a9742d5622c67e31399b75468b9e", size = 8680651 }, + { url = "https://pypi.netflix.net/packages/19451426866/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb", size = 8925045 }, + { url = "https://pypi.netflix.net/packages/19451426867/scikit_learn-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702", size = 8507994 }, + { url = "https://pypi.netflix.net/packages/19451426868/scikit_learn-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde", size = 7869518 }, + { url = "https://pypi.netflix.net/packages/19451428349/scikit_learn-1.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3", size = 8528667 }, + { url = "https://pypi.netflix.net/packages/19451428350/scikit_learn-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7", size = 8066524 }, + { url = "https://pypi.netflix.net/packages/19451428351/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6", size = 8657133 }, + { url = "https://pypi.netflix.net/packages/19451428352/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4", size = 8923223 }, + { url = "https://pypi.netflix.net/packages/19451428353/scikit_learn-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6", size = 8096518 }, + { url = "https://pypi.netflix.net/packages/19451428354/scikit_learn-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242", size = 7754546 }, + { url = "https://pypi.netflix.net/packages/19451428355/scikit_learn-1.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7", size = 8848305 }, + { url = "https://pypi.netflix.net/packages/19451429843/scikit_learn-1.8.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9", size = 8432257 }, + { url = "https://pypi.netflix.net/packages/19451429844/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f", size = 8678673 }, + { url = "https://pypi.netflix.net/packages/19451429845/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9", size = 8922467 }, + { url = "https://pypi.netflix.net/packages/19451429846/scikit_learn-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2", size = 8774395 }, + { url = "https://pypi.netflix.net/packages/19451429847/scikit_learn-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c", size = 8002647 }, +] + +[[package]] +name = "scipy" +version = "1.15.3" +source = { registry = "https://pypi.netflix.net/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.netflix.net/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://pypi.netflix.net/packages/18670433103/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214 } +wheels = [ + { url = "https://pypi.netflix.net/packages/18670387320/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770 }, + { url = "https://pypi.netflix.net/packages/18670387321/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511 }, + { url = "https://pypi.netflix.net/packages/18670387322/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151 }, + { url = "https://pypi.netflix.net/packages/18670387323/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732 }, + { url = "https://pypi.netflix.net/packages/18670389214/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617 }, + { url = "https://pypi.netflix.net/packages/18670389215/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964 }, + { url = "https://pypi.netflix.net/packages/18670391800/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749 }, + { url = "https://pypi.netflix.net/packages/18670391801/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383 }, + { url = "https://pypi.netflix.net/packages/18670394434/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201 }, + { url = "https://pypi.netflix.net/packages/18670394435/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255 }, + { url = "https://pypi.netflix.net/packages/18670394436/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035 }, + { url = "https://pypi.netflix.net/packages/18670396244/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499 }, + { url = "https://pypi.netflix.net/packages/18670396245/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602 }, + { url = "https://pypi.netflix.net/packages/18670396246/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415 }, + { url = "https://pypi.netflix.net/packages/18670401321/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622 }, + { url = "https://pypi.netflix.net/packages/18670401322/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796 }, + { url = "https://pypi.netflix.net/packages/18670405089/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684 }, + { url = "https://pypi.netflix.net/packages/18670406955/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504 }, + { url = "https://pypi.netflix.net/packages/18670412146/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735 }, + { url = "https://pypi.netflix.net/packages/18670412147/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284 }, + { url = "https://pypi.netflix.net/packages/18670412148/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958 }, + { url = "https://pypi.netflix.net/packages/18670414849/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454 }, + { url = "https://pypi.netflix.net/packages/18670414850/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199 }, + { url = "https://pypi.netflix.net/packages/18670414851/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455 }, + { url = "https://pypi.netflix.net/packages/18670416656/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140 }, + { url = "https://pypi.netflix.net/packages/18670416657/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549 }, + { url = "https://pypi.netflix.net/packages/18670418464/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184 }, + { url = "https://pypi.netflix.net/packages/18670418465/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256 }, + { url = "https://pypi.netflix.net/packages/18670420274/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540 }, + { url = "https://pypi.netflix.net/packages/18670420275/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115 }, + { url = "https://pypi.netflix.net/packages/18670420276/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884 }, + { url = "https://pypi.netflix.net/packages/18670422088/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018 }, + { url = "https://pypi.netflix.net/packages/18670422089/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716 }, + { url = "https://pypi.netflix.net/packages/18670422090/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342 }, + { url = "https://pypi.netflix.net/packages/18670423905/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869 }, + { url = "https://pypi.netflix.net/packages/18670431186/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851 }, + { url = "https://pypi.netflix.net/packages/18670425721/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011 }, + { url = "https://pypi.netflix.net/packages/18670425722/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407 }, + { url = "https://pypi.netflix.net/packages/18670425723/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030 }, + { url = "https://pypi.netflix.net/packages/18670425724/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709 }, + { url = "https://pypi.netflix.net/packages/18670427544/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045 }, + { url = "https://pypi.netflix.net/packages/18670427545/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062 }, + { url = "https://pypi.netflix.net/packages/18670427546/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132 }, + { url = "https://pypi.netflix.net/packages/18670429369/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503 }, + { url = "https://pypi.netflix.net/packages/18670429370/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097 }, +] + +[[package]] +name = "scipy" +version = "1.16.3" +source = { registry = "https://pypi.netflix.net/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "numpy", version = "2.4.0", source = { registry = "https://pypi.netflix.net/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://pypi.netflix.net/packages/19258282708/scipy-1.16.3.tar.gz", hash = "sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb", size = 30597883 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19258178806/scipy-1.16.3-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:40be6cf99e68b6c4321e9f8782e7d5ff8265af28ef2cd56e9c9b2638fa08ad97", size = 36630881 }, + { url = "https://pypi.netflix.net/packages/19258178807/scipy-1.16.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:8be1ca9170fcb6223cc7c27f4305d680ded114a1567c0bd2bfcbf947d1b17511", size = 28941012 }, + { url = "https://pypi.netflix.net/packages/19258178808/scipy-1.16.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bea0a62734d20d67608660f69dcda23e7f90fb4ca20974ab80b6ed40df87a005", size = 20931935 }, + { url = "https://pypi.netflix.net/packages/19258181561/scipy-1.16.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:2a207a6ce9c24f1951241f4693ede2d393f59c07abc159b2cb2be980820e01fb", size = 23534466 }, + { url = "https://pypi.netflix.net/packages/19258181562/scipy-1.16.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:532fb5ad6a87e9e9cd9c959b106b73145a03f04c7d57ea3e6f6bb60b86ab0876", size = 33593618 }, + { url = "https://pypi.netflix.net/packages/19258181563/scipy-1.16.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0151a0749efeaaab78711c78422d413c583b8cdd2011a3c1d6c794938ee9fdb2", size = 35899798 }, + { url = "https://pypi.netflix.net/packages/19258191580/scipy-1.16.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b7180967113560cca57418a7bc719e30366b47959dd845a93206fbed693c867e", size = 36226154 }, + { url = "https://pypi.netflix.net/packages/19258191581/scipy-1.16.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:deb3841c925eeddb6afc1e4e4a45e418d19ec7b87c5df177695224078e8ec733", size = 38878540 }, + { url = "https://pypi.netflix.net/packages/19258191582/scipy-1.16.3-cp311-cp311-win_amd64.whl", hash = "sha256:53c3844d527213631e886621df5695d35e4f6a75f620dca412bcd292f6b87d78", size = 38722107 }, + { url = "https://pypi.netflix.net/packages/19258193645/scipy-1.16.3-cp311-cp311-win_arm64.whl", hash = "sha256:9452781bd879b14b6f055b26643703551320aa8d79ae064a71df55c00286a184", size = 25506272 }, + { url = "https://pypi.netflix.net/packages/19258193646/scipy-1.16.3-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81fc5827606858cf71446a5e98715ba0e11f0dbc83d71c7409d05486592a45d6", size = 36659043 }, + { url = "https://pypi.netflix.net/packages/19258195711/scipy-1.16.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:c97176013d404c7346bf57874eaac5187d969293bf40497140b0a2b2b7482e07", size = 28898986 }, + { url = "https://pypi.netflix.net/packages/19258195712/scipy-1.16.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2b71d93c8a9936046866acebc915e2af2e292b883ed6e2cbe5c34beb094b82d9", size = 20889814 }, + { url = "https://pypi.netflix.net/packages/19258195713/scipy-1.16.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3d4a07a8e785d80289dfe66b7c27d8634a773020742ec7187b85ccc4b0e7b686", size = 23565795 }, + { url = "https://pypi.netflix.net/packages/19258195714/scipy-1.16.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0553371015692a898e1aa858fed67a3576c34edefa6b7ebdb4e9dde49ce5c203", size = 33349476 }, + { url = "https://pypi.netflix.net/packages/19258197783/scipy-1.16.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:72d1717fd3b5e6ec747327ce9bda32d5463f472c9dce9f54499e81fbd50245a1", size = 35676692 }, + { url = "https://pypi.netflix.net/packages/19258197784/scipy-1.16.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fb2472e72e24d1530debe6ae078db70fb1605350c88a3d14bc401d6306dbffe", size = 36019345 }, + { url = "https://pypi.netflix.net/packages/19258199855/scipy-1.16.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5192722cffe15f9329a3948c4b1db789fbb1f05c97899187dcf009b283aea70", size = 38678975 }, + { url = "https://pypi.netflix.net/packages/19258199856/scipy-1.16.3-cp312-cp312-win_amd64.whl", hash = "sha256:56edc65510d1331dae01ef9b658d428e33ed48b4f77b1d51caf479a0253f96dc", size = 38555926 }, + { url = "https://pypi.netflix.net/packages/19258199857/scipy-1.16.3-cp312-cp312-win_arm64.whl", hash = "sha256:a8a26c78ef223d3e30920ef759e25625a0ecdd0d60e5a8818b7513c3e5384cf2", size = 25463014 }, + { url = "https://pypi.netflix.net/packages/19258206399/scipy-1.16.3-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:d2ec56337675e61b312179a1ad124f5f570c00f920cc75e1000025451b88241c", size = 36617856 }, + { url = "https://pypi.netflix.net/packages/19258206400/scipy-1.16.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:16b8bc35a4cc24db80a0ec836a9286d0e31b2503cb2fd7ff7fb0e0374a97081d", size = 28874306 }, + { url = "https://pypi.netflix.net/packages/19258206401/scipy-1.16.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:5803c5fadd29de0cf27fa08ccbfe7a9e5d741bf63e4ab1085437266f12460ff9", size = 20865371 }, + { url = "https://pypi.netflix.net/packages/19258209060/scipy-1.16.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:b81c27fc41954319a943d43b20e07c40bdcd3ff7cf013f4fb86286faefe546c4", size = 23524877 }, + { url = "https://pypi.netflix.net/packages/19258209061/scipy-1.16.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0c3b4dd3d9b08dbce0f3440032c52e9e2ab9f96ade2d3943313dfe51a7056959", size = 33342103 }, + { url = "https://pypi.netflix.net/packages/19258211155/scipy-1.16.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7dc1360c06535ea6116a2220f760ae572db9f661aba2d88074fe30ec2aa1ff88", size = 35697297 }, + { url = "https://pypi.netflix.net/packages/19258214601/scipy-1.16.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:663b8d66a8748051c3ee9c96465fb417509315b99c71550fda2591d7dd634234", size = 36021756 }, + { url = "https://pypi.netflix.net/packages/19258214602/scipy-1.16.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eab43fae33a0c39006a88096cd7b4f4ef545ea0447d250d5ac18202d40b6611d", size = 38696566 }, + { url = "https://pypi.netflix.net/packages/19258250437/scipy-1.16.3-cp313-cp313-win_amd64.whl", hash = "sha256:062246acacbe9f8210de8e751b16fc37458213f124bef161a5a02c7a39284304", size = 38529877 }, + { url = "https://pypi.netflix.net/packages/19258252718/scipy-1.16.3-cp313-cp313-win_arm64.whl", hash = "sha256:50a3dbf286dbc7d84f176f9a1574c705f277cb6565069f88f60db9eafdbe3ee2", size = 25455366 }, + { url = "https://pypi.netflix.net/packages/19258220240/scipy-1.16.3-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:fb4b29f4cf8cc5a8d628bc8d8e26d12d7278cd1f219f22698a378c3d67db5e4b", size = 37027931 }, + { url = "https://pypi.netflix.net/packages/19258220241/scipy-1.16.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:8d09d72dc92742988b0e7750bddb8060b0c7079606c0d24a8cc8e9c9c11f9079", size = 29400081 }, + { url = "https://pypi.netflix.net/packages/19258222518/scipy-1.16.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:03192a35e661470197556de24e7cb1330d84b35b94ead65c46ad6f16f6b28f2a", size = 21391244 }, + { url = "https://pypi.netflix.net/packages/19258222519/scipy-1.16.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:57d01cb6f85e34f0946b33caa66e892aae072b64b034183f3d87c4025802a119", size = 23929753 }, + { url = "https://pypi.netflix.net/packages/19258238729/scipy-1.16.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:96491a6a54e995f00a28a3c3badfff58fd093bf26cd5fb34a2188c8c756a3a2c", size = 33496912 }, + { url = "https://pypi.netflix.net/packages/19258238730/scipy-1.16.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cd13e354df9938598af2be05822c323e97132d5e6306b83a3b4ee6724c6e522e", size = 35802371 }, + { url = "https://pypi.netflix.net/packages/19258246237/scipy-1.16.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63d3cdacb8a824a295191a723ee5e4ea7768ca5ca5f2838532d9f2e2b3ce2135", size = 36190477 }, + { url = "https://pypi.netflix.net/packages/19258246238/scipy-1.16.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e7efa2681ea410b10dde31a52b18b0154d66f2485328830e45fdf183af5aefc6", size = 38796678 }, + { url = "https://pypi.netflix.net/packages/19258248353/scipy-1.16.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2d1ae2cf0c350e7705168ff2429962a89ad90c2d49d1dd300686d8b2a5af22fc", size = 38640178 }, + { url = "https://pypi.netflix.net/packages/19258250447/scipy-1.16.3-cp313-cp313t-win_arm64.whl", hash = "sha256:0c623a54f7b79dd88ef56da19bc2873afec9673a48f3b85b18e4d402bdd29a5a", size = 25803246 }, + { url = "https://pypi.netflix.net/packages/19258254822/scipy-1.16.3-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:875555ce62743e1d54f06cdf22c1e0bc47b91130ac40fe5d783b6dfa114beeb6", size = 36606469 }, + { url = "https://pypi.netflix.net/packages/19258258555/scipy-1.16.3-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:bb61878c18a470021fb515a843dc7a76961a8daceaaaa8bad1332f1bf4b54657", size = 28872043 }, + { url = "https://pypi.netflix.net/packages/19258258556/scipy-1.16.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2622206f5559784fa5c4b53a950c3c7c1cf3e84ca1b9c4b6c03f062f289ca26", size = 20862952 }, + { url = "https://pypi.netflix.net/packages/19258263741/scipy-1.16.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7f68154688c515cdb541a31ef8eb66d8cd1050605be9dcd74199cbd22ac739bc", size = 23508512 }, + { url = "https://pypi.netflix.net/packages/19258263742/scipy-1.16.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3c820ddb80029fe9f43d61b81d8b488d3ef8ca010d15122b152db77dc94c22", size = 33413639 }, + { url = "https://pypi.netflix.net/packages/19258265841/scipy-1.16.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d3837938ae715fc0fe3c39c0202de3a8853aff22ca66781ddc2ade7554b7e2cc", size = 35704729 }, + { url = "https://pypi.netflix.net/packages/19258265842/scipy-1.16.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aadd23f98f9cb069b3bd64ddc900c4d277778242e961751f77a8cb5c4b946fb0", size = 36086251 }, + { url = "https://pypi.netflix.net/packages/19258267943/scipy-1.16.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b7c5f1bda1354d6a19bc6af73a649f8285ca63ac6b52e64e658a5a11d4d69800", size = 38716681 }, + { url = "https://pypi.netflix.net/packages/19258278190/scipy-1.16.3-cp314-cp314-win_amd64.whl", hash = "sha256:e5d42a9472e7579e473879a1990327830493a7047506d58d73fc429b84c1d49d", size = 39358423 }, + { url = "https://pypi.netflix.net/packages/19258278191/scipy-1.16.3-cp314-cp314-win_arm64.whl", hash = "sha256:6020470b9d00245926f2d5bb93b119ca0340f0d564eb6fbaad843eaebf9d690f", size = 26135027 }, + { url = "https://pypi.netflix.net/packages/19258267944/scipy-1.16.3-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:e1d27cbcb4602680a49d787d90664fa4974063ac9d4134813332a8c53dbe667c", size = 37028379 }, + { url = "https://pypi.netflix.net/packages/19258270096/scipy-1.16.3-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:9b9c9c07b6d56a35777a1b4cc8966118fb16cfd8daf6743867d17d36cfad2d40", size = 29400052 }, + { url = "https://pypi.netflix.net/packages/19258270097/scipy-1.16.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:3a4c460301fb2cffb7f88528f30b3127742cff583603aa7dc964a52c463b385d", size = 21391183 }, + { url = "https://pypi.netflix.net/packages/19258273632/scipy-1.16.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:f667a4542cc8917af1db06366d3f78a5c8e83badd56409f94d1eac8d8d9133fa", size = 23930174 }, + { url = "https://pypi.netflix.net/packages/19258273633/scipy-1.16.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f379b54b77a597aa7ee5e697df0d66903e41b9c85a6dd7946159e356319158e8", size = 33497852 }, + { url = "https://pypi.netflix.net/packages/19258275740/scipy-1.16.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4aff59800a3b7f786b70bfd6ab551001cb553244988d7d6b8299cb1ea653b353", size = 35798595 }, + { url = "https://pypi.netflix.net/packages/19258275741/scipy-1.16.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:da7763f55885045036fabcebd80144b757d3db06ab0861415d1c3b7c69042146", size = 36186269 }, + { url = "https://pypi.netflix.net/packages/19258275742/scipy-1.16.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ffa6eea95283b2b8079b821dc11f50a17d0571c92b43e2b5b12764dc5f9b285d", size = 38802779 }, + { url = "https://pypi.netflix.net/packages/19258278200/scipy-1.16.3-cp314-cp314t-win_amd64.whl", hash = "sha256:d9f48cafc7ce94cf9b15c6bffdc443a81a27bf7075cf2dcd5c8b40f85d10c4e7", size = 39471128 }, + { url = "https://pypi.netflix.net/packages/19258278201/scipy-1.16.3-cp314-cp314t-win_arm64.whl", hash = "sha256:21d9d6b197227a12dcbf9633320a4e34c6b0e51c57268df255a0942983bac562", size = 26464127 }, +] + +[[package]] +name = "sentence-transformers" +version = "5.2.0" +source = { registry = "https://pypi.netflix.net/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.netflix.net/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.netflix.net/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.netflix.net/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.16.3", source = { registry = "https://pypi.netflix.net/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "torch" }, + { name = "tqdm" }, + { name = "transformers" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://pypi.netflix.net/packages/19458505845/sentence_transformers-5.2.0.tar.gz", hash = "sha256:acaeb38717de689f3dab45d5e5a02ebe2f75960a4764ea35fea65f58a4d3019f", size = 381004 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19458505844/sentence_transformers-5.2.0-py3-none-any.whl", hash = "sha256:aa57180f053687d29b08206766ae7db549be5074f61849def7b17bf0b8025ca2", size = 493748 }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/18720628310/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958 } +wheels = [ + { url = "https://pypi.netflix.net/packages/18720628309/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/9618641273/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://pypi.netflix.net/packages/9618641272/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "starlette" +version = "0.50.0" +source = { registry = "https://pypi.netflix.net/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://pypi.netflix.net/packages/19277394801/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19277394800/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033 }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.netflix.net/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://pypi.netflix.net/packages/18641753906/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921 } +wheels = [ + { url = "https://pypi.netflix.net/packages/18641753905/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353 }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/18525745157/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274 } +wheels = [ + { url = "https://pypi.netflix.net/packages/18525745156/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638 }, +] + +[[package]] +name = "tiktoken" +version = "0.12.0" +source = { registry = "https://pypi.netflix.net/simple" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://pypi.netflix.net/packages/19172716906/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19172714799/tiktoken-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3de02f5a491cfd179aec916eddb70331814bd6bf764075d39e21d5862e533970", size = 1051991 }, + { url = "https://pypi.netflix.net/packages/19172714800/tiktoken-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b6cfb6d9b7b54d20af21a912bfe63a2727d9cfa8fbda642fd8322c70340aad16", size = 995798 }, + { url = "https://pypi.netflix.net/packages/19172714801/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:cde24cdb1b8a08368f709124f15b36ab5524aac5fa830cc3fdce9c03d4fb8030", size = 1129865 }, + { url = "https://pypi.netflix.net/packages/19172714802/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6de0da39f605992649b9cfa6f84071e3f9ef2cec458d08c5feb1b6f0ff62e134", size = 1152856 }, + { url = "https://pypi.netflix.net/packages/19172714803/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6faa0534e0eefbcafaccb75927a4a380463a2eaa7e26000f0173b920e98b720a", size = 1195308 }, + { url = "https://pypi.netflix.net/packages/19172714804/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:82991e04fc860afb933efb63957affc7ad54f83e2216fe7d319007dab1ba5892", size = 1255697 }, + { url = "https://pypi.netflix.net/packages/19172714805/tiktoken-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:6fb2995b487c2e31acf0a9e17647e3b242235a20832642bb7a9d1a181c0c1bb1", size = 879375 }, + { url = "https://pypi.netflix.net/packages/19172714806/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb", size = 1051565 }, + { url = "https://pypi.netflix.net/packages/19172714807/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", size = 995284 }, + { url = "https://pypi.netflix.net/packages/19172714808/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201 }, + { url = "https://pypi.netflix.net/packages/19172715304/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", size = 1152444 }, + { url = "https://pypi.netflix.net/packages/19172715305/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", size = 1195080 }, + { url = "https://pypi.netflix.net/packages/19172715306/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", size = 1255240 }, + { url = "https://pypi.netflix.net/packages/19172715307/tiktoken-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def", size = 879422 }, + { url = "https://pypi.netflix.net/packages/19172715308/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728 }, + { url = "https://pypi.netflix.net/packages/19172715309/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049 }, + { url = "https://pypi.netflix.net/packages/19172715310/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008 }, + { url = "https://pypi.netflix.net/packages/19172715311/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665 }, + { url = "https://pypi.netflix.net/packages/19172715312/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230 }, + { url = "https://pypi.netflix.net/packages/19172715313/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688 }, + { url = "https://pypi.netflix.net/packages/19172715314/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694 }, + { url = "https://pypi.netflix.net/packages/19172715315/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802 }, + { url = "https://pypi.netflix.net/packages/19172715823/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995 }, + { url = "https://pypi.netflix.net/packages/19172715824/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948 }, + { url = "https://pypi.netflix.net/packages/19172715825/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986 }, + { url = "https://pypi.netflix.net/packages/19172715826/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222 }, + { url = "https://pypi.netflix.net/packages/19172715827/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097 }, + { url = "https://pypi.netflix.net/packages/19172715828/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117 }, + { url = "https://pypi.netflix.net/packages/19172715829/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309 }, + { url = "https://pypi.netflix.net/packages/19172715830/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712 }, + { url = "https://pypi.netflix.net/packages/19172715831/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725 }, + { url = "https://pypi.netflix.net/packages/19172715832/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875 }, + { url = "https://pypi.netflix.net/packages/19172715833/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451 }, + { url = "https://pypi.netflix.net/packages/19172716352/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794 }, + { url = "https://pypi.netflix.net/packages/19172716353/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777 }, + { url = "https://pypi.netflix.net/packages/19172716354/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188 }, + { url = "https://pypi.netflix.net/packages/19172716355/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978 }, + { url = "https://pypi.netflix.net/packages/19172716356/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271 }, + { url = "https://pypi.netflix.net/packages/19172716357/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216 }, + { url = "https://pypi.netflix.net/packages/19172716358/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860 }, + { url = "https://pypi.netflix.net/packages/19172716359/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567 }, + { url = "https://pypi.netflix.net/packages/19172716360/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067 }, + { url = "https://pypi.netflix.net/packages/19172716361/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473 }, + { url = "https://pypi.netflix.net/packages/19172716362/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855 }, + { url = "https://pypi.netflix.net/packages/19172716363/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022 }, + { url = "https://pypi.netflix.net/packages/19172716364/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736 }, + { url = "https://pypi.netflix.net/packages/19172716896/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908 }, + { url = "https://pypi.netflix.net/packages/19172716897/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706 }, + { url = "https://pypi.netflix.net/packages/19172716898/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667 }, +] + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.netflix.net/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://pypi.netflix.net/packages/19556222481/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19556215569/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275 }, + { url = "https://pypi.netflix.net/packages/19556215570/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472 }, + { url = "https://pypi.netflix.net/packages/19556212687/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736 }, + { url = "https://pypi.netflix.net/packages/19556212688/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835 }, + { url = "https://pypi.netflix.net/packages/19556215573/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673 }, + { url = "https://pypi.netflix.net/packages/19556212689/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818 }, + { url = "https://pypi.netflix.net/packages/19556212690/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195 }, + { url = "https://pypi.netflix.net/packages/19556215576/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982 }, + { url = "https://pypi.netflix.net/packages/19556215577/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245 }, + { url = "https://pypi.netflix.net/packages/19556222468/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069 }, + { url = "https://pypi.netflix.net/packages/19556222469/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263 }, + { url = "https://pypi.netflix.net/packages/19556222470/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429 }, + { url = "https://pypi.netflix.net/packages/19556225252/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363 }, + { url = "https://pypi.netflix.net/packages/19556222471/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786 }, + { url = "https://pypi.netflix.net/packages/19556222472/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133 }, + { url = "https://pypi.netflix.net/packages/19556212691/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4", size = 3282301 }, + { url = "https://pypi.netflix.net/packages/19556212692/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c", size = 3161308 }, + { url = "https://pypi.netflix.net/packages/19556212693/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195", size = 3718964 }, + { url = "https://pypi.netflix.net/packages/19556215581/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5", size = 3373542 }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/19181074713/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19181074321/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236 }, + { url = "https://pypi.netflix.net/packages/19181074322/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084 }, + { url = "https://pypi.netflix.net/packages/19181074323/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832 }, + { url = "https://pypi.netflix.net/packages/19181074324/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052 }, + { url = "https://pypi.netflix.net/packages/19181074325/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555 }, + { url = "https://pypi.netflix.net/packages/19181074326/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128 }, + { url = "https://pypi.netflix.net/packages/19181074327/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445 }, + { url = "https://pypi.netflix.net/packages/19181074328/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165 }, + { url = "https://pypi.netflix.net/packages/19181074329/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891 }, + { url = "https://pypi.netflix.net/packages/19181074330/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796 }, + { url = "https://pypi.netflix.net/packages/19181074331/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121 }, + { url = "https://pypi.netflix.net/packages/19181074332/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070 }, + { url = "https://pypi.netflix.net/packages/19181074333/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859 }, + { url = "https://pypi.netflix.net/packages/19181074334/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296 }, + { url = "https://pypi.netflix.net/packages/19181074335/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124 }, + { url = "https://pypi.netflix.net/packages/19181074433/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698 }, + { url = "https://pypi.netflix.net/packages/19181074434/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819 }, + { url = "https://pypi.netflix.net/packages/19181074435/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766 }, + { url = "https://pypi.netflix.net/packages/19181074436/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771 }, + { url = "https://pypi.netflix.net/packages/19181074437/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586 }, + { url = "https://pypi.netflix.net/packages/19181074438/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792 }, + { url = "https://pypi.netflix.net/packages/19181074439/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909 }, + { url = "https://pypi.netflix.net/packages/19181074440/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946 }, + { url = "https://pypi.netflix.net/packages/19181074441/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705 }, + { url = "https://pypi.netflix.net/packages/19181074442/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244 }, + { url = "https://pypi.netflix.net/packages/19181074443/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637 }, + { url = "https://pypi.netflix.net/packages/19181074552/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925 }, + { url = "https://pypi.netflix.net/packages/19181074553/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045 }, + { url = "https://pypi.netflix.net/packages/19181074554/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835 }, + { url = "https://pypi.netflix.net/packages/19181074555/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109 }, + { url = "https://pypi.netflix.net/packages/19181074556/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930 }, + { url = "https://pypi.netflix.net/packages/19181074557/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964 }, + { url = "https://pypi.netflix.net/packages/19181074558/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065 }, + { url = "https://pypi.netflix.net/packages/19181074559/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088 }, + { url = "https://pypi.netflix.net/packages/19181074560/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193 }, + { url = "https://pypi.netflix.net/packages/19181074561/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488 }, + { url = "https://pypi.netflix.net/packages/19181074562/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669 }, + { url = "https://pypi.netflix.net/packages/19181074563/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709 }, + { url = "https://pypi.netflix.net/packages/19181074564/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563 }, + { url = "https://pypi.netflix.net/packages/19181074565/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756 }, + { url = "https://pypi.netflix.net/packages/19181074712/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408 }, +] + +[[package]] +name = "torch" +version = "2.9.1" +source = { registry = "https://pypi.netflix.net/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.netflix.net/simple" }, marker = "python_full_version < '3.11'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.netflix.net/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "setuptools", marker = "python_full_version >= '3.12'" }, + { name = "sympy" }, + { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://pypi.netflix.net/packages/19327596191/torch-2.9.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:1cc208435f6c379f9b8fdfd5ceb5be1e3b72a6bdf1cb46c0d2812aa73472db9e", size = 104207681 }, + { url = "https://pypi.netflix.net/packages/19327600488/torch-2.9.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:9fd35c68b3679378c11f5eb73220fdcb4e6f4592295277fbb657d31fd053237c", size = 899794036 }, + { url = "https://pypi.netflix.net/packages/19327600489/torch-2.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:2af70e3be4a13becba4655d6cc07dcfec7ae844db6ac38d6c1dafeb245d17d65", size = 110969861 }, + { url = "https://pypi.netflix.net/packages/19327597677/torch-2.9.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:a83b0e84cc375e3318a808d032510dde99d696a85fe9473fc8575612b63ae951", size = 74452222 }, + { url = "https://pypi.netflix.net/packages/19327596933/torch-2.9.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:62b3fd888277946918cba4478cf849303da5359f0fb4e3bfb86b0533ba2eaf8d", size = 104220430 }, + { url = "https://pypi.netflix.net/packages/19327596934/torch-2.9.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d033ff0ac3f5400df862a51bdde9bad83561f3739ea0046e68f5401ebfa67c1b", size = 899821446 }, + { url = "https://pypi.netflix.net/packages/19327601243/torch-2.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:0d06b30a9207b7c3516a9e0102114024755a07045f0c1d2f2a56b1819ac06bcb", size = 110973074 }, + { url = "https://pypi.netflix.net/packages/19327596935/torch-2.9.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:52347912d868653e1528b47cafaf79b285b98be3f4f35d5955389b1b95224475", size = 74463887 }, + { url = "https://pypi.netflix.net/packages/19327597681/torch-2.9.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:da5f6f4d7f4940a173e5572791af238cb0b9e21b1aab592bd8b26da4c99f1cd6", size = 104126592 }, + { url = "https://pypi.netflix.net/packages/19327603071/torch-2.9.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:27331cd902fb4322252657f3902adf1c4f6acad9dcad81d8df3ae14c7c4f07c4", size = 899742281 }, + { url = "https://pypi.netflix.net/packages/19327600495/torch-2.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:81a285002d7b8cfd3fdf1b98aa8df138d41f1a8334fd9ea37511517cedf43083", size = 110940568 }, + { url = "https://pypi.netflix.net/packages/19327600496/torch-2.9.1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:c0d25d1d8e531b8343bea0ed811d5d528958f1dcbd37e7245bc686273177ad7e", size = 74479191 }, + { url = "https://pypi.netflix.net/packages/19327601248/torch-2.9.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c29455d2b910b98738131990394da3e50eea8291dfeb4b12de71ecf1fdeb21cb", size = 104135743 }, + { url = "https://pypi.netflix.net/packages/19327682816/torch-2.9.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:524de44cd13931208ba2c4bde9ec7741fd4ae6bfd06409a604fc32f6520c2bc9", size = 899749493 }, + { url = "https://pypi.netflix.net/packages/19327602003/torch-2.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:545844cc16b3f91e08ce3b40e9c2d77012dd33a48d505aed34b7740ed627a1b2", size = 110944162 }, + { url = "https://pypi.netflix.net/packages/19327601249/torch-2.9.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5be4bf7496f1e3ffb1dd44b672adb1ac3f081f204c5ca81eba6442f5f634df8e", size = 74830751 }, + { url = "https://pypi.netflix.net/packages/19327601250/torch-2.9.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:30a3e170a84894f3652434b56d59a64a2c11366b0ed5776fab33c2439396bf9a", size = 104142929 }, + { url = "https://pypi.netflix.net/packages/19327609028/torch-2.9.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:8301a7b431e51764629208d0edaa4f9e4c33e6df0f2f90b90e261d623df6a4e2", size = 899748978 }, + { url = "https://pypi.netflix.net/packages/19327602006/torch-2.9.1-cp313-cp313t-win_amd64.whl", hash = "sha256:2e1c42c0ae92bf803a4b2409fdfed85e30f9027a66887f5e7dcdbc014c7531db", size = 111176995 }, + { url = "https://pypi.netflix.net/packages/19327602007/torch-2.9.1-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:2c14b3da5df416cf9cb5efab83aa3056f5b8cd8620b8fde81b4987ecab730587", size = 74480347 }, + { url = "https://pypi.netflix.net/packages/19327603080/torch-2.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1edee27a7c9897f4e0b7c14cfc2f3008c571921134522d5b9b5ec4ebbc69041a", size = 74433245 }, + { url = "https://pypi.netflix.net/packages/19327603081/torch-2.9.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:19d144d6b3e29921f1fc70503e9f2fc572cde6a5115c0c0de2f7ca8b1483e8b6", size = 104134804 }, + { url = "https://pypi.netflix.net/packages/19327682825/torch-2.9.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:c432d04376f6d9767a9852ea0def7b47a7bbc8e7af3b16ac9cf9ce02b12851c9", size = 899747132 }, + { url = "https://pypi.netflix.net/packages/19327603843/torch-2.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:d187566a2cdc726fc80138c3cdb260970fab1c27e99f85452721f7759bbd554d", size = 110934845 }, + { url = "https://pypi.netflix.net/packages/19327603082/torch-2.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cb10896a1f7fedaddbccc2017ce6ca9ecaaf990f0973bdfcf405439750118d2c", size = 74823558 }, + { url = "https://pypi.netflix.net/packages/19327682828/torch-2.9.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:0a2bd769944991c74acf0c4ef23603b9c777fdf7637f115605a4b2d8023110c7", size = 104145788 }, + { url = "https://pypi.netflix.net/packages/19327682829/torch-2.9.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:07c8a9660bc9414c39cac530ac83b1fb1b679d7155824144a40a54f4a47bfa73", size = 899735500 }, + { url = "https://pypi.netflix.net/packages/19327609035/torch-2.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c88d3299ddeb2b35dcc31753305612db485ab6f1823e37fb29451c8b2732b87e", size = 111163659 }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.netflix.net/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://pypi.netflix.net/packages/17567615557/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } +wheels = [ + { url = "https://pypi.netflix.net/packages/17567615556/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, +] + +[[package]] +name = "transformers" +version = "4.57.3" +source = { registry = "https://pypi.netflix.net/simple" } +dependencies = [ + { name = "filelock" }, + { name = "huggingface-hub" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.netflix.net/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.0", source = { registry = "https://pypi.netflix.net/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "requests" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, +] +sdist = { url = "https://pypi.netflix.net/packages/19387983500/transformers-4.57.3.tar.gz", hash = "sha256:df4945029aaddd7c09eec5cad851f30662f8bd1746721b34cc031d70c65afebc", size = 10139680 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19387983499/transformers-4.57.3-py3-none-any.whl", hash = "sha256:c77d353a4851b1880191603d36acb313411d3577f6e2897814f333841f7003f4", size = 11993463 }, +] + +[[package]] +name = "triton" +version = "3.5.1" +source = { registry = "https://pypi.netflix.net/simple" } +wheels = [ + { url = "https://pypi.netflix.net/packages/19323343359/triton-3.5.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f63e34dcb32d7bd3a1d0195f60f30d2aee8b08a69a0424189b71017e23dfc3d2", size = 159821655 }, + { url = "https://pypi.netflix.net/packages/19323248465/triton-3.5.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5fc53d849f879911ea13f4a877243afc513187bc7ee92d1f2c0f1ba3169e3c94", size = 170320692 }, + { url = "https://pypi.netflix.net/packages/19323343361/triton-3.5.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da47169e30a779bade679ce78df4810fca6d78a955843d2ddb11f226adc517dc", size = 159928005 }, + { url = "https://pypi.netflix.net/packages/19323248466/triton-3.5.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61413522a48add32302353fdbaaf92daaaab06f6b5e3229940d21b5207f47579", size = 170425802 }, + { url = "https://pypi.netflix.net/packages/19323345697/triton-3.5.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:275a045b6ed670dd1bd005c3e6c2d61846c74c66f4512d6f33cc027b11de8fd4", size = 159940689 }, + { url = "https://pypi.netflix.net/packages/19323248467/triton-3.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d2c6b915a03888ab931a9fd3e55ba36785e1fe70cbea0b40c6ef93b20fc85232", size = 170470207 }, + { url = "https://pypi.netflix.net/packages/19323345699/triton-3.5.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56765ffe12c554cd560698398b8a268db1f616c120007bfd8829d27139abd24a", size = 159955460 }, + { url = "https://pypi.netflix.net/packages/19323248468/triton-3.5.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3f4346b6ebbd4fad18773f5ba839114f4826037c9f2f34e0148894cd5dd3dba", size = 170470410 }, + { url = "https://pypi.netflix.net/packages/19323345701/triton-3.5.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02c770856f5e407d24d28ddc66e33cf026e6f4d360dcb8b2fabe6ea1fc758621", size = 160072799 }, + { url = "https://pypi.netflix.net/packages/19323248584/triton-3.5.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0b4d2c70127fca6a23e247f9348b8adde979d2e7a20391bfbabaac6aebc7e6a8", size = 170579924 }, + { url = "https://pypi.netflix.net/packages/19323356884/triton-3.5.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f617aa7925f9ea9968ec2e1adaf93e87864ff51549c8f04ce658f29bbdb71e2d", size = 159956163 }, + { url = "https://pypi.netflix.net/packages/19323248585/triton-3.5.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0637b1efb1db599a8e9dc960d53ab6e4637db7d4ab6630a0974705d77b14b60", size = 170480488 }, + { url = "https://pypi.netflix.net/packages/19323356886/triton-3.5.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8932391d7f93698dfe5bc9bead77c47a24f97329e9f20c10786bb230a9083f56", size = 160073620 }, + { url = "https://pypi.netflix.net/packages/19323248586/triton-3.5.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bac7f7d959ad0f48c0e97d6643a1cc0fd5786fe61cb1f83b537c6b2d54776478", size = 170582192 }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/19006597195/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19006597194/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.netflix.net/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://pypi.netflix.net/packages/19151033440/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19151033439/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611 }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.netflix.net/simple" } +sdist = { url = "https://pypi.netflix.net/packages/19566271943/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19566271942/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584 }, +] + +[[package]] +name = "uvicorn" +version = "0.40.0" +source = { registry = "https://pypi.netflix.net/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://pypi.netflix.net/packages/19509782004/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761 } +wheels = [ + { url = "https://pypi.netflix.net/packages/19509782003/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502 }, +]