Ctrl K
ComunidadGuías…

test/stream/test_history_queries.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Copyright (c) 2026, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
#
# SPDX-License-Identifier: Apache-2.0

import asyncio
from collections.abc import Iterable

import pytest

from ag2 import Context, MemoryStream
from ag2.context import StreamId
from ag2.events import BaseEvent, ModelRequest, ModelResponse, TextInput, ToolCallEvent, ToolCallsEvent
from ag2.history import MemoryStorage


class SpecialToolCall(ToolCallEvent):
    pass


class RecordedStorage(MemoryStorage):
    def __init__(self) -> None:
        super().__init__()
        self.reads = 0

    async def get_history(self, stream_id: StreamId) -> Iterable[BaseEvent]:
        self.reads += 1
        return iter(tuple(await super().get_history(stream_id)))


class UnavailableStorage(MemoryStorage):
    async def get_history(self, stream_id: StreamId) -> Iterable[BaseEvent]:
        raise OSError("storage unavailable")


@pytest.mark.asyncio
async def test_history_selects_retained_events_in_publication_order() -> None:
    stream = MemoryStream()
    context = Context(stream)
    first = ToolCallEvent(name="lookup", arguments="{}")
    last = ToolCallEvent(name="lookup", arguments='{"page": 2}')
    await context.send(first)
    await context.send(ModelRequest([TextInput("question")]))
    await context.send(last)

    query = stream.history.where(ToolCallEvent)

    assert await query.all() == [first, last]
    assert await query.first() is first
    assert await query.last() is last
    assert await query.count() == 2


@pytest.mark.asyncio
async def test_history_query_combines_conditions_without_changing_original() -> None:
    stream = MemoryStream()
    context = Context(stream)
    first = ToolCallEvent(name="lookup", arguments="{}")
    second = SpecialToolCall(name="other", arguments="{}")
    await context.send(first)
    await context.send(second)

    calls = stream.history.where(ToolCallEvent)
    lookups = calls.where(ToolCallEvent.name == "lookup")

    assert await lookups.all() == [first]
    assert await calls.all() == [first, second]
    assert await stream.history.where(ToolCallEvent.name == "other").all() == [second]
    assert await lookups.where(ToolCallEvent.name == "other").all() == []


@pytest.mark.asyncio
async def test_empty_history_queries_complete_without_waiting_for_publication() -> None:
    query = MemoryStream().history.where(ToolCallEvent)

    assert await asyncio.wait_for(query.all(), timeout=1) == []
    assert await asyncio.wait_for(query.first(), timeout=1) is None
    assert await asyncio.wait_for(query.last(), timeout=1) is None
    assert await asyncio.wait_for(query.count(), timeout=1) == 0


@pytest.mark.asyncio
async def test_history_queries_keep_top_level_occurrences_and_return_fresh_lists() -> None:
    stream = MemoryStream()
    context = Context(stream)
    call = ToolCallEvent(name="lookup", arguments="{}")
    batch = ToolCallsEvent([call])
    await context.send(ModelResponse(tool_calls=batch))
    await context.send(batch)
    await context.send(call)
    await context.send(call)

    query = stream.history.where(ToolCallEvent)
    selected = await query.all()
    assert selected == [call, call]
    selected.clear()

    assert await query.count() == 2
    assert await query.all() == [call, call]
    assert len(list(await stream.history.get_events())) == 4


@pytest.mark.asyncio
async def test_history_query_uses_one_fresh_custom_storage_snapshot_per_terminal() -> None:
    storage = RecordedStorage()
    stream = MemoryStream(storage=storage)
    context = Context(stream)
    first = ToolCallEvent(name="first", arguments="{}")
    second = ToolCallEvent(name="second", arguments="{}")
    await context.send(first)
    query = stream.history.where(ToolCallEvent).where(ToolCallEvent.name != "ignored")
    assert storage.reads == 0

    assert await query.all() == [first]
    assert storage.reads == 1
    await context.send(second)
    assert await query.last() is second
    assert storage.reads == 2
    assert await query.first() is first
    assert storage.reads == 3
    assert await query.count() == 2
    assert storage.reads == 4

    await stream.history.replace([second])
    restored = MemoryStream(storage=storage, id=stream.id)
    assert await restored.history.where(ToolCallEvent).all() == [second]


@pytest.mark.asyncio
async def test_filtered_stream_query_uses_parent_history_without_live_filter() -> None:
    stream = MemoryStream()
    context = Context(stream)
    request = ModelRequest([TextInput("question")])
    call = ToolCallEvent(name="lookup", arguments="{}")
    await context.send(request)
    await context.send(call)

    filtered = stream.where(ToolCallEvent)

    assert await filtered.history.where(ModelRequest).all() == [request]
    assert await filtered.history.where(BaseEvent).all() == [request, call]


@pytest.mark.asyncio
async def test_history_query_propagates_storage_errors() -> None:
    query = MemoryStream(storage=UnavailableStorage()).history.where(BaseEvent)

    with pytest.raises(OSError, match="storage unavailable"):
        await query.count()