Que las conversaciones largas no se rompan ni gasten de más: Ventana de contexto por modelo (antes: budget estático 120k/200k para todos): - cost.resolve_context_window: lee context_length del catálogo OpenRouter/DeepSeek en Redis, con fallback a litellm. config.budget_for_window deriva el budget de la ventana real (window - max_output - reserve). build_context lo aplica por turno (param model_id) en vez del fijo de settings. - Self-heal del catálogo OpenRouter: el admin panel lo cachea con TTL 1h y solo lo repuebla al abrir su ventana de IA → en runtime caducaba y se perdían ventana y precio. Ahora cost._get_catalog lo refresca solo (fetch público, mismo shape, cooldown 5min, TTL 24h). Arregla también el coste (caía al fijo). Recuperación ante overflow: - adapters.base.ContextOverflowError; openai_adapter traduce el error de context-length del proveedor (init e iteración del stream). - base.py: retry proactivo que recompacta hasta caber en la ventana ANTES de llamar al LLM; si ni así cabe → error accionable (no rompe la sesión). - engine.py: mensaje user-facing claro (modelo + ventana). Tests: ventana/budget, self-heal (mockeado), overflow, y sesión REAL de Redis. 106 verdes. evals/: harness para evaluar al agente acai-code (driver + README + resultados). Comparativa kimi vs deepseek vs glm (deepseek-v4-pro high = mejor calidad/precio). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
111 lines
3.6 KiB
Python
111 lines
3.6 KiB
Python
"""Test de integración contra sesiones REALES de Redis (db 1).
|
|
|
|
Valida el budget por-ventana y la compactación sobre las conversaciones reales
|
|
del agentic (las que los usuarios mantienen abiertas), no sobre fixtures
|
|
sintéticos. Es OPT-IN: se salta si no hay Redis disponible o no hay sesiones,
|
|
para no acoplar la suite a datos de cliente ni romper en CI.
|
|
|
|
Ejecutar contra el Redis real:
|
|
docker run --rm --network acai-net \\
|
|
-v "$PWD/agenticSystem/src:/app/src" -v "$PWD/agenticSystem/tests:/app/tests" \\
|
|
-e AGENTIC_REDIS_HOST=redis -w /app acai-vscode-plugin-agentic \\
|
|
sh -lc "pip install -q pytest pytest-asyncio; python -m pytest tests/test_context_real_session.py -q"
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import enum
|
|
import json
|
|
import sys
|
|
import types
|
|
|
|
import pytest
|
|
|
|
if not hasattr(enum, "StrEnum"):
|
|
class _CompatStrEnum(str, enum.Enum):
|
|
pass
|
|
|
|
enum.StrEnum = _CompatStrEnum
|
|
|
|
for _name, _attr in (("anthropic", "AsyncAnthropic"), ("openai", "AsyncOpenAI")):
|
|
if _name not in sys.modules:
|
|
_stub = types.ModuleType(_name)
|
|
setattr(_stub, _attr, type("_Stub", (), {}))
|
|
sys.modules[_name] = _stub
|
|
|
|
from src.config import settings
|
|
from src.context.compactor import estimate_tokens
|
|
from src.context.engine import ContextEngine
|
|
from src.models.agent import AgentProfile
|
|
from src.models.session import SessionState
|
|
|
|
|
|
def _load_largest_real_session():
|
|
"""Mayor sesión real de Redis db 1, o None si no hay acceso/sesiones."""
|
|
try:
|
|
import redis
|
|
|
|
r = redis.Redis(
|
|
host=settings.redis_host,
|
|
port=settings.redis_port,
|
|
db=1,
|
|
password=settings.redis_password or None,
|
|
decode_responses=True,
|
|
socket_connect_timeout=2,
|
|
)
|
|
keys = [
|
|
k for k in r.scan_iter("agentic:session:*")
|
|
if not k.endswith((":events", ":artifacts"))
|
|
]
|
|
if not keys:
|
|
return None
|
|
biggest = max(keys, key=lambda k: r.strlen(k))
|
|
raw = r.get(biggest)
|
|
return json.loads(raw) if raw else None
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def test_real_session_compacts_under_model_window(monkeypatch):
|
|
data = _load_largest_real_session()
|
|
if not data or not data.get("recent_messages"):
|
|
pytest.skip("sin Redis/sesiones reales disponibles")
|
|
|
|
rm = data["recent_messages"]
|
|
raw_tokens = sum(estimate_tokens(json.dumps(m)) for m in rm)
|
|
|
|
from src.orchestrator import cost
|
|
|
|
async def _fake_window(model_id):
|
|
return 32_000
|
|
|
|
monkeypatch.setattr(cost, "resolve_context_window", _fake_window)
|
|
|
|
session = SessionState(
|
|
immutable_rules=data.get("immutable_rules") or ["No romper"],
|
|
project_profile=data.get("project_profile") or {},
|
|
task_history=data.get("task_history") or [],
|
|
recent_messages=rm,
|
|
)
|
|
session.begin_task("Sigamos con lo anterior")
|
|
agent = AgentProfile(
|
|
role="acai",
|
|
name="Acai",
|
|
system_prompt="Haz el trabajo.",
|
|
context_sections=["immutable_rules", "task_state"],
|
|
)
|
|
|
|
pkg = asyncio.run(
|
|
ContextEngine().build_context(
|
|
session=session, agent=agent, conversation=rm, model_id="openrouter/x"
|
|
)
|
|
)
|
|
|
|
# Budget derivado de la ventana REAL del modelo (32k), no del fijo de 120k/200k.
|
|
assert pkg.budget_tokens == settings.budget_for_window(32_000)
|
|
# La sesión real se compactó de verdad (no se reenvía cruda).
|
|
assert pkg.total_token_estimate < raw_tokens
|
|
# Y el resultado cabe en el budget del modelo → no habría overflow.
|
|
assert pkg.total_token_estimate <= pkg.budget_tokens
|