183 lines
5.6 KiB
Python
183 lines
5.6 KiB
Python
import os
|
|
|
|
import pytest
|
|
|
|
import aisuite as ai
|
|
from aisuite.agents.postgres_state_store import (
|
|
_message_artifact_refs,
|
|
_replace_ordered_subsequence,
|
|
_shared_message_prefix_length,
|
|
)
|
|
|
|
|
|
def make_state():
|
|
return ai.RunState(
|
|
agent_name="assistant",
|
|
messages=[
|
|
{"role": "user", "content": "Hello"},
|
|
{"role": "assistant", "content": "Hi"},
|
|
{"role": "user", "content": "Continue"},
|
|
],
|
|
run_name="chat",
|
|
group_id="group_1",
|
|
metadata={"request_id": "req_1"},
|
|
)
|
|
|
|
|
|
def test_replace_ordered_subsequence_replaces_contiguous_source():
|
|
assert _replace_ordered_subsequence(
|
|
["m1", "m2", "m3", "m4"],
|
|
["m2", "m3"],
|
|
["summary"],
|
|
) == ["m1", "summary", "m4"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"source",
|
|
[[], ["m1", "m3"], ["missing"]],
|
|
)
|
|
def test_replace_ordered_subsequence_rejects_invalid_source(source):
|
|
with pytest.raises(ValueError):
|
|
_replace_ordered_subsequence(["m1", "m2", "m3"], source, ["summary"])
|
|
|
|
|
|
def test_shared_message_prefix_length_detects_reused_context_prefix():
|
|
old_messages = [
|
|
{"role": "system", "content": "summary"},
|
|
{"role": "user", "content": "latest"},
|
|
]
|
|
new_messages = [
|
|
{"role": "system", "content": "summary"},
|
|
{"role": "user", "content": "latest"},
|
|
{"role": "assistant", "content": "answer"},
|
|
]
|
|
|
|
assert _shared_message_prefix_length(old_messages, new_messages) == 2
|
|
assert _shared_message_prefix_length(old_messages, new_messages[1:]) == 0
|
|
|
|
|
|
def test_message_artifact_refs_extracts_nested_artifacts_once():
|
|
artifact_value = {
|
|
"type": "artifact_ref",
|
|
"preview": "abc",
|
|
"artifact_ref": {
|
|
"artifact_id": "artifact_1",
|
|
"uri": "artifact://artifact_1",
|
|
"media_type": "text/plain",
|
|
"size_bytes": 3,
|
|
"metadata": {"field": "stdout"},
|
|
},
|
|
}
|
|
message = {
|
|
"role": "tool",
|
|
"content": {"stdout": artifact_value},
|
|
"artifact_refs": [artifact_value],
|
|
}
|
|
|
|
refs = _message_artifact_refs(message)
|
|
|
|
assert len(refs) == 1
|
|
assert refs[0]["artifact_ref"]["artifact_id"] == "artifact_1"
|
|
assert refs[0]["artifact_ref"]["metadata"]["field"] == "stdout"
|
|
|
|
|
|
def test_compaction_record_serializes():
|
|
record = ai.CompactionRecord(
|
|
compaction_id="cmp_1",
|
|
thread_id="thread_1",
|
|
source_message_ids=["m1", "m2"],
|
|
summary_message_id="m_summary",
|
|
summary_text="Earlier context summary",
|
|
model="openai:gpt-4o-mini",
|
|
input_token_count=100,
|
|
output_token_count=20,
|
|
metadata={"kind": "manual"},
|
|
)
|
|
|
|
assert record.to_dict()["source_message_ids"] == ["m1", "m2"]
|
|
assert record.to_dict()["summary_text"] == "Earlier context summary"
|
|
|
|
|
|
@pytest.mark.integration
|
|
def test_postgres_state_store_round_trip_and_compaction():
|
|
dsn = os.environ.get("AISUITE_TEST_POSTGRES_DSN")
|
|
if not dsn:
|
|
pytest.skip("Set AISUITE_TEST_POSTGRES_DSN to run Postgres integration test")
|
|
|
|
store = ai.PostgresStateStore.from_dsn(dsn, create_schema=True)
|
|
thread_id = "test_thread_state_store"
|
|
store.delete_state(thread_id)
|
|
|
|
stored = store.save_state(
|
|
thread_id,
|
|
make_state(),
|
|
metadata={"user_id": "user_1"},
|
|
)
|
|
loaded = store.load_state(thread_id)
|
|
assert loaded.revision == 1
|
|
assert loaded.metadata == {"user_id": "user_1"}
|
|
assert [message["content"] for message in loaded.state.messages] == [
|
|
"Hello",
|
|
"Hi",
|
|
"Continue",
|
|
]
|
|
|
|
head = store.get_thread_head(thread_id)
|
|
assert head["model_context_message_ids"] == head["full_history_message_ids"]
|
|
assert len(head["model_context_message_ids"]) == 3
|
|
|
|
compacted = store.compact_state(
|
|
thread_id,
|
|
head["model_context_message_ids"][:2],
|
|
{
|
|
"role": "system",
|
|
"content": "Summary: user said hello and assistant replied.",
|
|
},
|
|
revision=stored.revision,
|
|
reason="test compaction",
|
|
model="test-model",
|
|
input_token_count=25,
|
|
output_token_count=9,
|
|
metadata={"kind": "manual"},
|
|
)
|
|
assert compacted.revision == 2
|
|
assert [message["content"] for message in compacted.state.messages] == [
|
|
"Summary: user said hello and assistant replied.",
|
|
"Continue",
|
|
]
|
|
|
|
compacted_head = store.get_thread_head(thread_id)
|
|
assert len(compacted_head["model_context_message_ids"]) == 2
|
|
assert len(compacted_head["full_history_message_ids"]) == 3
|
|
assert (
|
|
compacted_head["compacted_from_message_ids"]
|
|
== head["model_context_message_ids"][:2]
|
|
)
|
|
|
|
compactions = store.list_compactions(thread_id)
|
|
assert len(compactions) == 1
|
|
assert compactions[0].source_message_ids == head["model_context_message_ids"][:2]
|
|
assert (
|
|
compactions[0].summary_text == "Summary: user said hello and assistant replied."
|
|
)
|
|
|
|
continued_state = compacted.state
|
|
continued_state.add_user_message("After compaction")
|
|
saved_after_compaction = store.save_state(
|
|
thread_id,
|
|
continued_state,
|
|
revision=compacted.revision,
|
|
)
|
|
final_head = store.get_thread_head(thread_id)
|
|
assert saved_after_compaction.revision == 3
|
|
assert len(final_head["model_context_message_ids"]) == 3
|
|
assert len(final_head["full_history_message_ids"]) == 4
|
|
|
|
with pytest.raises(ai.StateConflictError):
|
|
store.save_state(thread_id, make_state(), revision=stored.revision)
|
|
|
|
store.delete_state(thread_id)
|
|
assert store.load_state(thread_id) is None
|
|
assert store.get_thread_head(thread_id) is None
|
|
assert store.list_compactions(thread_id) == []
|