longcache 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
longcache/cli.py ADDED
@@ -0,0 +1,216 @@
1
+ """
2
+ LongCache CLI — Command-line interface for cache management.
3
+
4
+ Provides commands for viewing stats, clearing cache, inspecting
5
+ entries, and exporting cache data.
6
+
7
+ Usage:
8
+ longcache stats
9
+ longcache clear --namespace default
10
+ longcache clear --all
11
+ longcache inspect --query "refund"
12
+ longcache export --format json
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import sys
19
+ from typing import Optional
20
+
21
+ import click
22
+
23
+ from longcache.config import CacheConfig
24
+
25
+
26
+ def _get_cache(config: Optional[CacheConfig] = None):
27
+ """Get a LongCache instance, lazily initialized."""
28
+ from longcache import LongCache
29
+
30
+ return LongCache(config or CacheConfig())
31
+
32
+
33
+ @click.group()
34
+ @click.version_option(version="0.1.0", prog_name="longcache")
35
+ def main():
36
+ """
37
+ LongCache — Semantic similarity cache for LLM/RAG pipelines.
38
+
39
+ View stats, manage entries, and configure your cache from the command line.
40
+ """
41
+ pass
42
+
43
+
44
+ @main.command()
45
+ @click.option("--namespace", default=None, help="Namespace to show stats for.")
46
+ @click.option("--db-path", default=".longcache/cache.db", help="Path to cache database.")
47
+ def stats(namespace: Optional[str], db_path: str):
48
+ """
49
+ Show cache hit rate, estimated savings, and entry count.
50
+
51
+ Displays key metrics about your cache performance including
52
+ hit rate, estimated tokens saved, and estimated cost savings.
53
+
54
+ Examples:
55
+
56
+ longcache stats
57
+
58
+ longcache stats --namespace faq
59
+ """
60
+ config = CacheConfig(db_path=db_path, namespace=namespace or "default")
61
+ cache = _get_cache(config)
62
+
63
+ s = cache.stats()
64
+
65
+ click.echo("=" * 50)
66
+ click.echo(" LongCache Statistics")
67
+ click.echo("=" * 50)
68
+ click.echo(f" Namespace: {config.namespace}")
69
+ click.echo(f" Cache Hits: {s['hits']}")
70
+ click.echo(f" Cache Misses: {s['misses']}")
71
+ click.echo(f" Hit Rate: {s['hit_rate']:.1%}")
72
+ click.echo(f" Total Entries: {s['total_entries']}")
73
+ click.echo(f" Est. Tokens Saved: {s['estimated_tokens_saved']:,}")
74
+ click.echo(f" Est. Cost Saved: ${s['estimated_cost_saved_usd']:.4f}")
75
+ click.echo("=" * 50)
76
+
77
+
78
+ @main.command()
79
+ @click.option("--namespace", default=None, help="Namespace to clear.")
80
+ @click.option("--all", "clear_all", is_flag=True, help="Clear ALL namespaces.")
81
+ @click.option("--db-path", default=".longcache/cache.db", help="Path to cache database.")
82
+ @click.confirmation_option(prompt="Are you sure you want to clear the cache?")
83
+ def clear(namespace: Optional[str], clear_all: bool, db_path: str):
84
+ """
85
+ Clear cache entries by namespace or entirely.
86
+
87
+ Use --namespace to clear a specific namespace, or --all
88
+ to clear the entire cache across all namespaces.
89
+
90
+ Examples:
91
+
92
+ longcache clear --namespace faq
93
+
94
+ longcache clear --all
95
+ """
96
+ config = CacheConfig(db_path=db_path)
97
+ cache = _get_cache(config)
98
+
99
+ if clear_all:
100
+ count = cache.clear()
101
+ click.echo(f"Cleared entire cache: {count} entries deleted.")
102
+ elif namespace:
103
+ count = cache.invalidate(namespace)
104
+ click.echo(f"Cleared namespace '{namespace}': {count} entries deleted.")
105
+ else:
106
+ count = cache.invalidate()
107
+ click.echo(f"Cleared namespace '{config.namespace}': {count} entries deleted.")
108
+
109
+
110
+ @main.command()
111
+ @click.option("--query", required=True, help="Query text to search for similar cached entries.")
112
+ @click.option("--namespace", default=None, help="Namespace to search in.")
113
+ @click.option("--db-path", default=".longcache/cache.db", help="Path to cache database.")
114
+ def inspect(query: str, namespace: Optional[str], db_path: str):
115
+ """
116
+ Inspect the nearest cached entry for a query.
117
+
118
+ Shows the most similar cached entry regardless of the
119
+ similarity threshold. Useful for debugging and understanding
120
+ what would be cached for a given query.
121
+
122
+ Examples:
123
+
124
+ longcache inspect --query "What is the refund policy?"
125
+
126
+ longcache inspect --query "refund" --namespace faq
127
+ """
128
+ config = CacheConfig(db_path=db_path, namespace=namespace or "default")
129
+ cache = _get_cache(config)
130
+
131
+ result = cache.inspect(query, namespace=namespace)
132
+
133
+ if result is None:
134
+ click.echo(f"No similar entries found for query: {query!r}")
135
+ return
136
+
137
+ click.echo("=" * 50)
138
+ click.echo(" Nearest Cached Entry")
139
+ click.echo("=" * 50)
140
+ click.echo(f" ID: {result['id']}")
141
+ click.echo(f" Cached Query: {result['query']!r}")
142
+ click.echo(f" Namespace: {result['namespace']}")
143
+ click.echo(f" Hit Count: {result['hit_count']}")
144
+ click.echo(f" Created At: {result['created_at']}")
145
+ click.echo(f" Expires At: {result['expires_at'] or 'Never'}")
146
+ click.echo(f" Is Expired: {result['is_expired']}")
147
+ click.echo("=" * 50)
148
+
149
+
150
+ @main.command()
151
+ @click.option(
152
+ "--format",
153
+ "export_format",
154
+ default="json",
155
+ type=click.Choice(["json"]),
156
+ help="Export format (currently only JSON supported).",
157
+ )
158
+ @click.option("--namespace", default=None, help="Only export entries from this namespace.")
159
+ @click.option("--db-path", default=".longcache/cache.db", help="Path to cache database.")
160
+ @click.option("--output", "-o", default=None, help="Output file path. Defaults to stdout.")
161
+ def export(export_format: str, namespace: Optional[str], db_path: str, output: Optional[str]):
162
+ """
163
+ Export cache entries as JSON.
164
+
165
+ Dumps all cache entries (or entries from a specific namespace)
166
+ in JSON format. Useful for backup, migration, or analysis.
167
+
168
+ Examples:
169
+
170
+ longcache export --format json
171
+
172
+ longcache export --namespace faq -o faq_cache.json
173
+ """
174
+ config = CacheConfig(db_path=db_path, namespace=namespace or "default")
175
+ cache = _get_cache(config)
176
+
177
+ # Access the backend directly for export
178
+ from longcache.backends.sqlite import SQLiteCacheBackend
179
+
180
+ backend = cache.backend
181
+
182
+ if isinstance(backend, SQLiteCacheBackend):
183
+ entries = backend.export_all()
184
+ else:
185
+ # Generic fallback
186
+ ns = namespace or config.namespace
187
+ entries_raw = backend.list_entries(ns)
188
+ entries = [
189
+ {
190
+ "id": e.id,
191
+ "namespace": e.namespace,
192
+ "query": e.query,
193
+ "response": str(e.response),
194
+ "created_at": e.created_at,
195
+ "expires_at": e.expires_at,
196
+ "hit_count": e.hit_count,
197
+ }
198
+ for e in entries_raw
199
+ ]
200
+
201
+ # Filter by namespace if specified
202
+ if namespace:
203
+ entries = [e for e in entries if e.get("namespace") == namespace]
204
+
205
+ json_output = json.dumps(entries, indent=2, default=str)
206
+
207
+ if output:
208
+ with open(output, "w") as f:
209
+ f.write(json_output)
210
+ click.echo(f"Exported {len(entries)} entries to {output}")
211
+ else:
212
+ click.echo(json_output)
213
+
214
+
215
+ if __name__ == "__main__":
216
+ main()
longcache/config.py ADDED
@@ -0,0 +1,127 @@
1
+ """
2
+ Configuration for LongCache.
3
+
4
+ Provides CacheConfig dataclass with sensible defaults,
5
+ validation, and factory methods for construction from dicts or env vars.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from dataclasses import dataclass, field, asdict
12
+ from typing import Optional
13
+
14
+
15
+ @dataclass
16
+ class CacheConfig:
17
+ """
18
+ Configuration for LongCache with sensible defaults.
19
+
20
+ Attributes:
21
+ similarity_threshold: Cosine similarity threshold for cache hits (0.0-1.0).
22
+ Higher = stricter matching. 0.92 is a good default for most RAG use cases.
23
+ ttl_seconds: Time-to-live for cache entries in seconds. 0 = no expiry.
24
+ max_entries: Maximum number of entries per namespace. Oldest entries
25
+ are evicted first when the limit is reached.
26
+ namespace: Default namespace for cache entries. Namespaces isolate
27
+ different applications or use cases sharing the same backend.
28
+ embedder_model: Sentence-transformers model name for local embedding.
29
+ Must be compatible with sentence-transformers library.
30
+ backend: Storage backend type. One of: "sqlite", "redis", "memory".
31
+ db_path: Path to the SQLite database file (only used when backend="sqlite").
32
+ redis_url: Redis connection URL (only used when backend="redis").
33
+ track_stats: Whether to track hit/miss statistics internally.
34
+ """
35
+
36
+ similarity_threshold: float = 0.92
37
+ ttl_seconds: int = 3600
38
+ max_entries: int = 10_000
39
+ namespace: str = "default"
40
+ embedder_model: str = "BAAI/bge-small-en-v1.5"
41
+ backend: str = "sqlite"
42
+ db_path: str = ".longcache/cache.db"
43
+ redis_url: str = "redis://localhost:6379/0"
44
+ track_stats: bool = True
45
+ faiss_index_type: str = "flat"
46
+ hnsw_m: int = 32
47
+ hnsw_ef_search: int = 64
48
+ model: str = ""
49
+ system_prompt: str = ""
50
+ temperature: float = -1.0
51
+
52
+ def __post_init__(self):
53
+ """Validate configuration values after initialization."""
54
+ if not 0.0 <= self.similarity_threshold <= 1.0:
55
+ raise ValueError(
56
+ f"similarity_threshold must be between 0.0 and 1.0, got {self.similarity_threshold}"
57
+ )
58
+ if self.ttl_seconds < 0:
59
+ raise ValueError(f"ttl_seconds must be >= 0, got {self.ttl_seconds}")
60
+ if self.max_entries < 1:
61
+ raise ValueError(f"max_entries must be >= 1, got {self.max_entries}")
62
+ if self.backend not in ("sqlite", "redis", "async_redis", "memory"):
63
+ raise ValueError(
64
+ f"backend must be one of 'sqlite', 'redis', 'async_redis', 'memory', "
65
+ f"got '{self.backend}'"
66
+ )
67
+
68
+ @classmethod
69
+ def from_dict(cls, data: dict) -> CacheConfig:
70
+ """
71
+ Create a CacheConfig from a dictionary.
72
+
73
+ Only keys that match CacheConfig fields are used; others are ignored.
74
+
75
+ Args:
76
+ data: Dictionary with configuration keys.
77
+
78
+ Returns:
79
+ CacheConfig instance with values from the dict.
80
+ """
81
+ valid_keys = {f.name for f in cls.__dataclass_fields__.values()}
82
+ filtered = {k: v for k, v in data.items() if k in valid_keys}
83
+ return cls(**filtered)
84
+
85
+ @classmethod
86
+ def from_env(cls, prefix: str = "LONGCACHE_") -> CacheConfig:
87
+ """
88
+ Create a CacheConfig from environment variables.
89
+
90
+ Environment variables are mapped by uppercasing the field name and
91
+ prepending the prefix. For example, similarity_threshold becomes
92
+ LONGCACHE_SIMILARITY_THRESHOLD.
93
+
94
+ Args:
95
+ prefix: Environment variable prefix. Defaults to "LONGCACHE_".
96
+
97
+ Returns:
98
+ CacheConfig instance with values from environment variables.
99
+ """
100
+ field_types = {f.name: f.type for f in cls.__dataclass_fields__.values()}
101
+ env_values = {}
102
+
103
+ for field_name in cls.__dataclass_fields__:
104
+ env_key = f"{prefix}{field_name.upper()}"
105
+ env_val = os.environ.get(env_key)
106
+ if env_val is not None:
107
+ # Cast to appropriate type
108
+ field_type = field_types[field_name]
109
+ if field_type == "float":
110
+ env_values[field_name] = float(env_val)
111
+ elif field_type == "int":
112
+ env_values[field_name] = int(env_val)
113
+ elif field_type == "bool":
114
+ env_values[field_name] = env_val.lower() in ("true", "1", "yes")
115
+ else:
116
+ env_values[field_name] = env_val
117
+
118
+ return cls(**env_values)
119
+
120
+ def to_dict(self) -> dict:
121
+ """
122
+ Convert the configuration to a dictionary.
123
+
124
+ Returns:
125
+ Dictionary representation of the configuration.
126
+ """
127
+ return asdict(self)
@@ -0,0 +1,8 @@
1
+ """Core LongCache engine components."""
2
+
3
+ from longcache.core.cache import LongCache
4
+ from longcache.core.entry import CacheEntry
5
+ from longcache.core.embedder import LocalEmbedder
6
+ from longcache.core.index import SimilarityIndex
7
+
8
+ __all__ = ["LongCache", "CacheEntry", "LocalEmbedder", "SimilarityIndex"]
@@ -0,0 +1,43 @@
1
+ """Abstract base class for all embedder implementations."""
2
+
3
+ from __future__ import annotations
4
+ from abc import ABC, abstractmethod
5
+ import numpy as np
6
+
7
+
8
+ class BaseEmbedder(ABC):
9
+ """
10
+ Abstract interface for embedding models.
11
+
12
+ All embedders must produce L2-normalized float32 vectors
13
+ of consistent dimension. The dimension is used to initialize
14
+ the FAISS index and must not change between calls.
15
+ """
16
+
17
+ @property
18
+ @abstractmethod
19
+ def dimension(self) -> int:
20
+ """Return the dimensionality of the embedding vectors."""
21
+ ...
22
+
23
+ @abstractmethod
24
+ def embed(self, text: str) -> np.ndarray:
25
+ """
26
+ Embed a single text string.
27
+
28
+ Returns:
29
+ L2-normalized float32 numpy array of shape (dimension,).
30
+ """
31
+ ...
32
+
33
+ def embed_batch(self, texts: list[str]) -> np.ndarray:
34
+ """
35
+ Embed multiple texts. Default: loop over embed().
36
+ Override for batched efficiency.
37
+
38
+ Returns:
39
+ float32 array of shape (len(texts), dimension).
40
+ """
41
+ import numpy as np
42
+
43
+ return np.stack([self.embed(t) for t in texts]).astype(np.float32)