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.
Files changed (94) hide show
  1. proxilion/__init__.py +136 -0
  2. proxilion/audit/__init__.py +133 -0
  3. proxilion/audit/base_exporters.py +527 -0
  4. proxilion/audit/compliance/__init__.py +130 -0
  5. proxilion/audit/compliance/base.py +457 -0
  6. proxilion/audit/compliance/eu_ai_act.py +603 -0
  7. proxilion/audit/compliance/iso27001.py +544 -0
  8. proxilion/audit/compliance/soc2.py +491 -0
  9. proxilion/audit/events.py +493 -0
  10. proxilion/audit/explainability.py +1173 -0
  11. proxilion/audit/exporters/__init__.py +58 -0
  12. proxilion/audit/exporters/aws_s3.py +636 -0
  13. proxilion/audit/exporters/azure_storage.py +608 -0
  14. proxilion/audit/exporters/cloud_base.py +468 -0
  15. proxilion/audit/exporters/gcp_storage.py +570 -0
  16. proxilion/audit/exporters/multi_exporter.py +498 -0
  17. proxilion/audit/hash_chain.py +652 -0
  18. proxilion/audit/logger.py +543 -0
  19. proxilion/caching/__init__.py +49 -0
  20. proxilion/caching/tool_cache.py +633 -0
  21. proxilion/context/__init__.py +73 -0
  22. proxilion/context/context_window.py +556 -0
  23. proxilion/context/message_history.py +505 -0
  24. proxilion/context/session.py +735 -0
  25. proxilion/contrib/__init__.py +51 -0
  26. proxilion/contrib/anthropic.py +609 -0
  27. proxilion/contrib/google.py +1012 -0
  28. proxilion/contrib/langchain.py +641 -0
  29. proxilion/contrib/mcp.py +893 -0
  30. proxilion/contrib/openai.py +646 -0
  31. proxilion/core.py +3058 -0
  32. proxilion/decorators.py +966 -0
  33. proxilion/engines/__init__.py +287 -0
  34. proxilion/engines/base.py +266 -0
  35. proxilion/engines/casbin_engine.py +412 -0
  36. proxilion/engines/opa_engine.py +493 -0
  37. proxilion/engines/simple.py +437 -0
  38. proxilion/exceptions.py +887 -0
  39. proxilion/guards/__init__.py +54 -0
  40. proxilion/guards/input_guard.py +522 -0
  41. proxilion/guards/output_guard.py +634 -0
  42. proxilion/observability/__init__.py +198 -0
  43. proxilion/observability/cost_tracker.py +866 -0
  44. proxilion/observability/hooks.py +683 -0
  45. proxilion/observability/metrics.py +798 -0
  46. proxilion/observability/session_cost_tracker.py +1063 -0
  47. proxilion/policies/__init__.py +67 -0
  48. proxilion/policies/base.py +304 -0
  49. proxilion/policies/builtin.py +486 -0
  50. proxilion/policies/registry.py +376 -0
  51. proxilion/providers/__init__.py +201 -0
  52. proxilion/providers/adapter.py +468 -0
  53. proxilion/providers/anthropic_adapter.py +330 -0
  54. proxilion/providers/gemini_adapter.py +391 -0
  55. proxilion/providers/openai_adapter.py +294 -0
  56. proxilion/py.typed +0 -0
  57. proxilion/resilience/__init__.py +81 -0
  58. proxilion/resilience/degradation.py +615 -0
  59. proxilion/resilience/fallback.py +555 -0
  60. proxilion/resilience/retry.py +554 -0
  61. proxilion/scheduling/__init__.py +57 -0
  62. proxilion/scheduling/priority_queue.py +419 -0
  63. proxilion/scheduling/scheduler.py +459 -0
  64. proxilion/security/__init__.py +244 -0
  65. proxilion/security/agent_trust.py +968 -0
  66. proxilion/security/behavioral_drift.py +794 -0
  67. proxilion/security/cascade_protection.py +869 -0
  68. proxilion/security/circuit_breaker.py +428 -0
  69. proxilion/security/cost_limiter.py +690 -0
  70. proxilion/security/idor_protection.py +460 -0
  71. proxilion/security/intent_capsule.py +849 -0
  72. proxilion/security/intent_validator.py +495 -0
  73. proxilion/security/memory_integrity.py +767 -0
  74. proxilion/security/rate_limiter.py +509 -0
  75. proxilion/security/scope_enforcer.py +680 -0
  76. proxilion/security/sequence_validator.py +636 -0
  77. proxilion/security/trust_boundaries.py +784 -0
  78. proxilion/streaming/__init__.py +70 -0
  79. proxilion/streaming/detector.py +761 -0
  80. proxilion/streaming/transformer.py +674 -0
  81. proxilion/timeouts/__init__.py +55 -0
  82. proxilion/timeouts/decorators.py +477 -0
  83. proxilion/timeouts/manager.py +545 -0
  84. proxilion/tools/__init__.py +69 -0
  85. proxilion/tools/decorators.py +493 -0
  86. proxilion/tools/registry.py +732 -0
  87. proxilion/types.py +339 -0
  88. proxilion/validation/__init__.py +93 -0
  89. proxilion/validation/pydantic_schema.py +351 -0
  90. proxilion/validation/schema.py +651 -0
  91. proxilion-0.0.1.dist-info/METADATA +872 -0
  92. proxilion-0.0.1.dist-info/RECORD +94 -0
  93. proxilion-0.0.1.dist-info/WHEEL +4 -0
  94. 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
+ ]