feat(memory): add incremental transcript compaction
This commit is contained in:
1 parent
cbe8f4f0c6
commit
41ae386a3a
65 files changed
+9971
-815
No files matched your search
@@ -0,0 +1,173 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests for the M0 session token metering pipeline (pure recording)."""
|
||||
|
||||
from unittest.mock import Mock
|
||||
from uuid import uuid4
|
||||
|
||||
from langchain_core.messages import AIMessage
|
||||
|
||||
from artemis.services.token_meter import (
|
||||
SessionTokenMeter,
|
||||
extract_usage,
|
||||
get_meter,
|
||||
record_llm_usage,
|
||||
)
|
||||
|
||||
|
||||
def _msg_with_usage(prompt=1000, completion=50, cached=0):
|
||||
usage = {
|
||||
"input_tokens": prompt,
|
||||
"output_tokens": completion,
|
||||
"total_tokens": prompt + completion,
|
||||
}
|
||||
if cached:
|
||||
usage["input_token_details"] = {"cache_read": cached}
|
||||
return AIMessage(content="ok", usage_metadata=usage)
|
||||
|
||||
|
||||
def test_extract_usage_from_usage_metadata():
|
||||
usage = extract_usage(_msg_with_usage(prompt=1200, completion=80, cached=900))
|
||||
assert usage == {
|
||||
"prompt_tokens": 1200,
|
||||
"completion_tokens": 80,
|
||||
"total_tokens": 1280,
|
||||
"cached_tokens": 900,
|
||||
}
|
||||
|
||||
|
||||
def test_extract_usage_from_response_metadata_fallback():
|
||||
msg = AIMessage(content="ok")
|
||||
msg.response_metadata = {
|
||||
"usage_metadata": {
|
||||
"prompt_token_count": 500,
|
||||
"candidates_token_count": 25,
|
||||
"total_token_count": 525,
|
||||
"cached_content_token_count": 400,
|
||||
}
|
||||
}
|
||||
usage = extract_usage(msg)
|
||||
assert usage == {
|
||||
"prompt_tokens": 500,
|
||||
"completion_tokens": 25,
|
||||
"total_tokens": 525,
|
||||
"cached_tokens": 400,
|
||||
}
|
||||
|
||||
|
||||
def test_extract_usage_absent_returns_none():
|
||||
assert extract_usage(AIMessage(content="no usage")) is None
|
||||
assert extract_usage(object()) is None
|
||||
|
||||
|
||||
def test_meter_accumulates_session_totals():
|
||||
meter = SessionTokenMeter("s1")
|
||||
meter.record({"prompt_tokens": 100, "completion_tokens": 10, "cached_tokens": 0})
|
||||
snapshot = meter.record(
|
||||
{"prompt_tokens": 300, "completion_tokens": 20, "cached_tokens": 250}
|
||||
)
|
||||
assert snapshot == {
|
||||
"session_llm_calls": 2,
|
||||
"session_prompt_tokens": 400,
|
||||
"session_completion_tokens": 30,
|
||||
"session_cached_tokens": 250,
|
||||
"session_cache_hit_calls": 1,
|
||||
}
|
||||
assert meter.last_prompt_tokens == 300
|
||||
|
||||
|
||||
def test_record_llm_usage_records_trace_with_context_base():
|
||||
engine = Mock()
|
||||
engine.current_session_id = uuid4()
|
||||
engine.current_step_id = uuid4()
|
||||
|
||||
payload = record_llm_usage(
|
||||
engine, _msg_with_usage(prompt=2000, completion=100, cached=1500), source="google:test"
|
||||
)
|
||||
|
||||
assert payload is not None
|
||||
assert payload["context_base_tokens"] == 2000
|
||||
assert payload["cached_tokens"] == 1500
|
||||
assert payload["source"] == "google:test"
|
||||
|
||||
engine.record_trace.assert_called_once()
|
||||
kwargs = engine.record_trace.call_args.kwargs
|
||||
assert kwargs["type"] == "llm_call"
|
||||
assert kwargs["name"] == "llm_usage"
|
||||
assert kwargs["step_id"] == engine.current_step_id
|
||||
assert kwargs["payload"]["prompt_tokens"] == 2000
|
||||
|
||||
|
||||
def test_record_llm_usage_accumulates_across_calls_same_session():
|
||||
engine = Mock()
|
||||
engine.current_session_id = uuid4()
|
||||
engine.current_step_id = None
|
||||
|
||||
record_llm_usage(engine, _msg_with_usage(prompt=100, completion=5))
|
||||
payload = record_llm_usage(engine, _msg_with_usage(prompt=250, completion=10, cached=90))
|
||||
|
||||
assert payload["session_llm_calls"] == 2
|
||||
assert payload["session_prompt_tokens"] == 350
|
||||
assert payload["session_cached_tokens"] == 90
|
||||
assert payload["session_cache_hit_calls"] == 1
|
||||
|
||||
meter = get_meter(engine.current_session_id)
|
||||
assert meter.last_prompt_tokens == 250
|
||||
|
||||
|
||||
def test_record_llm_usage_never_raises_and_skips_gracefully():
|
||||
# No engine
|
||||
assert record_llm_usage(None, _msg_with_usage()) is None
|
||||
|
||||
# Engine without a session
|
||||
engine = Mock()
|
||||
engine.current_session_id = None
|
||||
assert record_llm_usage(engine, _msg_with_usage()) is None
|
||||
engine.record_trace.assert_not_called()
|
||||
|
||||
# Response without usage
|
||||
engine2 = Mock()
|
||||
engine2.current_session_id = uuid4()
|
||||
assert record_llm_usage(engine2, AIMessage(content="nope")) is None
|
||||
engine2.record_trace.assert_not_called()
|
||||
|
||||
# record_trace blowing up must not propagate
|
||||
engine3 = Mock()
|
||||
engine3.current_session_id = uuid4()
|
||||
engine3.record_trace.side_effect = RuntimeError("boom")
|
||||
assert record_llm_usage(engine3, _msg_with_usage()) is None
|
||||
|
||||
|
||||
def test_record_llm_usage_lens_calls_do_not_touch_last_prompt_tokens():
|
||||
"""Background lens calls accumulate totals but never overwrite the live
|
||||
context base (last_prompt_tokens) consumed by the compaction thresholds."""
|
||||
engine = Mock()
|
||||
engine.current_session_id = uuid4()
|
||||
engine.current_step_id = None
|
||||
|
||||
record_llm_usage(engine, _msg_with_usage(prompt=15000))
|
||||
meter = get_meter(engine.current_session_id)
|
||||
assert meter.last_prompt_tokens == 15000
|
||||
|
||||
payload = record_llm_usage(
|
||||
engine,
|
||||
_msg_with_usage(prompt=300, completion=40),
|
||||
source="lens:visual_transition:test-model",
|
||||
update_last_prompt=False,
|
||||
)
|
||||
assert payload["source"] == "lens:visual_transition:test-model"
|
||||
assert meter.last_prompt_tokens == 15000 # untouched by the lens call
|
||||
assert meter.llm_calls == 2
|
||||
assert meter.prompt_tokens == 15300
|
||||
Reference in new issue
Block a user