From c2ff0d1cdafa11a34855fb91dbfa8aceb3b09ce4 Mon Sep 17 00:00:00 2001 From: lawcontinue <134219708+lawcontinue@users.noreply.github.com> Date: Tue, 21 Apr 2026 00:47:16 +0800 Subject: [PATCH 1/3] fix: correct context.terminate comment per Crit P0 review Clarify that context.terminate = True terminates the auto function calling loop, not just skips a function call. --- python/samples/concepts/filtering/README.md | 81 +++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 python/samples/concepts/filtering/README.md diff --git a/python/samples/concepts/filtering/README.md b/python/samples/concepts/filtering/README.md new file mode 100644 index 000000000000..68b9a466753d --- /dev/null +++ b/python/samples/concepts/filtering/README.md @@ -0,0 +1,81 @@ +# Filtering Samples + +This directory contains samples demonstrating the Python filter system in Semantic Kernel. + +Filters allow you to intercept and modify pipeline execution at specific points. The Python SDK supports three filter types. + +## Filter Types + +| Filter | Decorator | Purpose | +|--------|-----------|---------| +| **Prompt Rendering** | `@kernel.filter(FilterTypes.PROMPT_RENDERING)` | Intercept before/after prompt is rendered | +| **Function Invocation** | `@kernel.filter(FilterTypes.FUNCTION_INVOCATION)` | Intercept before/after any function call | +| **Auto Function Invoke** | `@kernel.filter(FilterTypes.AUTO_FUNCTION_INVOCATION)` | Control automatic tool calls | + +## Samples + +### [prompt_filters.py](./prompt_filters.py) + +Basic prompt rendering filter. Demonstrates how to inspect and modify prompts before they're sent to the model. + +### [function_invocation_filters.py](./function_invocation_filters.py) + +Function invocation filter with logging and exception handling. Shows both `kernel.add_filter()` and `@kernel.filter()` decorator registration. + +### [function_invocation_filters_stream.py](./function_invocation_filters_stream.py) + +Same as above but for **streaming** responses. Use this when working with streaming chat completions. + +### [auto_function_invoke_filters.py](./auto_function_invoke_filters.py) + +Controls which automatic tool calls are allowed. Demonstrates `context.terminate = True` to terminate the auto function calling loop, and `FunctionResultContent` handling. + +### [retry_with_filters.py](./retry_with_filters.py) + +Implements automatic retry logic using function invocation filters. Retries on failure with a different model. + +### [retry_with_different_model.py](./retry_with_different_model.py) + +Similar retry pattern but specifically switches to a fallback model on failure. + +## Registration + +There are two ways to register a filter: + +```python +# Method 1: Decorator +@kernel.filter(filter_type=FilterTypes.FUNCTION_INVOCATION) +async def my_filter(context, next): + await next(context) + +# Method 2: Add function +kernel.add_filter("function_invocation", my_filter) +``` + +Both are equivalent. Use whichever fits your code style. + +## Filter Signature + +All filters follow the same signature: + +```python +async def filter_name(context: , next: Callable) -> None: + # Code before next filter/function runs + await next(context) + # Code after next filter/function runs +``` + +The `next` callable passes control to the next filter in the chain, then to the actual function. You can skip execution by not calling `await next(context)`. + +## Terminating Auto Function Calls + +To prevent an auto-invoked function from executing: + +```python +@kernel.filter(FilterTypes.AUTO_FUNCTION_INVOCATION) +async def selective_filter(context: AutoFunctionInvocationContext, next): + if context.function.name == "dangerous_function": + context.terminate = True # Terminate the auto function calling loop + return + await next(context) +``` From 236fa3fad87b5952ed21aa380c35ae7ce16483ce Mon Sep 17 00:00:00 2001 From: lawcontinue <134219708+lawcontinue@users.noreply.github.com> Date: Wed, 29 Apr 2026 21:08:27 +0800 Subject: [PATCH 2/3] fix: Add chat history reducer support to GroupChatOrchestration Allow GroupChatOrchestration to accept an optional ChatHistoryReducer that automatically summarizes or truncates the internal chat history after each agent response. This fixes #12303 where ChatHistorySummarizationReducer was accepted by the API but never triggered because GroupChatOrchestration manages its own ChatHistory internally, bypassing the reducer path. Changes: - Add chat_history_reducer param to GroupChatOrchestration.__init__ - Add _maybe_reduce_chat_history to GroupChatManagerActor - Call reducer after each agent response in _handle_response_message - Fully backward compatible: default is None (no reducer) --- .../agents/orchestration/group_chat.py | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/python/semantic_kernel/agents/orchestration/group_chat.py b/python/semantic_kernel/agents/orchestration/group_chat.py index 65c4640e72a5..3f90eb0d783f 100644 --- a/python/semantic_kernel/agents/orchestration/group_chat.py +++ b/python/semantic_kernel/agents/orchestration/group_chat.py @@ -18,6 +18,7 @@ from semantic_kernel.agents.runtime.core.topic import TopicId from semantic_kernel.agents.runtime.in_process.type_subscription import TypeSubscription from semantic_kernel.contents.chat_history import ChatHistory +from semantic_kernel.contents.history_reducer.chat_history_reducer import ChatHistoryReducer from semantic_kernel.contents.chat_message_content import ChatMessageContent from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent from semantic_kernel.contents.utils.author_role import AuthorRole @@ -259,6 +260,7 @@ def __init__( participant_descriptions: dict[str, str], exception_callback: Callable[[BaseException], None], result_callback: Callable[[DefaultTypeAlias], Awaitable[None]] | None = None, + chat_history_reducer: ChatHistoryReducer | None = None, ): """Initialize the group chat manager actor. @@ -271,7 +273,8 @@ def __init__( """ self._manager = manager self._internal_topic_type = internal_topic_type - self._chat_history = ChatHistory() + self._chat_history: ChatHistory = ChatHistory() + self._chat_history_reducer = chat_history_reducer self._participant_descriptions = participant_descriptions self._result_callback = result_callback @@ -301,9 +304,22 @@ async def _handle_response_message(self, message: GroupChatResponseMessage, ctx: ) ) self._chat_history.add_message(message.body) + await self._maybe_reduce_chat_history() await self._determine_state_and_take_action(ctx.cancellation_token) + async def _maybe_reduce_chat_history(self) -> None: + """Reduce the chat history if a reducer is configured and the threshold is exceeded.""" + if self._chat_history_reducer is not None: + # Sync messages from the internal history to the reducer + self._chat_history_reducer.messages = self._chat_history.messages + result = await self._chat_history_reducer.reduce() + if result is not None: + self._chat_history.messages = result.messages + logger.debug( + f"Chat history reduced to {len(self._chat_history.messages)} messages." + ) + @ActorBase.exception_handler async def _determine_state_and_take_action(self, cancellation_token: CancellationToken) -> None: """Determine the state of the group chat and take action accordingly.""" @@ -377,6 +393,7 @@ def __init__( agent_response_callback: Callable[[DefaultTypeAlias], Awaitable[None] | None] | None = None, streaming_agent_response_callback: Callable[[StreamingChatMessageContent, bool], Awaitable[None] | None] | None = None, + chat_history_reducer: ChatHistoryReducer | None = None, ) -> None: """Initialize the group chat orchestration. @@ -392,8 +409,12 @@ def __init__( by the agents. streaming_agent_response_callback (Callable | None): A function that is called when a streaming response is produced by the agents. + chat_history_reducer (ChatHistoryReducer | None): An optional reducer to summarize or truncate + the chat history during the group chat. When provided, the reducer is called after each + agent response to keep the history within bounds. """ self._manager = manager + self._chat_history_reducer = chat_history_reducer for member in members: if member.description is None: @@ -496,6 +517,7 @@ async def _register_manager( participant_descriptions={agent.name: agent.description for agent in self._members}, # type: ignore[misc] exception_callback=exception_callback, result_callback=result_callback, + chat_history_reducer=self._chat_history_reducer, ), ) From 326eea469a51e840b8955c75333dcca082d6201b Mon Sep 17 00:00:00 2001 From: lawcontinue <134219708+lawcontinue@users.noreply.github.com> Date: Wed, 29 Apr 2026 21:50:57 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20Address=20Copilot=20review=20?= =?UTF-8?q?=E2=80=94=20preserve=20reducer=20messages,=20clarify=20scope?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Copy reducer pre-seeded messages into _chat_history at init (fixes #4) - Use list() copy to avoid mutating reducer state - Update docstring to clarify scope: manager-level reduce only - AgentThread-level reduce is a separate enhancement --- .../agents/orchestration/group_chat.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/python/semantic_kernel/agents/orchestration/group_chat.py b/python/semantic_kernel/agents/orchestration/group_chat.py index 3f90eb0d783f..2cbfea89d83d 100644 --- a/python/semantic_kernel/agents/orchestration/group_chat.py +++ b/python/semantic_kernel/agents/orchestration/group_chat.py @@ -275,6 +275,8 @@ def __init__( self._internal_topic_type = internal_topic_type self._chat_history: ChatHistory = ChatHistory() self._chat_history_reducer = chat_history_reducer + if chat_history_reducer is not None and chat_history_reducer.messages: + self._chat_history.messages = list(chat_history_reducer.messages) self._participant_descriptions = participant_descriptions self._result_callback = result_callback @@ -309,10 +311,15 @@ async def _handle_response_message(self, message: GroupChatResponseMessage, ctx: await self._determine_state_and_take_action(ctx.cancellation_token) async def _maybe_reduce_chat_history(self) -> None: - """Reduce the chat history if a reducer is configured and the threshold is exceeded.""" + """Reduce the chat history if a reducer is configured and the threshold is exceeded. + + The reducer operates on the manager\'s internal chat history, which is used + for agent selection and termination decisions. Note that this does not reduce + the history passed to individual agent LLM calls — that would require reducer + support at the AgentThread level, which is a separate enhancement. + """ if self._chat_history_reducer is not None: - # Sync messages from the internal history to the reducer - self._chat_history_reducer.messages = self._chat_history.messages + self._chat_history_reducer.messages = list(self._chat_history.messages) result = await self._chat_history_reducer.reduce() if result is not None: self._chat_history.messages = result.messages