proxilion 0.0.1__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.
- proxilion/__init__.py +136 -0
- proxilion/audit/__init__.py +133 -0
- proxilion/audit/base_exporters.py +527 -0
- proxilion/audit/compliance/__init__.py +130 -0
- proxilion/audit/compliance/base.py +457 -0
- proxilion/audit/compliance/eu_ai_act.py +603 -0
- proxilion/audit/compliance/iso27001.py +544 -0
- proxilion/audit/compliance/soc2.py +491 -0
- proxilion/audit/events.py +493 -0
- proxilion/audit/explainability.py +1173 -0
- proxilion/audit/exporters/__init__.py +58 -0
- proxilion/audit/exporters/aws_s3.py +636 -0
- proxilion/audit/exporters/azure_storage.py +608 -0
- proxilion/audit/exporters/cloud_base.py +468 -0
- proxilion/audit/exporters/gcp_storage.py +570 -0
- proxilion/audit/exporters/multi_exporter.py +498 -0
- proxilion/audit/hash_chain.py +652 -0
- proxilion/audit/logger.py +543 -0
- proxilion/caching/__init__.py +49 -0
- proxilion/caching/tool_cache.py +633 -0
- proxilion/context/__init__.py +73 -0
- proxilion/context/context_window.py +556 -0
- proxilion/context/message_history.py +505 -0
- proxilion/context/session.py +735 -0
- proxilion/contrib/__init__.py +51 -0
- proxilion/contrib/anthropic.py +609 -0
- proxilion/contrib/google.py +1012 -0
- proxilion/contrib/langchain.py +641 -0
- proxilion/contrib/mcp.py +893 -0
- proxilion/contrib/openai.py +646 -0
- proxilion/core.py +3058 -0
- proxilion/decorators.py +966 -0
- proxilion/engines/__init__.py +287 -0
- proxilion/engines/base.py +266 -0
- proxilion/engines/casbin_engine.py +412 -0
- proxilion/engines/opa_engine.py +493 -0
- proxilion/engines/simple.py +437 -0
- proxilion/exceptions.py +887 -0
- proxilion/guards/__init__.py +54 -0
- proxilion/guards/input_guard.py +522 -0
- proxilion/guards/output_guard.py +634 -0
- proxilion/observability/__init__.py +198 -0
- proxilion/observability/cost_tracker.py +866 -0
- proxilion/observability/hooks.py +683 -0
- proxilion/observability/metrics.py +798 -0
- proxilion/observability/session_cost_tracker.py +1063 -0
- proxilion/policies/__init__.py +67 -0
- proxilion/policies/base.py +304 -0
- proxilion/policies/builtin.py +486 -0
- proxilion/policies/registry.py +376 -0
- proxilion/providers/__init__.py +201 -0
- proxilion/providers/adapter.py +468 -0
- proxilion/providers/anthropic_adapter.py +330 -0
- proxilion/providers/gemini_adapter.py +391 -0
- proxilion/providers/openai_adapter.py +294 -0
- proxilion/py.typed +0 -0
- proxilion/resilience/__init__.py +81 -0
- proxilion/resilience/degradation.py +615 -0
- proxilion/resilience/fallback.py +555 -0
- proxilion/resilience/retry.py +554 -0
- proxilion/scheduling/__init__.py +57 -0
- proxilion/scheduling/priority_queue.py +419 -0
- proxilion/scheduling/scheduler.py +459 -0
- proxilion/security/__init__.py +244 -0
- proxilion/security/agent_trust.py +968 -0
- proxilion/security/behavioral_drift.py +794 -0
- proxilion/security/cascade_protection.py +869 -0
- proxilion/security/circuit_breaker.py +428 -0
- proxilion/security/cost_limiter.py +690 -0
- proxilion/security/idor_protection.py +460 -0
- proxilion/security/intent_capsule.py +849 -0
- proxilion/security/intent_validator.py +495 -0
- proxilion/security/memory_integrity.py +767 -0
- proxilion/security/rate_limiter.py +509 -0
- proxilion/security/scope_enforcer.py +680 -0
- proxilion/security/sequence_validator.py +636 -0
- proxilion/security/trust_boundaries.py +784 -0
- proxilion/streaming/__init__.py +70 -0
- proxilion/streaming/detector.py +761 -0
- proxilion/streaming/transformer.py +674 -0
- proxilion/timeouts/__init__.py +55 -0
- proxilion/timeouts/decorators.py +477 -0
- proxilion/timeouts/manager.py +545 -0
- proxilion/tools/__init__.py +69 -0
- proxilion/tools/decorators.py +493 -0
- proxilion/tools/registry.py +732 -0
- proxilion/types.py +339 -0
- proxilion/validation/__init__.py +93 -0
- proxilion/validation/pydantic_schema.py +351 -0
- proxilion/validation/schema.py +651 -0
- proxilion-0.0.1.dist-info/METADATA +872 -0
- proxilion-0.0.1.dist-info/RECORD +94 -0
- proxilion-0.0.1.dist-info/WHEEL +4 -0
- proxilion-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool call result caching.
|
|
3
|
+
|
|
4
|
+
Provides caching for tool call results to avoid redundant
|
|
5
|
+
executions and improve performance.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import fnmatch
|
|
11
|
+
import functools
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import sys
|
|
16
|
+
import threading
|
|
17
|
+
from collections import OrderedDict
|
|
18
|
+
from collections.abc import Callable
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from datetime import datetime, timezone
|
|
21
|
+
from enum import Enum, auto
|
|
22
|
+
from typing import Any, ParamSpec, TypeVar
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
P = ParamSpec("P")
|
|
27
|
+
T = TypeVar("T")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class EvictionPolicy(Enum):
|
|
31
|
+
"""Cache eviction policies."""
|
|
32
|
+
|
|
33
|
+
LRU = auto() # Least Recently Used
|
|
34
|
+
LFU = auto() # Least Frequently Used
|
|
35
|
+
FIFO = auto() # First In First Out
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class CacheConfig:
|
|
40
|
+
"""
|
|
41
|
+
Configuration for the tool cache.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
max_size: Maximum number of entries in cache.
|
|
45
|
+
default_ttl: Default time-to-live in seconds.
|
|
46
|
+
eviction_policy: Policy for evicting entries when full.
|
|
47
|
+
per_user_cache: Whether to maintain separate caches per user.
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
>>> config = CacheConfig(
|
|
51
|
+
... max_size=500,
|
|
52
|
+
... default_ttl=600,
|
|
53
|
+
... eviction_policy=EvictionPolicy.LRU,
|
|
54
|
+
... )
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
max_size: int = 1000
|
|
58
|
+
default_ttl: int | None = 300 # 5 minutes
|
|
59
|
+
eviction_policy: EvictionPolicy = EvictionPolicy.LRU
|
|
60
|
+
per_user_cache: bool = False
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class CacheEntry:
|
|
65
|
+
"""
|
|
66
|
+
A single cache entry.
|
|
67
|
+
|
|
68
|
+
Attributes:
|
|
69
|
+
key: Cache key (hash).
|
|
70
|
+
value: Cached value.
|
|
71
|
+
created_at: When entry was created.
|
|
72
|
+
expires_at: When entry expires (None = never).
|
|
73
|
+
hits: Number of times this entry was accessed.
|
|
74
|
+
size_bytes: Estimated size in bytes.
|
|
75
|
+
tool_name: Name of the cached tool.
|
|
76
|
+
user_id: User ID if per-user caching.
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
>>> entry = CacheEntry(
|
|
80
|
+
... key="abc123",
|
|
81
|
+
... value={"result": "data"},
|
|
82
|
+
... tool_name="get_weather",
|
|
83
|
+
... )
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
key: str
|
|
87
|
+
value: Any
|
|
88
|
+
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
89
|
+
expires_at: datetime | None = None
|
|
90
|
+
hits: int = 0
|
|
91
|
+
size_bytes: int = 0
|
|
92
|
+
tool_name: str = ""
|
|
93
|
+
user_id: str | None = None
|
|
94
|
+
|
|
95
|
+
def __post_init__(self) -> None:
|
|
96
|
+
"""Calculate size on creation."""
|
|
97
|
+
if self.size_bytes == 0:
|
|
98
|
+
self.size_bytes = self._estimate_size(self.value)
|
|
99
|
+
|
|
100
|
+
def _estimate_size(self, obj: Any) -> int:
|
|
101
|
+
"""Estimate object size in bytes."""
|
|
102
|
+
try:
|
|
103
|
+
return sys.getsizeof(json.dumps(obj, default=str))
|
|
104
|
+
except (TypeError, ValueError):
|
|
105
|
+
return sys.getsizeof(str(obj))
|
|
106
|
+
|
|
107
|
+
def is_expired(self) -> bool:
|
|
108
|
+
"""Check if this entry has expired."""
|
|
109
|
+
if self.expires_at is None:
|
|
110
|
+
return False
|
|
111
|
+
return datetime.now(timezone.utc) >= self.expires_at
|
|
112
|
+
|
|
113
|
+
def access(self) -> None:
|
|
114
|
+
"""Record an access to this entry."""
|
|
115
|
+
self.hits += 1
|
|
116
|
+
|
|
117
|
+
def to_dict(self) -> dict[str, Any]:
|
|
118
|
+
"""Convert to dictionary."""
|
|
119
|
+
return {
|
|
120
|
+
"key": self.key,
|
|
121
|
+
"tool_name": self.tool_name,
|
|
122
|
+
"user_id": self.user_id,
|
|
123
|
+
"created_at": self.created_at.isoformat(),
|
|
124
|
+
"expires_at": self.expires_at.isoformat() if self.expires_at else None,
|
|
125
|
+
"hits": self.hits,
|
|
126
|
+
"size_bytes": self.size_bytes,
|
|
127
|
+
"is_expired": self.is_expired(),
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass
|
|
132
|
+
class CacheStats:
|
|
133
|
+
"""
|
|
134
|
+
Cache statistics.
|
|
135
|
+
|
|
136
|
+
Attributes:
|
|
137
|
+
hits: Number of cache hits.
|
|
138
|
+
misses: Number of cache misses.
|
|
139
|
+
evictions: Number of evicted entries.
|
|
140
|
+
size: Current number of entries.
|
|
141
|
+
size_bytes: Estimated total size in bytes.
|
|
142
|
+
|
|
143
|
+
Example:
|
|
144
|
+
>>> stats = cache.get_stats()
|
|
145
|
+
>>> print(f"Hit rate: {stats.hit_rate:.2%}")
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
hits: int = 0
|
|
149
|
+
misses: int = 0
|
|
150
|
+
evictions: int = 0
|
|
151
|
+
size: int = 0
|
|
152
|
+
size_bytes: int = 0
|
|
153
|
+
expirations: int = 0
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def hit_rate(self) -> float:
|
|
157
|
+
"""Calculate cache hit rate."""
|
|
158
|
+
total = self.hits + self.misses
|
|
159
|
+
return self.hits / total if total > 0 else 0.0
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def total_requests(self) -> int:
|
|
163
|
+
"""Get total number of cache requests."""
|
|
164
|
+
return self.hits + self.misses
|
|
165
|
+
|
|
166
|
+
def to_dict(self) -> dict[str, Any]:
|
|
167
|
+
"""Convert to dictionary."""
|
|
168
|
+
return {
|
|
169
|
+
"hits": self.hits,
|
|
170
|
+
"misses": self.misses,
|
|
171
|
+
"evictions": self.evictions,
|
|
172
|
+
"expirations": self.expirations,
|
|
173
|
+
"size": self.size,
|
|
174
|
+
"size_bytes": self.size_bytes,
|
|
175
|
+
"hit_rate": self.hit_rate,
|
|
176
|
+
"total_requests": self.total_requests,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@dataclass
|
|
181
|
+
class CachePolicy:
|
|
182
|
+
"""
|
|
183
|
+
Policy for selective caching.
|
|
184
|
+
|
|
185
|
+
Defines which tools should be cached and their TTL settings.
|
|
186
|
+
|
|
187
|
+
Attributes:
|
|
188
|
+
never_cache: Tools that should never be cached (have side effects).
|
|
189
|
+
short_ttl: Tools with short TTL (frequently changing data).
|
|
190
|
+
long_ttl: Tools with long TTL (stable data).
|
|
191
|
+
default_ttl: Default TTL for unlisted tools.
|
|
192
|
+
|
|
193
|
+
Example:
|
|
194
|
+
>>> policy = CachePolicy(
|
|
195
|
+
... never_cache={"send_email", "create_file"},
|
|
196
|
+
... short_ttl={"get_stock_price": 60},
|
|
197
|
+
... long_ttl={"get_config": 86400},
|
|
198
|
+
... )
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
never_cache: set[str] = field(default_factory=lambda: {
|
|
202
|
+
"send_email",
|
|
203
|
+
"create_file",
|
|
204
|
+
"delete_*",
|
|
205
|
+
"execute_*",
|
|
206
|
+
"write_*",
|
|
207
|
+
"update_*",
|
|
208
|
+
"insert_*",
|
|
209
|
+
})
|
|
210
|
+
short_ttl: dict[str, int] = field(default_factory=lambda: {
|
|
211
|
+
"get_stock_price": 60,
|
|
212
|
+
"get_weather": 300,
|
|
213
|
+
"get_current_time": 1,
|
|
214
|
+
})
|
|
215
|
+
long_ttl: dict[str, int] = field(default_factory=lambda: {
|
|
216
|
+
"get_user_profile": 3600,
|
|
217
|
+
"get_config": 86400,
|
|
218
|
+
"get_schema": 86400,
|
|
219
|
+
})
|
|
220
|
+
default_ttl: int | None = 300
|
|
221
|
+
|
|
222
|
+
def should_cache(self, tool_name: str) -> bool:
|
|
223
|
+
"""
|
|
224
|
+
Check if a tool should be cached.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
tool_name: Name of the tool.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
True if the tool should be cached.
|
|
231
|
+
"""
|
|
232
|
+
return all(not fnmatch.fnmatch(tool_name, pattern) for pattern in self.never_cache)
|
|
233
|
+
|
|
234
|
+
def get_ttl(self, tool_name: str) -> int | None:
|
|
235
|
+
"""
|
|
236
|
+
Get TTL for a tool.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
tool_name: Name of the tool.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
TTL in seconds, or None for no expiration.
|
|
243
|
+
"""
|
|
244
|
+
if tool_name in self.short_ttl:
|
|
245
|
+
return self.short_ttl[tool_name]
|
|
246
|
+
if tool_name in self.long_ttl:
|
|
247
|
+
return self.long_ttl[tool_name]
|
|
248
|
+
return self.default_ttl
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class ToolCache:
|
|
252
|
+
"""
|
|
253
|
+
Tool call result cache.
|
|
254
|
+
|
|
255
|
+
Caches tool call results to avoid redundant executions.
|
|
256
|
+
Supports multiple eviction policies and per-user caching.
|
|
257
|
+
|
|
258
|
+
Example:
|
|
259
|
+
>>> cache = ToolCache(CacheConfig(max_size=500))
|
|
260
|
+
>>>
|
|
261
|
+
>>> # Store result
|
|
262
|
+
>>> cache.set("get_weather", {"city": "NYC"}, {"temp": 72})
|
|
263
|
+
>>>
|
|
264
|
+
>>> # Retrieve result
|
|
265
|
+
>>> result = cache.get("get_weather", {"city": "NYC"})
|
|
266
|
+
>>>
|
|
267
|
+
>>> # Check stats
|
|
268
|
+
>>> print(cache.get_stats())
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
def __init__(
|
|
272
|
+
self,
|
|
273
|
+
config: CacheConfig | None = None,
|
|
274
|
+
policy: CachePolicy | None = None,
|
|
275
|
+
) -> None:
|
|
276
|
+
"""
|
|
277
|
+
Initialize the cache.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
config: Cache configuration.
|
|
281
|
+
policy: Caching policy for tool-specific settings.
|
|
282
|
+
"""
|
|
283
|
+
self.config = config or CacheConfig()
|
|
284
|
+
self.policy = policy or CachePolicy()
|
|
285
|
+
|
|
286
|
+
# Use OrderedDict for LRU/FIFO eviction
|
|
287
|
+
self._cache: OrderedDict[str, CacheEntry] = OrderedDict()
|
|
288
|
+
self._lock = threading.RLock()
|
|
289
|
+
self._stats = CacheStats()
|
|
290
|
+
|
|
291
|
+
def _generate_key(
|
|
292
|
+
self,
|
|
293
|
+
tool_name: str,
|
|
294
|
+
args: dict[str, Any],
|
|
295
|
+
user_id: str | None = None,
|
|
296
|
+
) -> str:
|
|
297
|
+
"""
|
|
298
|
+
Generate deterministic cache key.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
tool_name: Name of the tool.
|
|
302
|
+
args: Tool arguments.
|
|
303
|
+
user_id: Optional user ID for per-user caching.
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
SHA-256 hash as cache key.
|
|
307
|
+
"""
|
|
308
|
+
key_data = {
|
|
309
|
+
"tool": tool_name,
|
|
310
|
+
"args": json.dumps(args, sort_keys=True, default=str),
|
|
311
|
+
}
|
|
312
|
+
if self.config.per_user_cache and user_id:
|
|
313
|
+
key_data["user"] = user_id
|
|
314
|
+
|
|
315
|
+
key_str = json.dumps(key_data, sort_keys=True)
|
|
316
|
+
return hashlib.sha256(key_str.encode()).hexdigest()
|
|
317
|
+
|
|
318
|
+
def get(
|
|
319
|
+
self,
|
|
320
|
+
tool_name: str,
|
|
321
|
+
args: dict[str, Any],
|
|
322
|
+
user_id: str | None = None,
|
|
323
|
+
) -> Any | None:
|
|
324
|
+
"""
|
|
325
|
+
Get a cached result.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
tool_name: Name of the tool.
|
|
329
|
+
args: Tool arguments.
|
|
330
|
+
user_id: Optional user ID.
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
Cached value or None if not found/expired.
|
|
334
|
+
"""
|
|
335
|
+
key = self._generate_key(tool_name, args, user_id)
|
|
336
|
+
|
|
337
|
+
with self._lock:
|
|
338
|
+
entry = self._cache.get(key)
|
|
339
|
+
|
|
340
|
+
if entry is None:
|
|
341
|
+
self._stats.misses += 1
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
if entry.is_expired():
|
|
345
|
+
# Remove expired entry
|
|
346
|
+
del self._cache[key]
|
|
347
|
+
self._stats.misses += 1
|
|
348
|
+
self._stats.expirations += 1
|
|
349
|
+
return None
|
|
350
|
+
|
|
351
|
+
# Record hit and move to end (for LRU)
|
|
352
|
+
entry.access()
|
|
353
|
+
self._stats.hits += 1
|
|
354
|
+
|
|
355
|
+
if self.config.eviction_policy == EvictionPolicy.LRU:
|
|
356
|
+
self._cache.move_to_end(key)
|
|
357
|
+
|
|
358
|
+
return entry.value
|
|
359
|
+
|
|
360
|
+
def set(
|
|
361
|
+
self,
|
|
362
|
+
tool_name: str,
|
|
363
|
+
args: dict[str, Any],
|
|
364
|
+
result: Any,
|
|
365
|
+
ttl: int | None = None,
|
|
366
|
+
user_id: str | None = None,
|
|
367
|
+
) -> bool:
|
|
368
|
+
"""
|
|
369
|
+
Cache a tool result.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
tool_name: Name of the tool.
|
|
373
|
+
args: Tool arguments.
|
|
374
|
+
result: Result to cache.
|
|
375
|
+
ttl: Time-to-live in seconds (overrides policy).
|
|
376
|
+
user_id: Optional user ID.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
True if cached, False if caching is disabled for this tool.
|
|
380
|
+
"""
|
|
381
|
+
# Check policy
|
|
382
|
+
if not self.policy.should_cache(tool_name):
|
|
383
|
+
return False
|
|
384
|
+
|
|
385
|
+
# Get TTL
|
|
386
|
+
if ttl is None:
|
|
387
|
+
ttl = self.policy.get_ttl(tool_name)
|
|
388
|
+
|
|
389
|
+
key = self._generate_key(tool_name, args, user_id)
|
|
390
|
+
|
|
391
|
+
# Calculate expiration
|
|
392
|
+
expires_at = None
|
|
393
|
+
if ttl is not None:
|
|
394
|
+
from datetime import timedelta
|
|
395
|
+
expires_at = datetime.now(timezone.utc) + timedelta(seconds=ttl)
|
|
396
|
+
|
|
397
|
+
entry = CacheEntry(
|
|
398
|
+
key=key,
|
|
399
|
+
value=result,
|
|
400
|
+
expires_at=expires_at,
|
|
401
|
+
tool_name=tool_name,
|
|
402
|
+
user_id=user_id,
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
with self._lock:
|
|
406
|
+
# Evict if necessary
|
|
407
|
+
while len(self._cache) >= self.config.max_size:
|
|
408
|
+
self._evict_one()
|
|
409
|
+
|
|
410
|
+
self._cache[key] = entry
|
|
411
|
+
self._stats.size = len(self._cache)
|
|
412
|
+
self._stats.size_bytes += entry.size_bytes
|
|
413
|
+
|
|
414
|
+
return True
|
|
415
|
+
|
|
416
|
+
def _evict_one(self) -> None:
|
|
417
|
+
"""Evict one entry based on policy."""
|
|
418
|
+
if not self._cache:
|
|
419
|
+
return
|
|
420
|
+
|
|
421
|
+
if self.config.eviction_policy == EvictionPolicy.LRU:
|
|
422
|
+
# Remove oldest (first) item
|
|
423
|
+
key, entry = self._cache.popitem(last=False)
|
|
424
|
+
elif self.config.eviction_policy == EvictionPolicy.FIFO:
|
|
425
|
+
# Remove first inserted item
|
|
426
|
+
key, entry = self._cache.popitem(last=False)
|
|
427
|
+
elif self.config.eviction_policy == EvictionPolicy.LFU:
|
|
428
|
+
# Remove least frequently used
|
|
429
|
+
min_hits = float("inf")
|
|
430
|
+
min_key = None
|
|
431
|
+
for k, e in self._cache.items():
|
|
432
|
+
if e.hits < min_hits:
|
|
433
|
+
min_hits = e.hits
|
|
434
|
+
min_key = k
|
|
435
|
+
if min_key:
|
|
436
|
+
entry = self._cache.pop(min_key)
|
|
437
|
+
else:
|
|
438
|
+
return
|
|
439
|
+
else:
|
|
440
|
+
return
|
|
441
|
+
|
|
442
|
+
self._stats.evictions += 1
|
|
443
|
+
self._stats.size_bytes -= entry.size_bytes
|
|
444
|
+
logger.debug(f"Evicted cache entry: {entry.tool_name}")
|
|
445
|
+
|
|
446
|
+
def invalidate(
|
|
447
|
+
self,
|
|
448
|
+
tool_name: str,
|
|
449
|
+
args: dict[str, Any] | None = None,
|
|
450
|
+
user_id: str | None = None,
|
|
451
|
+
) -> int:
|
|
452
|
+
"""
|
|
453
|
+
Invalidate cache entries.
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
tool_name: Name of the tool to invalidate.
|
|
457
|
+
args: Specific arguments (None = all entries for tool).
|
|
458
|
+
user_id: Specific user (None = all users).
|
|
459
|
+
|
|
460
|
+
Returns:
|
|
461
|
+
Number of entries invalidated.
|
|
462
|
+
"""
|
|
463
|
+
with self._lock:
|
|
464
|
+
if args is not None:
|
|
465
|
+
# Invalidate specific entry
|
|
466
|
+
key = self._generate_key(tool_name, args, user_id)
|
|
467
|
+
if key in self._cache:
|
|
468
|
+
entry = self._cache.pop(key)
|
|
469
|
+
self._stats.size_bytes -= entry.size_bytes
|
|
470
|
+
self._stats.size = len(self._cache)
|
|
471
|
+
return 1
|
|
472
|
+
return 0
|
|
473
|
+
|
|
474
|
+
# Invalidate all entries for this tool
|
|
475
|
+
keys_to_remove = [
|
|
476
|
+
k for k, v in self._cache.items()
|
|
477
|
+
if v.tool_name == tool_name
|
|
478
|
+
and (user_id is None or v.user_id == user_id)
|
|
479
|
+
]
|
|
480
|
+
|
|
481
|
+
for key in keys_to_remove:
|
|
482
|
+
entry = self._cache.pop(key)
|
|
483
|
+
self._stats.size_bytes -= entry.size_bytes
|
|
484
|
+
|
|
485
|
+
self._stats.size = len(self._cache)
|
|
486
|
+
return len(keys_to_remove)
|
|
487
|
+
|
|
488
|
+
def clear(self) -> int:
|
|
489
|
+
"""
|
|
490
|
+
Clear all cache entries.
|
|
491
|
+
|
|
492
|
+
Returns:
|
|
493
|
+
Number of entries cleared.
|
|
494
|
+
"""
|
|
495
|
+
with self._lock:
|
|
496
|
+
count = len(self._cache)
|
|
497
|
+
self._cache.clear()
|
|
498
|
+
self._stats.size = 0
|
|
499
|
+
self._stats.size_bytes = 0
|
|
500
|
+
return count
|
|
501
|
+
|
|
502
|
+
def get_stats(self) -> CacheStats:
|
|
503
|
+
"""
|
|
504
|
+
Get cache statistics.
|
|
505
|
+
|
|
506
|
+
Returns:
|
|
507
|
+
Current cache statistics.
|
|
508
|
+
"""
|
|
509
|
+
with self._lock:
|
|
510
|
+
self._stats.size = len(self._cache)
|
|
511
|
+
return CacheStats(
|
|
512
|
+
hits=self._stats.hits,
|
|
513
|
+
misses=self._stats.misses,
|
|
514
|
+
evictions=self._stats.evictions,
|
|
515
|
+
expirations=self._stats.expirations,
|
|
516
|
+
size=self._stats.size,
|
|
517
|
+
size_bytes=self._stats.size_bytes,
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
def get_entries(self, tool_name: str | None = None) -> list[CacheEntry]:
|
|
521
|
+
"""
|
|
522
|
+
Get all cache entries.
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
tool_name: Filter by tool name (optional).
|
|
526
|
+
|
|
527
|
+
Returns:
|
|
528
|
+
List of cache entries.
|
|
529
|
+
"""
|
|
530
|
+
with self._lock:
|
|
531
|
+
entries = list(self._cache.values())
|
|
532
|
+
if tool_name:
|
|
533
|
+
entries = [e for e in entries if e.tool_name == tool_name]
|
|
534
|
+
return entries
|
|
535
|
+
|
|
536
|
+
def cleanup_expired(self) -> int:
|
|
537
|
+
"""
|
|
538
|
+
Remove all expired entries.
|
|
539
|
+
|
|
540
|
+
Returns:
|
|
541
|
+
Number of entries removed.
|
|
542
|
+
"""
|
|
543
|
+
with self._lock:
|
|
544
|
+
expired_keys = [
|
|
545
|
+
k for k, v in self._cache.items()
|
|
546
|
+
if v.is_expired()
|
|
547
|
+
]
|
|
548
|
+
|
|
549
|
+
for key in expired_keys:
|
|
550
|
+
entry = self._cache.pop(key)
|
|
551
|
+
self._stats.size_bytes -= entry.size_bytes
|
|
552
|
+
self._stats.expirations += 1
|
|
553
|
+
|
|
554
|
+
self._stats.size = len(self._cache)
|
|
555
|
+
return len(expired_keys)
|
|
556
|
+
|
|
557
|
+
def __contains__(self, key: tuple[str, dict[str, Any]]) -> bool:
|
|
558
|
+
"""Check if a tool/args combination is cached."""
|
|
559
|
+
tool_name, args = key
|
|
560
|
+
return self.get(tool_name, args) is not None
|
|
561
|
+
|
|
562
|
+
def __len__(self) -> int:
|
|
563
|
+
"""Get number of cached entries."""
|
|
564
|
+
with self._lock:
|
|
565
|
+
return len(self._cache)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def cached_tool(
|
|
569
|
+
cache: ToolCache,
|
|
570
|
+
ttl: int | None = None,
|
|
571
|
+
key_params: list[str] | None = None,
|
|
572
|
+
) -> Callable[[Callable[P, T]], Callable[P, T]]:
|
|
573
|
+
"""
|
|
574
|
+
Decorator that caches tool call results.
|
|
575
|
+
|
|
576
|
+
Args:
|
|
577
|
+
cache: ToolCache instance to use.
|
|
578
|
+
ttl: Time-to-live in seconds (overrides policy).
|
|
579
|
+
key_params: Specific parameters to use for cache key.
|
|
580
|
+
|
|
581
|
+
Returns:
|
|
582
|
+
Decorated function.
|
|
583
|
+
|
|
584
|
+
Example:
|
|
585
|
+
>>> cache = ToolCache()
|
|
586
|
+
>>>
|
|
587
|
+
>>> @cached_tool(cache, ttl=300)
|
|
588
|
+
... def get_weather(city: str) -> dict:
|
|
589
|
+
... return weather_api.get(city)
|
|
590
|
+
>>>
|
|
591
|
+
>>> # First call - cache miss, executes function
|
|
592
|
+
>>> result1 = get_weather("NYC")
|
|
593
|
+
>>>
|
|
594
|
+
>>> # Second call - cache hit, returns cached value
|
|
595
|
+
>>> result2 = get_weather("NYC")
|
|
596
|
+
"""
|
|
597
|
+
|
|
598
|
+
def decorator(func: Callable[P, T]) -> Callable[P, T]:
|
|
599
|
+
tool_name = func.__name__
|
|
600
|
+
|
|
601
|
+
@functools.wraps(func)
|
|
602
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
603
|
+
# Build args dict for cache key
|
|
604
|
+
import inspect
|
|
605
|
+
sig = inspect.signature(func)
|
|
606
|
+
bound = sig.bind(*args, **kwargs)
|
|
607
|
+
bound.apply_defaults()
|
|
608
|
+
all_args = dict(bound.arguments)
|
|
609
|
+
|
|
610
|
+
# Filter to key_params if specified
|
|
611
|
+
if key_params:
|
|
612
|
+
cache_args = {k: v for k, v in all_args.items() if k in key_params}
|
|
613
|
+
else:
|
|
614
|
+
cache_args = all_args
|
|
615
|
+
|
|
616
|
+
# Check cache
|
|
617
|
+
cached_result = cache.get(tool_name, cache_args)
|
|
618
|
+
if cached_result is not None:
|
|
619
|
+
logger.debug(f"Cache hit for {tool_name}")
|
|
620
|
+
return cached_result
|
|
621
|
+
|
|
622
|
+
# Execute function
|
|
623
|
+
result = func(*args, **kwargs)
|
|
624
|
+
|
|
625
|
+
# Store in cache
|
|
626
|
+
cache.set(tool_name, cache_args, result, ttl=ttl)
|
|
627
|
+
logger.debug(f"Cached result for {tool_name}")
|
|
628
|
+
|
|
629
|
+
return result
|
|
630
|
+
|
|
631
|
+
return wrapper
|
|
632
|
+
|
|
633
|
+
return decorator
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Context management for AI agent sessions.
|
|
3
|
+
|
|
4
|
+
Provides:
|
|
5
|
+
- Session tracking with metadata
|
|
6
|
+
- Message history management
|
|
7
|
+
- Context window optimization
|
|
8
|
+
- Multi-turn conversation state
|
|
9
|
+
|
|
10
|
+
This module enables stateful agent interactions, including context windows,
|
|
11
|
+
message history, and session metadata. Essential for chatbot applications
|
|
12
|
+
and multi-turn agent conversations.
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
>>> from proxilion.context import (
|
|
16
|
+
... Session, SessionManager, SessionConfig,
|
|
17
|
+
... MessageHistory, Message, MessageRole,
|
|
18
|
+
... ContextWindow, ContextStrategy,
|
|
19
|
+
... )
|
|
20
|
+
>>> from proxilion.types import UserContext
|
|
21
|
+
>>>
|
|
22
|
+
>>> # Create a session manager
|
|
23
|
+
>>> config = SessionConfig(max_duration=3600, max_messages=100)
|
|
24
|
+
>>> manager = SessionManager(config)
|
|
25
|
+
>>>
|
|
26
|
+
>>> # Create a session for a user
|
|
27
|
+
>>> user = UserContext(user_id="user_123", roles=["user"])
|
|
28
|
+
>>> session = manager.create_session(user)
|
|
29
|
+
>>>
|
|
30
|
+
>>> # Add messages
|
|
31
|
+
>>> session.add_message(MessageRole.USER, "Hello, how can you help?")
|
|
32
|
+
>>> session.add_message(MessageRole.ASSISTANT, "I can help with many tasks!")
|
|
33
|
+
>>>
|
|
34
|
+
>>> # Get context for LLM
|
|
35
|
+
>>> context = session.get_context_for_llm(max_tokens=4000)
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from proxilion.context.context_window import (
|
|
39
|
+
ContextStrategy,
|
|
40
|
+
ContextWindow,
|
|
41
|
+
KeepSystemRecentStrategy,
|
|
42
|
+
SlidingWindowStrategy,
|
|
43
|
+
)
|
|
44
|
+
from proxilion.context.message_history import (
|
|
45
|
+
Message,
|
|
46
|
+
MessageHistory,
|
|
47
|
+
MessageRole,
|
|
48
|
+
estimate_tokens,
|
|
49
|
+
)
|
|
50
|
+
from proxilion.context.session import (
|
|
51
|
+
Session,
|
|
52
|
+
SessionConfig,
|
|
53
|
+
SessionManager,
|
|
54
|
+
SessionState,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
__all__ = [
|
|
58
|
+
# Session management
|
|
59
|
+
"Session",
|
|
60
|
+
"SessionConfig",
|
|
61
|
+
"SessionManager",
|
|
62
|
+
"SessionState",
|
|
63
|
+
# Message history
|
|
64
|
+
"Message",
|
|
65
|
+
"MessageHistory",
|
|
66
|
+
"MessageRole",
|
|
67
|
+
"estimate_tokens",
|
|
68
|
+
# Context window
|
|
69
|
+
"ContextStrategy",
|
|
70
|
+
"ContextWindow",
|
|
71
|
+
"KeepSystemRecentStrategy",
|
|
72
|
+
"SlidingWindowStrategy",
|
|
73
|
+
]
|