846 lines
29 KiB
Python
846 lines
29 KiB
Python
# pyright: reportPrivateUsage=false, reportUnknownVariableType=false
|
|
"""Integration tests for Prometheus token metrics tracking.
|
|
|
|
These tests verify that deriver and dialectic token metrics are correctly
|
|
emitted with accurate token counts when processing messages and dialectic queries.
|
|
|
|
The approach uses delta-based verification by directly accessing Prometheus counters:
|
|
1. Capture counter values before test execution
|
|
2. Run the code under test (with mocked LLM)
|
|
3. Verify deltas match expected values
|
|
"""
|
|
|
|
from collections.abc import Iterator
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
import pytest
|
|
from nanoid import generate as generate_nanoid
|
|
from prometheus_client import Counter
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from src import crud, models, schemas
|
|
from src.llm import HonchoLLMCallResponse
|
|
from src.models import Peer, Workspace
|
|
from src.schemas import (
|
|
ResolvedConfiguration,
|
|
ResolvedDreamConfiguration,
|
|
ResolvedPeerCardConfiguration,
|
|
ResolvedReasoningConfiguration,
|
|
ResolvedSummaryConfiguration,
|
|
)
|
|
from src.telemetry.prometheus.metrics import (
|
|
deriver_tokens_processed_counter,
|
|
dialectic_tokens_processed_counter,
|
|
)
|
|
from src.utils.representation import ExplicitObservationBase, PromptRepresentation
|
|
from src.utils.summarizer import (
|
|
SummaryType,
|
|
_create_and_save_summary,
|
|
estimate_short_summary_prompt_tokens,
|
|
)
|
|
|
|
# =============================================================================
|
|
# Fixtures
|
|
# =============================================================================
|
|
|
|
|
|
class PrometheusMetricChecker:
|
|
"""Utility class to capture and verify Prometheus counter deltas."""
|
|
|
|
def _get_counter_value(self, counter: Counter, labels: dict[str, str]) -> float:
|
|
"""Get current value of a counter with specific labels.
|
|
|
|
Note: For prometheus_client counters, we access the internal _value
|
|
of the labeled metric. This is implementation-specific but works for testing.
|
|
"""
|
|
try:
|
|
labeled_counter = counter.labels(**labels)
|
|
return labeled_counter._value.get()
|
|
except Exception:
|
|
return 0.0
|
|
|
|
def capture(self, counter: Counter, labels: dict[str, str]) -> float:
|
|
"""Capture current value of a counter with specific labels."""
|
|
return self._get_counter_value(counter, labels)
|
|
|
|
def get_delta(
|
|
self, counter: Counter, labels: dict[str, str], before: float
|
|
) -> float:
|
|
"""Get the delta between a before value and current."""
|
|
return self.capture(counter, labels) - before
|
|
|
|
def assert_delta(
|
|
self,
|
|
counter: Counter,
|
|
labels: dict[str, str],
|
|
before: float,
|
|
expected: int | float,
|
|
message: str = "",
|
|
) -> None:
|
|
"""Assert that the delta matches expected value."""
|
|
delta = self.get_delta(counter, labels, before)
|
|
assert (
|
|
delta == expected
|
|
), f"{message}: expected delta {expected}, got {delta}. Labels: {labels}"
|
|
|
|
|
|
@pytest.fixture
|
|
def prometheus_test_setup(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> Iterator[PrometheusMetricChecker]:
|
|
"""Set up Prometheus metrics for testing.
|
|
|
|
Yields:
|
|
A PrometheusMetricChecker for verifying metrics
|
|
"""
|
|
# Enable METRICS in settings and set namespace for test assertions
|
|
monkeypatch.setattr("src.config.settings.METRICS.ENABLED", True)
|
|
monkeypatch.setattr("src.config.settings.METRICS.NAMESPACE", "test")
|
|
|
|
checker = PrometheusMetricChecker()
|
|
|
|
yield checker
|
|
|
|
|
|
@pytest.fixture
|
|
def metric_checker(
|
|
prometheus_test_setup: PrometheusMetricChecker,
|
|
) -> PrometheusMetricChecker:
|
|
"""Fixture providing a metric checker instance."""
|
|
return prometheus_test_setup
|
|
|
|
|
|
# =============================================================================
|
|
# Test Data Helpers
|
|
# =============================================================================
|
|
|
|
|
|
async def create_test_session_with_peer(
|
|
db_session: AsyncSession,
|
|
workspace: Workspace,
|
|
peer: Peer,
|
|
) -> models.Session:
|
|
"""Create a session with a peer configured for observation."""
|
|
session = (
|
|
await crud.get_or_create_session(
|
|
db_session,
|
|
schemas.SessionCreate(
|
|
name=str(generate_nanoid()),
|
|
peers={peer.name: schemas.SessionPeerConfig(observe_me=True)},
|
|
),
|
|
workspace.name,
|
|
)
|
|
).resource
|
|
await db_session.commit()
|
|
return session
|
|
|
|
|
|
async def create_test_messages(
|
|
db_session: AsyncSession,
|
|
workspace_name: str,
|
|
session_name: str,
|
|
peer_name: str,
|
|
count: int = 1,
|
|
content_prefix: str = "Test message",
|
|
) -> list[models.Message]:
|
|
"""Create test messages in the database."""
|
|
messages: list[models.Message] = []
|
|
for i in range(count):
|
|
message = models.Message(
|
|
workspace_name=workspace_name,
|
|
session_name=session_name,
|
|
peer_name=peer_name,
|
|
content=f"{content_prefix} {i}",
|
|
public_id=generate_nanoid(),
|
|
seq_in_session=i + 1,
|
|
token_count=10,
|
|
)
|
|
db_session.add(message)
|
|
messages.append(message)
|
|
|
|
await db_session.commit()
|
|
# Refresh to get IDs
|
|
for msg in messages:
|
|
await db_session.refresh(msg)
|
|
return messages
|
|
|
|
|
|
def create_test_configuration() -> ResolvedConfiguration:
|
|
"""Create a test configuration to avoid DB lookups in tests."""
|
|
return ResolvedConfiguration(
|
|
reasoning=ResolvedReasoningConfiguration(enabled=True),
|
|
peer_card=ResolvedPeerCardConfiguration(use=False, create=False),
|
|
summary=ResolvedSummaryConfiguration(
|
|
enabled=True, messages_per_short_summary=20, messages_per_long_summary=60
|
|
),
|
|
dream=ResolvedDreamConfiguration(enabled=False),
|
|
)
|
|
|
|
|
|
def create_mock_deriver_response(
|
|
output_tokens: int = 42,
|
|
) -> HonchoLLMCallResponse[PromptRepresentation]:
|
|
"""Create a mock LLM response for the deriver."""
|
|
return HonchoLLMCallResponse(
|
|
content=PromptRepresentation(
|
|
explicit=[ExplicitObservationBase(content="Test observation from deriver")],
|
|
),
|
|
input_tokens=100,
|
|
output_tokens=output_tokens,
|
|
finish_reasons=["end_turn"],
|
|
)
|
|
|
|
|
|
def create_mock_dialectic_response(
|
|
input_tokens: int = 150, output_tokens: int = 75
|
|
) -> HonchoLLMCallResponse[str]:
|
|
"""Create a mock LLM response for the dialectic."""
|
|
return HonchoLLMCallResponse(
|
|
content="This is a test dialectic response about the peer.",
|
|
input_tokens=input_tokens,
|
|
output_tokens=output_tokens,
|
|
cache_read_input_tokens=0,
|
|
cache_creation_input_tokens=0,
|
|
finish_reasons=["end_turn"],
|
|
tool_calls_made=[],
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Deriver Ingestion Metrics Tests
|
|
# =============================================================================
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
class TestDeriverIngestionMetrics:
|
|
"""Test token metrics for deriver INGESTION task type."""
|
|
|
|
async def test_ingestion_tracks_output_tokens(
|
|
self,
|
|
db_session: AsyncSession,
|
|
sample_data: tuple[Workspace, Peer],
|
|
prometheus_test_setup: PrometheusMetricChecker,
|
|
):
|
|
"""Verify OUTPUT_TOTAL tokens match response.output_tokens from LLM."""
|
|
from src.deriver.deriver import process_representation_tasks_batch
|
|
|
|
metric_checker = prometheus_test_setup
|
|
workspace, peer = sample_data
|
|
session = await create_test_session_with_peer(db_session, workspace, peer)
|
|
messages = await create_test_messages(
|
|
db_session, workspace.name, session.name, peer.name, count=1
|
|
)
|
|
|
|
expected_output_tokens = 42
|
|
mock_response = create_mock_deriver_response(
|
|
output_tokens=expected_output_tokens
|
|
)
|
|
|
|
# Capture metrics before
|
|
labels = {
|
|
"namespace": "test",
|
|
"task_type": "ingestion",
|
|
"token_type": "output",
|
|
"component": "output_total",
|
|
}
|
|
before = metric_checker.capture(deriver_tokens_processed_counter, labels)
|
|
|
|
# Mock the LLM call and save_representation (we're testing metrics, not DB writes)
|
|
with (
|
|
patch(
|
|
"src.deriver.deriver.honcho_llm_call",
|
|
new=AsyncMock(return_value=mock_response),
|
|
),
|
|
patch(
|
|
"src.crud.representation.RepresentationManager.save_representation",
|
|
new=AsyncMock(),
|
|
),
|
|
):
|
|
await process_representation_tasks_batch(
|
|
messages=messages,
|
|
message_level_configuration=create_test_configuration(),
|
|
observers=[peer.name],
|
|
observed=peer.name,
|
|
queue_item_message_ids=[m.id for m in messages],
|
|
)
|
|
|
|
# Verify output tokens metric
|
|
metric_checker.assert_delta(
|
|
deriver_tokens_processed_counter,
|
|
labels,
|
|
before,
|
|
expected_output_tokens,
|
|
"Ingestion output tokens",
|
|
)
|
|
|
|
async def test_ingestion_tracks_prompt_input_tokens(
|
|
self,
|
|
db_session: AsyncSession,
|
|
sample_data: tuple[Workspace, Peer],
|
|
prometheus_test_setup: PrometheusMetricChecker,
|
|
):
|
|
"""Verify PROMPT component is tracked for ingestion input."""
|
|
from src.deriver.deriver import process_representation_tasks_batch
|
|
from src.deriver.prompts import estimate_minimal_deriver_prompt_tokens
|
|
|
|
metric_checker = prometheus_test_setup
|
|
workspace, peer = sample_data
|
|
session = await create_test_session_with_peer(db_session, workspace, peer)
|
|
messages = await create_test_messages(
|
|
db_session, workspace.name, session.name, peer.name, count=1
|
|
)
|
|
|
|
mock_response = create_mock_deriver_response()
|
|
|
|
# Get expected prompt tokens
|
|
expected_prompt_tokens = estimate_minimal_deriver_prompt_tokens()
|
|
|
|
labels = {
|
|
"namespace": "test",
|
|
"task_type": "ingestion",
|
|
"token_type": "input",
|
|
"component": "prompt",
|
|
}
|
|
before = metric_checker.capture(deriver_tokens_processed_counter, labels)
|
|
|
|
with (
|
|
patch(
|
|
"src.deriver.deriver.honcho_llm_call",
|
|
new=AsyncMock(return_value=mock_response),
|
|
),
|
|
patch(
|
|
"src.crud.representation.RepresentationManager.save_representation",
|
|
new=AsyncMock(),
|
|
),
|
|
):
|
|
await process_representation_tasks_batch(
|
|
messages=messages,
|
|
message_level_configuration=create_test_configuration(),
|
|
observers=[peer.name],
|
|
observed=peer.name,
|
|
queue_item_message_ids=[m.id for m in messages],
|
|
)
|
|
|
|
metric_checker.assert_delta(
|
|
deriver_tokens_processed_counter,
|
|
labels,
|
|
before,
|
|
expected_prompt_tokens,
|
|
"Ingestion prompt input tokens",
|
|
)
|
|
|
|
async def test_ingestion_tracks_messages_input_tokens(
|
|
self,
|
|
db_session: AsyncSession,
|
|
sample_data: tuple[Workspace, Peer],
|
|
prometheus_test_setup: PrometheusMetricChecker,
|
|
):
|
|
"""Verify MESSAGES component is tracked for ingestion input."""
|
|
from src.deriver.deriver import process_representation_tasks_batch
|
|
|
|
metric_checker = prometheus_test_setup
|
|
workspace, peer = sample_data
|
|
session = await create_test_session_with_peer(db_session, workspace, peer)
|
|
messages = await create_test_messages(
|
|
db_session,
|
|
workspace.name,
|
|
session.name,
|
|
peer.name,
|
|
count=1,
|
|
content_prefix="Hello this is a test message",
|
|
)
|
|
|
|
mock_response = create_mock_deriver_response()
|
|
|
|
labels = {
|
|
"namespace": "test",
|
|
"task_type": "ingestion",
|
|
"token_type": "input",
|
|
"component": "messages",
|
|
}
|
|
before = metric_checker.capture(deriver_tokens_processed_counter, labels)
|
|
|
|
with (
|
|
patch(
|
|
"src.deriver.deriver.honcho_llm_call",
|
|
new=AsyncMock(return_value=mock_response),
|
|
),
|
|
patch(
|
|
"src.crud.representation.RepresentationManager.save_representation",
|
|
new=AsyncMock(),
|
|
),
|
|
):
|
|
await process_representation_tasks_batch(
|
|
messages=messages,
|
|
message_level_configuration=create_test_configuration(),
|
|
observers=[peer.name],
|
|
observed=peer.name,
|
|
queue_item_message_ids=[m.id for m in messages],
|
|
)
|
|
|
|
# Verify messages tokens were tracked (should be > 0)
|
|
delta = metric_checker.get_delta(
|
|
deriver_tokens_processed_counter, labels, before
|
|
)
|
|
assert delta > 0, f"Expected messages input tokens > 0, got {delta}"
|
|
|
|
|
|
# =============================================================================
|
|
# Deriver Summary Metrics Tests
|
|
# =============================================================================
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
class TestDeriverSummaryMetrics:
|
|
"""Test token metrics for deriver SUMMARY task type."""
|
|
|
|
async def test_summary_tracks_output_tokens(
|
|
self,
|
|
db_session: AsyncSession,
|
|
sample_data: tuple[Workspace, Peer],
|
|
prometheus_test_setup: PrometheusMetricChecker,
|
|
):
|
|
"""Verify OUTPUT_TOTAL tokens are tracked for summary."""
|
|
|
|
metric_checker = prometheus_test_setup
|
|
workspace, peer = sample_data
|
|
session = await create_test_session_with_peer(db_session, workspace, peer)
|
|
|
|
# Create messages for summary
|
|
messages = await create_test_messages(
|
|
db_session, workspace.name, session.name, peer.name, count=5
|
|
)
|
|
last_message = messages[-1]
|
|
|
|
# Mock _create_summary to return a summary with known token count
|
|
expected_output_tokens = 25
|
|
mock_summary = {
|
|
"content": "This is a test summary.",
|
|
"token_count": expected_output_tokens,
|
|
"message_id": last_message.id,
|
|
"message_public_id": last_message.public_id,
|
|
}
|
|
|
|
labels = {
|
|
"namespace": "test",
|
|
"task_type": "summary",
|
|
"token_type": "output",
|
|
"component": "output_total",
|
|
}
|
|
before = metric_checker.capture(deriver_tokens_processed_counter, labels)
|
|
|
|
with (
|
|
patch(
|
|
"src.utils.summarizer._create_summary",
|
|
new=AsyncMock(
|
|
return_value=(mock_summary, False, 100, expected_output_tokens)
|
|
), # (summary, is_fallback, llm_input_tokens, llm_output_tokens)
|
|
),
|
|
patch(
|
|
"src.utils.summarizer._save_summary",
|
|
new=AsyncMock(),
|
|
),
|
|
):
|
|
await _create_and_save_summary(
|
|
workspace_name=workspace.name,
|
|
session_name=session.name,
|
|
message_id=last_message.id,
|
|
message_seq_in_session=last_message.seq_in_session,
|
|
summary_type=SummaryType.SHORT,
|
|
message_public_id=last_message.public_id,
|
|
configuration=create_test_configuration(),
|
|
)
|
|
|
|
# Verify output tokens match the summary token_count
|
|
metric_checker.assert_delta(
|
|
deriver_tokens_processed_counter,
|
|
labels,
|
|
before,
|
|
expected_output_tokens,
|
|
"Summary output tokens",
|
|
)
|
|
|
|
async def test_summary_tracks_prompt_input_tokens(
|
|
self,
|
|
db_session: AsyncSession,
|
|
sample_data: tuple[Workspace, Peer],
|
|
prometheus_test_setup: PrometheusMetricChecker,
|
|
):
|
|
"""Verify PROMPT component is tracked for summary input."""
|
|
|
|
metric_checker = prometheus_test_setup
|
|
workspace, peer = sample_data
|
|
session = await create_test_session_with_peer(db_session, workspace, peer)
|
|
messages = await create_test_messages(
|
|
db_session, workspace.name, session.name, peer.name, count=5
|
|
)
|
|
last_message = messages[-1]
|
|
|
|
expected_prompt_tokens = estimate_short_summary_prompt_tokens()
|
|
|
|
mock_summary = {
|
|
"content": "Test summary.",
|
|
"token_count": 10,
|
|
"message_id": last_message.id,
|
|
"message_public_id": last_message.public_id,
|
|
}
|
|
|
|
labels = {
|
|
"namespace": "test",
|
|
"task_type": "summary",
|
|
"token_type": "input",
|
|
"component": "prompt",
|
|
}
|
|
before = metric_checker.capture(deriver_tokens_processed_counter, labels)
|
|
|
|
with (
|
|
patch(
|
|
"src.utils.summarizer._create_summary",
|
|
new=AsyncMock(
|
|
return_value=(mock_summary, False, 100, 10)
|
|
), # (summary, is_fallback, llm_input_tokens, llm_output_tokens)
|
|
),
|
|
patch(
|
|
"src.utils.summarizer._save_summary",
|
|
new=AsyncMock(),
|
|
),
|
|
):
|
|
await _create_and_save_summary(
|
|
workspace_name=workspace.name,
|
|
session_name=session.name,
|
|
message_id=last_message.id,
|
|
message_seq_in_session=last_message.seq_in_session,
|
|
summary_type=SummaryType.SHORT,
|
|
message_public_id=last_message.public_id,
|
|
configuration=create_test_configuration(),
|
|
)
|
|
|
|
metric_checker.assert_delta(
|
|
deriver_tokens_processed_counter,
|
|
labels,
|
|
before,
|
|
expected_prompt_tokens,
|
|
"Summary prompt input tokens",
|
|
)
|
|
|
|
async def test_summary_tracks_messages_input_tokens(
|
|
self,
|
|
db_session: AsyncSession,
|
|
sample_data: tuple[Workspace, Peer],
|
|
prometheus_test_setup: PrometheusMetricChecker,
|
|
):
|
|
"""Verify MESSAGES component is tracked for summary input."""
|
|
|
|
metric_checker = prometheus_test_setup
|
|
workspace, peer = sample_data
|
|
session = await create_test_session_with_peer(db_session, workspace, peer)
|
|
messages = await create_test_messages(
|
|
db_session, workspace.name, session.name, peer.name, count=5
|
|
)
|
|
last_message = messages[-1]
|
|
|
|
# Get the actual messages that would be included in the summary
|
|
# (to match what _create_and_save_summary computes via get_messages_by_seq_range)
|
|
actual_messages = await crud.get_messages_by_seq_range(
|
|
db_session,
|
|
workspace.name,
|
|
session.name,
|
|
start_seq=1,
|
|
end_seq=last_message.seq_in_session,
|
|
)
|
|
expected_messages_tokens = sum(m.token_count for m in actual_messages)
|
|
|
|
mock_summary = {
|
|
"content": "Test summary.",
|
|
"token_count": 10,
|
|
"message_id": last_message.id,
|
|
"message_public_id": last_message.public_id,
|
|
}
|
|
|
|
labels = {
|
|
"namespace": "test",
|
|
"task_type": "summary",
|
|
"token_type": "input",
|
|
"component": "messages",
|
|
}
|
|
before = metric_checker.capture(deriver_tokens_processed_counter, labels)
|
|
|
|
with (
|
|
patch(
|
|
"src.utils.summarizer._create_summary",
|
|
new=AsyncMock(
|
|
return_value=(mock_summary, False, 100, 10)
|
|
), # (summary, is_fallback, llm_input_tokens, llm_output_tokens)
|
|
),
|
|
patch(
|
|
"src.utils.summarizer._save_summary",
|
|
new=AsyncMock(),
|
|
),
|
|
):
|
|
await _create_and_save_summary(
|
|
workspace_name=workspace.name,
|
|
session_name=session.name,
|
|
message_id=last_message.id,
|
|
message_seq_in_session=last_message.seq_in_session,
|
|
summary_type=SummaryType.SHORT,
|
|
message_public_id=last_message.public_id,
|
|
configuration=create_test_configuration(),
|
|
)
|
|
|
|
# Verify messages tokens match what summarizer actually computed
|
|
delta = metric_checker.get_delta(
|
|
deriver_tokens_processed_counter, labels, before
|
|
)
|
|
assert (
|
|
delta == expected_messages_tokens
|
|
), f"Expected messages input tokens {expected_messages_tokens}, got {delta}"
|
|
assert delta > 0, "Expected at least some message tokens to be tracked"
|
|
|
|
async def test_summary_fallback_does_not_track(
|
|
self,
|
|
db_session: AsyncSession,
|
|
sample_data: tuple[Workspace, Peer],
|
|
prometheus_test_setup: PrometheusMetricChecker,
|
|
):
|
|
"""Verify metrics are NOT emitted when _create_summary returns is_fallback=True."""
|
|
|
|
metric_checker = prometheus_test_setup
|
|
workspace, peer = sample_data
|
|
session = await create_test_session_with_peer(db_session, workspace, peer)
|
|
messages = await create_test_messages(
|
|
db_session, workspace.name, session.name, peer.name, count=5
|
|
)
|
|
last_message = messages[-1]
|
|
|
|
mock_summary = {
|
|
"content": "Fallback summary.",
|
|
"token_count": 10,
|
|
"message_id": last_message.id,
|
|
"message_public_id": last_message.public_id,
|
|
}
|
|
|
|
# Capture all relevant metric labels before
|
|
output_labels = {
|
|
"namespace": "test",
|
|
"task_type": "summary",
|
|
"token_type": "output",
|
|
"component": "output_total",
|
|
}
|
|
prompt_labels = {
|
|
"namespace": "test",
|
|
"task_type": "summary",
|
|
"token_type": "input",
|
|
"component": "prompt",
|
|
}
|
|
before_output = metric_checker.capture(
|
|
deriver_tokens_processed_counter, output_labels
|
|
)
|
|
before_prompt = metric_checker.capture(
|
|
deriver_tokens_processed_counter, prompt_labels
|
|
)
|
|
|
|
with patch(
|
|
"src.utils.summarizer._create_summary",
|
|
new=AsyncMock(
|
|
return_value=(mock_summary, True, 0, 0)
|
|
), # (summary, is_fallback=True, llm_input_tokens, llm_output_tokens)
|
|
):
|
|
await _create_and_save_summary(
|
|
workspace_name=workspace.name,
|
|
session_name=session.name,
|
|
message_id=last_message.id,
|
|
message_seq_in_session=last_message.seq_in_session,
|
|
summary_type=SummaryType.SHORT,
|
|
message_public_id=last_message.public_id,
|
|
configuration=create_test_configuration(),
|
|
)
|
|
|
|
# Verify NO change in metrics when fallback
|
|
output_delta = metric_checker.get_delta(
|
|
deriver_tokens_processed_counter, output_labels, before_output
|
|
)
|
|
prompt_delta = metric_checker.get_delta(
|
|
deriver_tokens_processed_counter, prompt_labels, before_prompt
|
|
)
|
|
|
|
assert (
|
|
output_delta == 0
|
|
), f"Expected no output token change on fallback, got {output_delta}"
|
|
assert (
|
|
prompt_delta == 0
|
|
), f"Expected no prompt token change on fallback, got {prompt_delta}"
|
|
|
|
|
|
# =============================================================================
|
|
# Dialectic Token Metrics Tests
|
|
# =============================================================================
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
class TestDialecticTokenMetrics:
|
|
"""Test token metrics for dialectic calls."""
|
|
|
|
async def test_dialectic_tracks_input_tokens(
|
|
self,
|
|
db_session: AsyncSession,
|
|
sample_data: tuple[Workspace, Peer],
|
|
prometheus_test_setup: PrometheusMetricChecker,
|
|
):
|
|
"""Verify INPUT tokens are tracked from LLM response."""
|
|
from src.dialectic.core import DialecticAgent
|
|
|
|
metric_checker = prometheus_test_setup
|
|
workspace, peer = sample_data
|
|
session = await create_test_session_with_peer(db_session, workspace, peer)
|
|
|
|
expected_input_tokens = 150
|
|
mock_response = create_mock_dialectic_response(
|
|
input_tokens=expected_input_tokens, output_tokens=75
|
|
)
|
|
|
|
labels = {
|
|
"namespace": "test",
|
|
"token_type": "input",
|
|
"component": "total",
|
|
"reasoning_level": "low",
|
|
}
|
|
before = metric_checker.capture(dialectic_tokens_processed_counter, labels)
|
|
|
|
agent = DialecticAgent(
|
|
workspace_name=workspace.name,
|
|
session_name=session.name,
|
|
observer=peer.name,
|
|
observed=peer.name,
|
|
)
|
|
|
|
with patch(
|
|
"src.dialectic.core.honcho_llm_call",
|
|
new=AsyncMock(return_value=mock_response),
|
|
):
|
|
await agent.answer("What do you know about this user?")
|
|
|
|
metric_checker.assert_delta(
|
|
dialectic_tokens_processed_counter,
|
|
labels,
|
|
before,
|
|
expected_input_tokens,
|
|
"Dialectic input tokens",
|
|
)
|
|
|
|
async def test_dialectic_tracks_output_tokens(
|
|
self,
|
|
db_session: AsyncSession,
|
|
sample_data: tuple[Workspace, Peer],
|
|
prometheus_test_setup: PrometheusMetricChecker,
|
|
):
|
|
"""Verify OUTPUT tokens are tracked from LLM response."""
|
|
from src.dialectic.core import DialecticAgent
|
|
|
|
metric_checker = prometheus_test_setup
|
|
workspace, peer = sample_data
|
|
session = await create_test_session_with_peer(db_session, workspace, peer)
|
|
|
|
expected_output_tokens = 75
|
|
mock_response = create_mock_dialectic_response(
|
|
input_tokens=150, output_tokens=expected_output_tokens
|
|
)
|
|
|
|
labels = {
|
|
"namespace": "test",
|
|
"token_type": "output",
|
|
"component": "total",
|
|
"reasoning_level": "low",
|
|
}
|
|
before = metric_checker.capture(dialectic_tokens_processed_counter, labels)
|
|
|
|
agent = DialecticAgent(
|
|
workspace_name=workspace.name,
|
|
session_name=session.name,
|
|
observer=peer.name,
|
|
observed=peer.name,
|
|
)
|
|
|
|
with patch(
|
|
"src.dialectic.core.honcho_llm_call",
|
|
new=AsyncMock(return_value=mock_response),
|
|
):
|
|
await agent.answer("What do you know about this user?")
|
|
|
|
metric_checker.assert_delta(
|
|
dialectic_tokens_processed_counter,
|
|
labels,
|
|
before,
|
|
expected_output_tokens,
|
|
"Dialectic output tokens",
|
|
)
|
|
|
|
async def test_dialectic_metrics_disabled_no_emission(
|
|
self,
|
|
db_session: AsyncSession,
|
|
sample_data: tuple[Workspace, Peer],
|
|
prometheus_test_setup: PrometheusMetricChecker,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
):
|
|
"""Verify metrics are NOT emitted when PROMETHEUS.ENABLED=False."""
|
|
from src.dialectic.core import DialecticAgent
|
|
|
|
metric_checker = prometheus_test_setup
|
|
|
|
# Explicitly disable Prometheus metrics
|
|
monkeypatch.setattr("src.config.settings.METRICS.ENABLED", False)
|
|
|
|
workspace, peer = sample_data
|
|
session = await create_test_session_with_peer(db_session, workspace, peer)
|
|
|
|
mock_response = create_mock_dialectic_response(
|
|
input_tokens=200, output_tokens=100
|
|
)
|
|
|
|
# Capture before values
|
|
input_labels = {
|
|
"namespace": "test",
|
|
"token_type": "input",
|
|
"component": "total",
|
|
"reasoning_level": "low",
|
|
}
|
|
output_labels = {
|
|
"namespace": "test",
|
|
"token_type": "output",
|
|
"component": "total",
|
|
"reasoning_level": "low",
|
|
}
|
|
before_input = metric_checker.capture(
|
|
dialectic_tokens_processed_counter, input_labels
|
|
)
|
|
before_output = metric_checker.capture(
|
|
dialectic_tokens_processed_counter, output_labels
|
|
)
|
|
|
|
agent = DialecticAgent(
|
|
workspace_name=workspace.name,
|
|
session_name=session.name,
|
|
observer=peer.name,
|
|
observed=peer.name,
|
|
)
|
|
|
|
with patch(
|
|
"src.dialectic.core.honcho_llm_call",
|
|
new=AsyncMock(return_value=mock_response),
|
|
):
|
|
await agent.answer("What do you know about this user?")
|
|
|
|
# Verify NO change in metrics
|
|
input_delta = metric_checker.get_delta(
|
|
dialectic_tokens_processed_counter, input_labels, before_input
|
|
)
|
|
output_delta = metric_checker.get_delta(
|
|
dialectic_tokens_processed_counter, output_labels, before_output
|
|
)
|
|
|
|
assert (
|
|
input_delta == 0
|
|
), f"Expected no input token change when disabled, got {input_delta}"
|
|
assert (
|
|
output_delta == 0
|
|
), f"Expected no output token change when disabled, got {output_delta}"
|