|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
| 3 | +from collections import OrderedDict |
3 | 4 | from collections.abc import Callable |
4 | 5 | from typing import Any |
5 | 6 |
|
@@ -47,19 +48,53 @@ def validate(self, output: Any, **context: Any) -> FailureResult | None: # noqa |
47 | 48 |
|
48 | 49 | class LlmJudge: |
49 | 50 | """EXPENSIVE: Each call invokes the provided LLM function. |
50 | | - The caller supplies the actual LLM call -- gateframe does not import any LLM SDK.""" |
| 51 | + The caller supplies the actual LLM call -- gateframe does not import any LLM SDK. |
| 52 | +
|
| 53 | + Set ``cache=True`` to memoize results keyed on the rendered prompt. This avoids |
| 54 | + redundant LLM calls when the same output+context is validated more than once within |
| 55 | + the lifetime of the judge instance (e.g. retry loops, multi-step pipelines). |
| 56 | + ``max_cache_size`` caps the in-memory LRU store; the oldest entry is evicted when |
| 57 | + the limit is reached. |
| 58 | + """ |
51 | 59 |
|
52 | 60 | def __init__( |
53 | 61 | self, |
54 | 62 | prompt_template: str, |
55 | 63 | llm_call: Callable[[str], str], |
56 | 64 | passing_response: str = "PASS", |
| 65 | + *, |
| 66 | + cache: bool = False, |
| 67 | + max_cache_size: int = 256, |
57 | 68 | ) -> None: |
58 | 69 | self._prompt_template = prompt_template |
59 | 70 | self._llm_call = llm_call |
60 | 71 | self._passing_response = passing_response |
| 72 | + self._cache: OrderedDict[str, bool] | None = OrderedDict() if cache else None |
| 73 | + self._max_cache_size = max_cache_size |
61 | 74 |
|
62 | 75 | def __call__(self, output: Any, **context: Any) -> bool: # noqa: ANN401 |
63 | 76 | prompt = self._prompt_template.format(output=output, **context) |
64 | | - response = self._llm_call(prompt) |
65 | | - return self._passing_response.lower() in response.strip().lower() |
| 77 | + |
| 78 | + if self._cache is not None: |
| 79 | + if prompt in self._cache: |
| 80 | + self._cache.move_to_end(prompt) |
| 81 | + return self._cache[prompt] |
| 82 | + |
| 83 | + result = self._passing_response.lower() in self._llm_call(prompt).strip().lower() |
| 84 | + |
| 85 | + if self._cache is not None: |
| 86 | + if len(self._cache) >= self._max_cache_size: |
| 87 | + self._cache.popitem(last=False) |
| 88 | + self._cache[prompt] = result |
| 89 | + |
| 90 | + return result |
| 91 | + |
| 92 | + def cache_clear(self) -> None: |
| 93 | + """Evict all cached prompt results.""" |
| 94 | + if self._cache is not None: |
| 95 | + self._cache.clear() |
| 96 | + |
| 97 | + @property |
| 98 | + def cache_size(self) -> int: |
| 99 | + """Number of cached entries (0 when caching is disabled).""" |
| 100 | + return len(self._cache) if self._cache is not None else 0 |
0 commit comments