5a558eb09e
TypeScript SDK Compatibility V1.x E2E Tests / Select Node version matrix (push) Has been cancelled
TypeScript SDK Compatibility V1.x E2E Tests / TypeScript SDK Compatibility V1.x E2E Tests Node ${{matrix.node_version}} (push) Has been cancelled
TypeScript SDK E2E Tests / TypeScript SDK E2E Tests Node ${{matrix.node_version}} (push) Has been cancelled
Opik Optimizer - E2E Tests / build-opik (push) Has been cancelled
TypeScript SDK Compatibility V1.x E2E Tests / build-opik (push) Has been cancelled
Python SDK E2E Tests / Select Python version matrix (push) Has been cancelled
Python SDK E2E Tests / Python SDK E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Python SDK E2E Tests / build-opik (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / Select Python version matrix (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / Python SDK Compatibility V1.x E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / build-opik (push) Has been cancelled
TypeScript SDK E2E Tests / Select Node version matrix (push) Has been cancelled
TypeScript SDK E2E Tests / build-opik (push) Has been cancelled
Opik Optimizer - E2E Tests / Opik Optimizer E2E Tests Python ${{matrix.python_version}} (push) Has been cancelled
Opik Optimizer - E2E Tests / Opik Optimizer Integration Smoke Tests (push) Has been cancelled
🐙 Code Quality / detect (push) Has been cancelled
🐙 Code Quality / lint (${{ matrix.leg.name }}) (push) Has been cancelled
🐙 Code Quality / summary (push) Has been cancelled
TypeScript SDK Library Integration Tests / Check Secrets (push) Has been cancelled
TypeScript SDK Library Integration Tests / opik-vercel (Vercel AI SDK / eve) (push) Has been cancelled
SDK Library Integration Tests Runner / Check Secrets (push) Has been cancelled
SDK Library Integration Tests Runner / Missed OpenAI API Key Warning (push) Has been cancelled
SDK Library Integration Tests Runner / Build (push) Has been cancelled
SDK Library Integration Tests Runner / openai_tests (push) Has been cancelled
SDK Library Integration Tests Runner / langchain_tests (push) Has been cancelled
SDK Library Integration Tests Runner / langchain_legacy_tests (push) Has been cancelled
SDK Library Integration Tests Runner / llama_index_tests (push) Has been cancelled
SDK Library Integration Tests Runner / anthropic_tests (push) Has been cancelled
SDK Library Integration Tests Runner / mistral_tests (push) Has been cancelled
SDK Library Integration Tests Runner / groq_tests (push) Has been cancelled
SDK Library Integration Tests Runner / aisuite_tests (push) Has been cancelled
SDK Library Integration Tests Runner / haystack_tests (push) Has been cancelled
SDK Library Integration Tests Runner / dspy_tests (push) Has been cancelled
SDK Library Integration Tests Runner / crewai_v0_tests (push) Has been cancelled
SDK Library Integration Tests Runner / crewai_v1_tests (push) Has been cancelled
SDK Library Integration Tests Runner / genai_tests (push) Has been cancelled
SDK Library Integration Tests Runner / adk_tests (push) Has been cancelled
SDK Library Integration Tests Runner / adk_legacy_1_3_0_tests (push) Has been cancelled
SDK Library Integration Tests Runner / evaluation_metrics_tests (push) Has been cancelled
SDK Library Integration Tests Runner / bedrock_tests (push) Has been cancelled
SDK Library Integration Tests Runner / litellm_tests (push) Has been cancelled
SDK Library Integration Tests Runner / harbor_tests (push) Has been cancelled
SDK Library Integration Tests Runner / Slack Notification (push) Has been cancelled
Lint Opik Helm Chart / render-equality (push) Has been cancelled
Opik Optimizer - Unit Tests / Opik Optimizer Unit Tests Python ${{matrix.python_version}} (push) Has been cancelled
Python BE E2E Tests / Python BE E2E (push) Has been cancelled
Python Backend Tests / run-python-backend-tests (push) Has been cancelled
Python SDK Unit Tests / Python SDK Unit Tests ${{matrix.python_version}} (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
SDK E2E Libraries Integration Tests / Check Secrets (push) Has been cancelled
SDK E2E Libraries Integration Tests / Missed OpenAI API Key Warning (push) Has been cancelled
SDK E2E Libraries Integration Tests / build-opik (push) Has been cancelled
SDK E2E Libraries Integration Tests / E2E Lib Integration Python ${{matrix.python_version}} (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-gemini) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-langchain) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-openai) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-otel) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-vercel) (push) Has been cancelled
TypeScript SDK Build & Publish / build-and-publish (push) Has been cancelled
TypeScript SDK Unit Tests / Test on Node ${{ matrix.node-version }} (push) Has been cancelled
Backend Tests / discover-tests (push) Has been cancelled
Backend Tests / ${{ matrix.name }} (push) Has been cancelled
Build and Publish SDK / build-and-publish (push) Has been cancelled
Build Opik Docker Images / set-version (push) Has been cancelled
Build Opik Docker Images / build-backend (push) Has been cancelled
Build Opik Docker Images / build-sandbox-executor-python (push) Has been cancelled
Build Opik Docker Images / build-python-backend (push) Has been cancelled
Build Opik Docker Images / build-frontend (push) Has been cancelled
Build Opik Docker Images / create-git-tag (push) Has been cancelled
ClickHouse Migration Cluster Check / validate-clickhouse-migrations (push) Has been cancelled
Docs - Publish / run (push) Has been cancelled
E2E Tests - Post Merge (v2) / 🧪 E2E v2 Tests (${{ github.event.inputs.tier || 't1' }}) (push) Has been cancelled
E2E Tests - Post Merge (v2) / 📢 Slack Notification (push) Has been cancelled
Frontend Unit Tests / Test on Node 20 (push) Has been cancelled
Guardrails E2E Tests / Select Python version matrix (push) Has been cancelled
Guardrails E2E Tests / Guardrails E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Guardrails E2E Tests / 📢 Slack Notification (push) Has been cancelled
Guardrails Backend Unit Tests / Guardrails Backend Unit Tests (push) Has been cancelled
Guardrails Backend Unit Tests / 📢 Slack Notification (push) Has been cancelled
Lint Opik Helm Chart / lint-helm-chart (Helm v3.21.0) (push) Has been cancelled
Lint Opik Helm Chart / lint-helm-chart (Helm v4.2.0) (push) Has been cancelled
Lint Opik Helm Chart / unittest-helm-chart (push) Has been cancelled
158 lines
6.3 KiB
Python
158 lines
6.3 KiB
Python
"""Regression tests for the ADK OpikTracer's per-invocation model-output cache.
|
|
|
|
A single ``OpikTracer`` is shared across every invocation of an instrumented
|
|
agent (the documented ``track_adk_agent_recursive`` pattern). The last model
|
|
output is cached per ADK ``invocation_id`` (``LastModelOutputCache``) so
|
|
concurrent invocations don't overwrite each other's output, and the cache is
|
|
size-bounded so it can't grow without bound when an invocation's
|
|
``after_agent_callback`` never runs (agent errors, early escalation,
|
|
cancellation).
|
|
|
|
The cache is unit-tested directly; ``after_agent_callback`` is exercised against
|
|
the modern ``OpikTracer`` (skipped on ADK < 1.3.0, which uses
|
|
``LegacyOpikTracer``). The autouse ``clear_context_storage`` fixture
|
|
(tests/conftest.py) resets context storage between tests.
|
|
"""
|
|
|
|
import types
|
|
|
|
from google.adk.models import LlmResponse
|
|
from google.genai import types as genai_types
|
|
from opik import context_storage
|
|
from opik.api_objects.trace.trace_data import TraceData
|
|
from opik.integrations.adk import OpikTracer
|
|
from opik.integrations.adk.output_cache import LastModelOutputCache
|
|
|
|
from . import helpers
|
|
|
|
|
|
def _callback_context(invocation_id: str) -> types.SimpleNamespace:
|
|
# after_agent_callback only reads ``invocation_id`` off the callback context.
|
|
return types.SimpleNamespace(invocation_id=invocation_id)
|
|
|
|
|
|
def test_last_model_output_cache__isolates_by_invocation_id():
|
|
cache = LastModelOutputCache()
|
|
|
|
cache.set("inv-A", {"text": "ALPHA"})
|
|
cache.set("inv-B", {"text": "BRAVO"}) # would clobber a single shared slot
|
|
|
|
assert cache.get("inv-A") == {"text": "ALPHA"}
|
|
assert cache.get("inv-B") == {"text": "BRAVO"}
|
|
assert cache.get("missing") is None
|
|
|
|
|
|
def test_last_model_output_cache__evicts_oldest_when_full():
|
|
cache = LastModelOutputCache(max_size=2)
|
|
|
|
cache.set("inv-1", {"n": 1})
|
|
cache.set("inv-2", {"n": 2})
|
|
cache.set("inv-3", {"n": 3}) # over capacity -> evicts the oldest (inv-1)
|
|
|
|
assert cache.get("inv-1") is None # bounded: abandoned entry evicted
|
|
assert cache.get("inv-2") == {"n": 2}
|
|
assert cache.get("inv-3") == {"n": 3}
|
|
|
|
|
|
def test_last_model_output_cache__discard_removes_entry():
|
|
cache = LastModelOutputCache()
|
|
cache.set("inv", {"text": "OLD"})
|
|
|
|
cache.discard("inv")
|
|
|
|
assert cache.get("inv") is None
|
|
cache.discard("inv") # idempotent
|
|
cache.discard("missing") # missing key is a no-op
|
|
|
|
|
|
@helpers.pytest_skip_for_adk_older_than_1_3_0
|
|
def test_after_agent_callback__stamps_only_its_own_invocations_output():
|
|
tracer = OpikTracer(project_name="adk-test")
|
|
|
|
# Two concurrent invocations recorded their model output on the shared tracer
|
|
# (as after_model_callback does), interleaved.
|
|
tracer._last_model_output.set("invocation-A", {"text": "ALPHA"})
|
|
tracer._last_model_output.set("invocation-B", {"text": "BRAVO"})
|
|
|
|
# Invocation A's agent finishes — no span on the stack, so the trace path.
|
|
trace_a = TraceData(name="agent-A")
|
|
context_storage.set_trace_data(trace_a)
|
|
tracer.after_agent_callback(_callback_context("invocation-A"))
|
|
assert trace_a.output == {"text": "ALPHA"} # A's output, never B's
|
|
|
|
# Invocation B's agent finishes and still gets its own output.
|
|
trace_b = TraceData(name="agent-B")
|
|
context_storage.set_trace_data(trace_b)
|
|
tracer.after_agent_callback(_callback_context("invocation-B"))
|
|
assert trace_b.output == {"text": "BRAVO"}
|
|
|
|
|
|
@helpers.pytest_skip_for_adk_older_than_1_3_0
|
|
def test_after_agent_callback__no_cached_output__stamps_none():
|
|
# No model output cached for this invocation (e.g. a failed conversion
|
|
# cleared it): after_agent_callback must stamp None, never a stale value.
|
|
tracer = OpikTracer(project_name="adk-test")
|
|
|
|
trace = TraceData(name="agent")
|
|
context_storage.set_trace_data(trace)
|
|
tracer.after_agent_callback(_callback_context("invocation-without-output"))
|
|
|
|
assert trace.output is None
|
|
|
|
|
|
def _model_response(text: str, *, partial: bool) -> LlmResponse:
|
|
return LlmResponse(
|
|
content=genai_types.Content(role="model", parts=[genai_types.Part(text=text)]),
|
|
partial=partial,
|
|
)
|
|
|
|
|
|
@helpers.pytest_skip_for_adk_older_than_1_3_0
|
|
def test_after_model_callback__recovers_output_when_span_detached():
|
|
# ContextCacheConfig (issue #5524) wraps the LLM call in its own OTel span,
|
|
# which forks the async context under SSE streaming — so the span pushed in
|
|
# before_model_callback is invisible here and top_span_data() is None. The
|
|
# actual model output must still be recovered and land on the trace output.
|
|
# Verified through the public callbacks only: after_model recovers, then
|
|
# after_agent stamps the recovered answer onto the trace.
|
|
tracer = OpikTracer(project_name="adk-test")
|
|
assert context_storage.top_span_data() is None # detached: no current span
|
|
|
|
tracer.after_model_callback(
|
|
_callback_context("inv-detached"),
|
|
_model_response("RECOVERED ANSWER", partial=False),
|
|
)
|
|
|
|
trace = TraceData(name="agent")
|
|
context_storage.set_trace_data(trace)
|
|
tracer.after_agent_callback(_callback_context("inv-detached"))
|
|
|
|
assert "RECOVERED ANSWER" in str(trace.output)
|
|
|
|
|
|
@helpers.pytest_skip_for_adk_older_than_1_3_0
|
|
def test_after_model_callback__detached_span__partial_chunk_discards():
|
|
# Partial streaming chunks must not cache — and they clear any prior value for
|
|
# the invocation (discard up front), so a stale earlier output can't leak onto
|
|
# the trace before the final response arrives. Driven entirely through the
|
|
# public callbacks: a recovered final response, then a partial chunk for the
|
|
# same invocation, must leave the trace output empty.
|
|
tracer = OpikTracer(project_name="adk-test")
|
|
assert context_storage.top_span_data() is None
|
|
|
|
# An earlier final response was recovered for this invocation...
|
|
tracer.after_model_callback(
|
|
_callback_context("inv-partial"),
|
|
_model_response("EARLIER ANSWER", partial=False),
|
|
)
|
|
# ...then a partial chunk arrives (still detached): it must discard, not stamp.
|
|
tracer.after_model_callback(
|
|
_callback_context("inv-partial"), _model_response("strea", partial=True)
|
|
)
|
|
|
|
trace = TraceData(name="agent")
|
|
context_storage.set_trace_data(trace)
|
|
tracer.after_agent_callback(_callback_context("inv-partial"))
|
|
|
|
assert trace.output is None # no stale "EARLIER ANSWER" leaked through
|