- Rate limiting: 1.5s tra azioni dello stesso client WS - Cache LLM: SHA256 key, TTL 5 min, max 100 entry - Cache riduce token spesi su azioni identiche ripetute
This commit is contained in:
1 parent
893823fa1d
commit
9b2fd0ee92
2 files changed
+56
-1
No files matched your search
+35
-1
@@ -1,14 +1,43 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import contextvars
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
|
||||
from src.engine.providers import LLMProvider, create_provider
|
||||
|
||||
|
||||
_provider_var: contextvars.ContextVar[LLMProvider | None] = contextvars.ContextVar("provider", default=None)
|
||||
|
||||
# Cache risposte LLM: {cache_key: (timestamp, response)}
|
||||
_cache: dict[str, tuple[float, str]] = {}
|
||||
_CACHE_TTL = 300 # 5 minuti
|
||||
|
||||
|
||||
def _cache_key(prompt: str, system: str) -> str:
|
||||
return hashlib.sha256(f"{system}||{prompt}".encode()).hexdigest()
|
||||
|
||||
|
||||
def _cache_get(prompt: str, system: str) -> str | None:
|
||||
key = _cache_key(prompt, system)
|
||||
entry = _cache.get(key)
|
||||
if entry and time.monotonic() - entry[0] < _CACHE_TTL:
|
||||
return entry[1]
|
||||
if entry:
|
||||
del _cache[key]
|
||||
return None
|
||||
|
||||
|
||||
def _cache_set(prompt: str, system: str, response: str) -> None:
|
||||
key = _cache_key(prompt, system)
|
||||
_cache[key] = (time.monotonic(), response)
|
||||
# Mantieni cache sotto 100 entry
|
||||
if len(_cache) > 100:
|
||||
oldest = min(_cache.keys(), key=lambda k: _cache[k][0])
|
||||
del _cache[oldest]
|
||||
|
||||
|
||||
def get_provider() -> LLMProvider:
|
||||
provider = _provider_var.get()
|
||||
@@ -24,7 +53,12 @@ def set_provider(provider: LLMProvider) -> None:
|
||||
|
||||
|
||||
def ask(prompt: str, system: str = "") -> str:
|
||||
return get_provider().ask(prompt, system=system)
|
||||
cached = _cache_get(prompt, system)
|
||||
if cached is not None:
|
||||
return cached
|
||||
response = get_provider().ask(prompt, system=system)
|
||||
_cache_set(prompt, system, response)
|
||||
return response
|
||||
|
||||
|
||||
def _try_repair_json(text: str) -> str | None:
|
||||
|
||||
@@ -4,6 +4,7 @@ import json
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
@@ -140,6 +141,19 @@ def add_character(
|
||||
|
||||
_executor = ThreadPoolExecutor(max_workers=4)
|
||||
|
||||
# Rate limiting: almeno 1.5s tra azioni dello stesso client
|
||||
_last_action: dict[str, float] = {}
|
||||
_RATE_LIMIT = 1.5
|
||||
|
||||
|
||||
def _check_rate(client_id: str) -> str | None:
|
||||
now = time.monotonic()
|
||||
last = _last_action.get(client_id, 0)
|
||||
if now - last < _RATE_LIMIT:
|
||||
return f"Troppo veloce! Aspetta {_RATE_LIMIT - (now - last):.1f}s"
|
||||
_last_action[client_id] = now
|
||||
return None
|
||||
|
||||
|
||||
@app.websocket("/ws/{campaign_id}")
|
||||
async def game_ws(ws: WebSocket, campaign_id: int):
|
||||
@@ -171,6 +185,13 @@ async def game_ws(ws: WebSocket, campaign_id: int):
|
||||
if not player_action:
|
||||
continue
|
||||
|
||||
# Rate limiting
|
||||
if ws.client:
|
||||
err = _check_rate(str(ws.client))
|
||||
if err:
|
||||
await ws.send_json({"type": "error", "message": err})
|
||||
continue
|
||||
|
||||
try:
|
||||
# dm_cycle è bloccante (chiamata HTTP al LLM) —
|
||||
# la eseguiamo in un thread worker per non bloccare
|
||||
|
||||
Reference in new issue
Block a user