Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 37 additions & 16 deletions camel/agents/chat_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -1046,10 +1046,11 @@ def _get_context_with_summarization(
f"exceed limit, full compression."
)
summary = self.summarize(include_summaries=True)
self._update_memory_with_summary(
summary.get("summary", ""), include_summaries=True
)
return self.memory.get_context()
if self._try_update_memory_with_summary(
summary, include_summaries=True
):
return self.memory.get_context()
return openai_messages, num_tokens

threshold = self._calculate_next_summary_threshold()
if num_tokens > threshold:
Expand All @@ -1058,10 +1059,11 @@ def _get_context_with_summarization(
f"({threshold}). Triggering summarization."
)
summary = self.summarize(include_summaries=False)
self._update_memory_with_summary(
summary.get("summary", ""), include_summaries=False
)
return self.memory.get_context()
if self._try_update_memory_with_summary(
summary, include_summaries=False
):
return self.memory.get_context()
return openai_messages, num_tokens

return openai_messages, num_tokens

Expand All @@ -1082,10 +1084,11 @@ async def _get_context_with_summarization_async(
f"exceed limit, full compression."
)
summary = await self.asummarize(include_summaries=True)
self._update_memory_with_summary(
summary.get("summary", ""), include_summaries=True
)
return self.memory.get_context()
if self._try_update_memory_with_summary(
summary, include_summaries=True
):
return self.memory.get_context()
return openai_messages, num_tokens

threshold = self._calculate_next_summary_threshold()
if num_tokens > threshold:
Expand All @@ -1094,10 +1097,11 @@ async def _get_context_with_summarization_async(
f"({threshold}). Triggering summarization."
)
summary = await self.asummarize(include_summaries=False)
self._update_memory_with_summary(
summary.get("summary", ""), include_summaries=False
)
return self.memory.get_context()
if self._try_update_memory_with_summary(
summary, include_summaries=False
):
return self.memory.get_context()
return openai_messages, num_tokens

return openai_messages, num_tokens

Expand Down Expand Up @@ -1143,6 +1147,23 @@ def _calculate_next_summary_threshold(self) -> int:

return threshold

def _try_update_memory_with_summary(
self,
summary_result: Dict[str, Any],
include_summaries: bool = False,
) -> bool:
r"""Update memory when summary text exists, even if saving failed."""
summary = summary_result.get("summary", "")
if not isinstance(summary, str) or not summary.strip():
logger.warning(
"Skipping memory update because summarization failed: %s",
summary_result.get("status") or "unknown error",
)
return False

self._update_memory_with_summary(summary, include_summaries)
return True

def _update_memory_with_summary(
self, summary: str, include_summaries: bool = False
) -> None:
Expand Down
108 changes: 108 additions & 0 deletions test/agents/test_chat_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,114 @@ def test_clean_snapshot_in_memory_skips_missing_records():
assert entry.cached is True


def _make_agent_with_summarization_history(token_limit=100_000):
model = DummyModel(ModelType.GPT_4O_MINI)
model._token_counter = MagicMock()
model._token_counter.count_tokens_from_messages.return_value = 10
agent = ChatAgent(
system_message="You are a helpful assistant.",
model=model,
token_limit=token_limit,
summarize_threshold=50,
)
agent.update_memory(
BaseMessage.make_user_message("user", "Remember this."),
OpenAIBackendRole.USER,
)
agent.update_memory(
BaseMessage.make_assistant_message("assistant", "I will."),
OpenAIBackendRole.ASSISTANT,
)
agent._calculate_next_summary_threshold = MagicMock(return_value=0)
return agent


@pytest.mark.parametrize(
"summary_result",
[
{
"summary": "",
"status": "Failed to generate summary using model: transient",
},
{"summary": "", "status": "success"},
],
)
def test_failed_automatic_summarization_preserves_memory(summary_result):
agent = _make_agent_with_summarization_history()
context_before = agent.memory.get_context()
agent.summarize = MagicMock(return_value=summary_result)

context_after = agent._get_context_with_summarization()

assert context_after == context_before
assert agent.memory.get_context() == context_before


@pytest.mark.asyncio
async def test_failed_async_automatic_summarization_preserves_memory():
agent = _make_agent_with_summarization_history()
context_before = agent.memory.get_context()
agent.asummarize = AsyncMock(
return_value={
"summary": "",
"status": "Failed to generate summary from model response.",
}
)

context_after = await agent._get_context_with_summarization_async()

assert context_after == context_before
assert agent.memory.get_context() == context_before


@pytest.mark.asyncio
@pytest.mark.parametrize("async_mode", [False, True])
@pytest.mark.parametrize("include_summaries", [False, True])
async def test_automatic_summarization_uses_summary_when_save_fails(
async_mode, include_summaries
):
agent = _make_agent_with_summarization_history(token_limit=1000)
count_tokens = agent.model_backend.token_counter.count_tokens_from_messages
count_tokens.side_effect = lambda messages: 30 * len(messages)
# Exercise the real threshold so repeated context builds detect retries.
del agent._calculate_next_summary_threshold
for _ in range(10):
agent.update_memory(
BaseMessage.make_user_message("user", "Remember this."),
OpenAIBackendRole.USER,
)
agent.update_memory(
BaseMessage.make_assistant_message("assistant", "I will."),
OpenAIBackendRole.ASSISTANT,
)
if include_summaries:
agent._summary_token_count = agent.token_limit
summary = "[CONTEXT_SUMMARY] A perfectly good summary."
result = {"summary": summary, "status": "Error: write failed"}
summarizer = (
AsyncMock(return_value=result)
if async_mode
else MagicMock(return_value=result)
)
if async_mode:
agent.asummarize = summarizer
else:
agent.summarize = summarizer

for _ in range(5):
if async_mode:
messages, _ = await agent._get_context_with_summarization_async()
else:
messages, _ = agent._get_context_with_summarization()
assert len(messages) == 3
assert messages[1]["content"] == summary
assert "Remember this." in messages[2]["content"]

summarizer.assert_called_once_with(include_summaries=include_summaries)
if async_mode:
summarizer.assert_awaited_once()


@pytest.mark.model_backend
def test_chat_agent_stored_messages():
system_msg = BaseMessage(
Expand Down
Loading