mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Implement annotation-based context compaction (#4469)
* Implement annotation-based context compaction Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Handle missing compaction attributes in BaseChatClient Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix CI typing and bandit issues Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Optimize incremental compaction annotation pass Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refinement * Python: add ToolResultCompactionStrategy and CompactionProvider Add ToolResultCompactionStrategy that collapses older tool-call groups into short summary messages (e.g. [Tool calls: get_weather]) while keeping the most recent groups verbatim. This mirrors the .NET ToolResultCompactionStrategy from PR #4533. Add CompactionProvider as a context-provider that auto-applies compaction before each agent turn and stores compacted history in session state after each turn. Includes tests and samples for both features. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refinement and alignment with dotnet PR * updated tool result compaction * updated tool result compaction * Python: add ToolResultCompactionStrategy, CompactionProvider, and skip_excluded - ToolResultCompactionStrategy collapses older tool-call groups into [Tool results: func_name: result] summaries with bidirectional tracing (same pattern as SummarizationStrategy). - CompactionProvider as BaseContextProvider with separate before_strategy and after_strategy parameters. before_strategy compacts loaded context; after_strategy compacts stored history via history_source_id. - InMemoryHistoryProvider gains skip_excluded flag to filter out messages marked as excluded by compaction strategies. - Tests, samples, and exports updated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fixed checks * fix mypy * Fix: ensure summary messages from both strategies get full compaction annotations SummarizationStrategy was not calling annotate_message_groups after inserting its summary message, so the summary lacked core group annotations (id, kind, index, has_reasoning, _excluded). Added the missing call. ToolResultCompactionStrategy already had it. Added tests verifying both strategies produce fully annotated summaries. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * updated propagation * fix mypy --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
565c0b1623
commit
3e03a305f6
@@ -0,0 +1,115 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import (
|
||||
CharacterEstimatorTokenizer,
|
||||
ChatResponse,
|
||||
Message,
|
||||
SelectiveToolCallCompactionStrategy,
|
||||
SlidingWindowStrategy,
|
||||
SummarizationStrategy,
|
||||
TokenBudgetComposedStrategy,
|
||||
annotate_message_groups,
|
||||
apply_compaction,
|
||||
included_token_count,
|
||||
)
|
||||
|
||||
"""This sample demonstrates composed in-run compaction with a token budget.
|
||||
|
||||
Key components:
|
||||
- TokenBudgetComposedStrategy
|
||||
- Sequential strategy composition
|
||||
- Summarization with a SupportsChatGetResponse-compatible summarizer client
|
||||
"""
|
||||
|
||||
|
||||
class BudgetSummaryClient:
|
||||
async def get_response(
|
||||
self,
|
||||
messages: list[Message],
|
||||
*,
|
||||
stream: bool = False,
|
||||
options: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> ChatResponse:
|
||||
summary_text = f"Budget summary generated from {len(messages)} prompt messages."
|
||||
return ChatResponse(messages=[Message(role="assistant", text=summary_text)])
|
||||
|
||||
|
||||
def _build_long_history() -> list[Message]:
|
||||
history = [Message(role="system", text="You are a migration copilot.")]
|
||||
for i in range(1, 8):
|
||||
history.append(
|
||||
Message(
|
||||
role="user",
|
||||
text=f"Iteration {i}: capture migration requirements and edge cases.",
|
||||
)
|
||||
)
|
||||
history.append(
|
||||
Message(
|
||||
role="assistant",
|
||||
text=(
|
||||
f"Iteration {i}: detailed plan with dependencies, rollback guidance, and testing details. "
|
||||
"This sentence is intentionally long to create token pressure."
|
||||
),
|
||||
)
|
||||
)
|
||||
return history
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
# 1. Build synthetic history representing long-running in-run growth.
|
||||
messages = _build_long_history()
|
||||
|
||||
# 2. Configure tokenizer and measure token count before compaction.
|
||||
tokenizer = CharacterEstimatorTokenizer()
|
||||
annotate_message_groups(messages, tokenizer=tokenizer)
|
||||
budget_before = included_token_count(messages)
|
||||
|
||||
# 3. Configure composed strategy stack.
|
||||
composed = TokenBudgetComposedStrategy(
|
||||
token_budget=200,
|
||||
tokenizer=tokenizer,
|
||||
strategies=[
|
||||
SelectiveToolCallCompactionStrategy(keep_last_tool_call_groups=0),
|
||||
SummarizationStrategy(
|
||||
client=BudgetSummaryClient(),
|
||||
target_count=3,
|
||||
threshold=3,
|
||||
),
|
||||
SlidingWindowStrategy(keep_last_groups=4),
|
||||
],
|
||||
)
|
||||
|
||||
# 4. Apply compaction and inspect the budget result.
|
||||
projected = await apply_compaction(messages, strategy=composed, tokenizer=tokenizer)
|
||||
budget_after = included_token_count(messages)
|
||||
|
||||
print(f"Projected messages after compaction: {len(projected)}")
|
||||
print(f"Included token count before compaction: {budget_before}")
|
||||
print(f"Included token count after compaction: {budget_after}")
|
||||
print("Projected roles:", [m.role for m in projected])
|
||||
print("Projected messages with token counts:")
|
||||
for msg in projected:
|
||||
group = msg.additional_properties.get("_group")
|
||||
token_count = group.get("token_count") if isinstance(group, dict) else None
|
||||
text_preview = msg.text[:80] if msg.text else "<non-text>"
|
||||
print(f"- [{msg.role}] {text_preview} ({token_count} tokens)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
"""
|
||||
Sample output:
|
||||
Projected messages after compaction: 3
|
||||
Included token count before compaction: 793
|
||||
Included token count after compaction: 144
|
||||
Projected roles: ['system', 'user', 'assistant']
|
||||
Projected messages with token counts:
|
||||
- [system] You are a migration copilot. (35 tokens)
|
||||
- [user] Iteration 7: capture migration requirements and edge cases. (43 tokens)
|
||||
- [assistant] Iteration 7: detailed plan with dependencies, rollback guidance, and testing det (66 tokens)
|
||||
"""
|
||||
Reference in New Issue
Block a user