Skip to content

Commit 784f13e

Browse files
authoredFeb 13, 2025
Python: Fix Chat History Agent format instructions regression. (microsoft#10512)
### Motivation and Context The chat history agent templating was inadvertently broken. This PR restores the functionality to be able to use templating with a Chat Completion Agent. <!-- Thank you for your contribution to the semantic-kernel repo! Please help reviewers and future users, providing the following information: 1. Why is this change required? 2. What problem does it solve? 3. What scenario does it contribute to? 4. If it fixes an open issue, please link to the issue here. --> ### Description Fix the templating for a Chat Completion Agent. Add unit tests for more coverage. - Fix a unit test fixture that was annotated to return an AsyncMock, but it's actually returning an async generator function. <!-- Describe your changes, the overall approach, the underlying design. These notes will help understanding how your code works. Thanks! --> ### Contribution Checklist <!-- Before submitting this PR, please make sure: --> - [X] The code builds clean without any errors or warnings - [X] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [X] All unit tests pass, and I have added new tests where possible - [X] I didn't break anyone 😄
1 parent 23a9036 commit 784f13e

14 files changed

+236
-154
lines changed
 

‎python/samples/learn_resources/agent_docs/agent_collaboration.py

+97-85
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,16 @@
33
import asyncio
44
import os
55

6+
from semantic_kernel import Kernel
67
from semantic_kernel.agents import AgentGroupChat, ChatCompletionAgent
7-
from semantic_kernel.agents.strategies.selection.kernel_function_selection_strategy import (
8+
from semantic_kernel.agents.strategies import (
89
KernelFunctionSelectionStrategy,
9-
)
10-
from semantic_kernel.agents.strategies.termination.kernel_function_termination_strategy import (
1110
KernelFunctionTerminationStrategy,
1211
)
13-
from semantic_kernel.agents.strategies.termination.termination_strategy import TerminationStrategy
14-
from semantic_kernel.connectors.ai.open_ai.services.azure_chat_completion import AzureChatCompletion
15-
from semantic_kernel.contents.chat_message_content import ChatMessageContent
12+
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion
13+
from semantic_kernel.contents import ChatHistoryTruncationReducer, ChatMessageContent
1614
from semantic_kernel.contents.utils.author_role import AuthorRole
1715
from semantic_kernel.functions.kernel_function_from_prompt import KernelFunctionFromPrompt
18-
from semantic_kernel.kernel import Kernel
1916

2017
###################################################################
2118
# The following sample demonstrates how to create a simple, #
@@ -24,117 +21,123 @@
2421
# complete a user's task. #
2522
###################################################################
2623

27-
28-
class ApprovalTerminationStrategy(TerminationStrategy):
29-
"""A strategy for determining when an agent should terminate."""
30-
31-
async def should_agent_terminate(self, agent, history):
32-
"""Check if the agent should terminate."""
33-
return "approved" in history[-1].content.lower()
34-
35-
24+
# Define agent names
3625
REVIEWER_NAME = "Reviewer"
37-
COPYWRITER_NAME = "Writer"
26+
WRITER_NAME = "Writer"
3827

3928

40-
def _create_kernel_with_chat_completion(service_id: str) -> Kernel:
29+
def create_kernel() -> Kernel:
30+
"""Creates a Kernel instance with an Azure OpenAI ChatCompletion service."""
4131
kernel = Kernel()
42-
kernel.add_service(AzureChatCompletion(service_id=service_id))
32+
kernel.add_service(service=AzureChatCompletion())
4333
return kernel
4434

4535

4636
async def main():
37+
# Create a single kernel instance for all agents.
38+
kernel = create_kernel()
39+
40+
# Create ChatCompletionAgents using the same kernel.
4741
agent_reviewer = ChatCompletionAgent(
4842
service_id=REVIEWER_NAME,
49-
kernel=_create_kernel_with_chat_completion(REVIEWER_NAME),
43+
kernel=kernel,
5044
name=REVIEWER_NAME,
5145
instructions="""
52-
Your responsibility is to review and identify how to improve user provided content.
53-
If the user has providing input or direction for content already provided, specify how to
54-
address this input.
55-
Never directly perform the correction or provide example.
56-
Once the content has been updated in a subsequent response, you will review the content
57-
again until satisfactory.
58-
Always copy satisfactory content to the clipboard using available tools and inform user.
59-
60-
RULES:
61-
- Only identify suggestions that are specific and actionable.
62-
- Verify previous suggestions have been addressed.
63-
- Never repeat previous suggestions.
64-
""",
46+
Your responsibility is to review and identify how to improve user provided content.
47+
If the user has provided input or direction for content already provided, specify how to address this input.
48+
Never directly perform the correction or provide an example.
49+
Once the content has been updated in a subsequent response, review it again until it is satisfactory.
50+
51+
RULES:
52+
- Only identify suggestions that are specific and actionable.
53+
- Verify previous suggestions have been addressed.
54+
- Never repeat previous suggestions.
55+
""",
6556
)
6657

6758
agent_writer = ChatCompletionAgent(
68-
service_id=COPYWRITER_NAME,
69-
kernel=_create_kernel_with_chat_completion(COPYWRITER_NAME),
70-
name=COPYWRITER_NAME,
59+
service_id=WRITER_NAME,
60+
kernel=kernel,
61+
name=WRITER_NAME,
7162
instructions="""
72-
Your sole responsibility is to rewrite content according to review suggestions.
73-
74-
- Always apply all review direction.
75-
- Always revise the content in its entirety without explanation.
76-
- Never address the user.
77-
""",
63+
Your sole responsibility is to rewrite content according to review suggestions.
64+
- Always apply all review directions.
65+
- Always revise the content in its entirety without explanation.
66+
- Never address the user.
67+
""",
7868
)
7969

70+
# Define a selection function to determine which agent should take the next turn.
8071
selection_function = KernelFunctionFromPrompt(
8172
function_name="selection",
8273
prompt=f"""
83-
Determine which participant takes the next turn in a conversation based on the the most recent participant.
84-
State only the name of the participant to take the next turn.
85-
No participant should take more than one turn in a row.
86-
87-
Choose only from these participants:
88-
- {REVIEWER_NAME}
89-
- {COPYWRITER_NAME}
90-
91-
Always follow these rules when selecting the next participant:
92-
- After user input, it is {COPYWRITER_NAME}'s turn.
93-
- After {COPYWRITER_NAME} replies, it is {REVIEWER_NAME}'s turn.
94-
- After {REVIEWER_NAME} provides feedback, it is {COPYWRITER_NAME}'s turn.
95-
96-
History:
97-
{{{{$history}}}}
98-
""",
74+
Examine the provided RESPONSE and choose the next participant.
75+
State only the name of the chosen participant without explanation.
76+
Never choose the participant named in the RESPONSE.
77+
78+
Choose only from these participants:
79+
- {REVIEWER_NAME}
80+
- {WRITER_NAME}
81+
82+
Rules:
83+
- If RESPONSE is user input, it is {REVIEWER_NAME}'s turn.
84+
- If RESPONSE is by {REVIEWER_NAME}, it is {WRITER_NAME}'s turn.
85+
- If RESPONSE is by {WRITER_NAME}, it is {REVIEWER_NAME}'s turn.
86+
87+
RESPONSE:
88+
{{{{$lastmessage}}}}
89+
""",
9990
)
10091

101-
TERMINATION_KEYWORD = "yes"
92+
# Define a termination function where the reviewer signals completion with "yes".
93+
termination_keyword = "yes"
10294

10395
termination_function = KernelFunctionFromPrompt(
10496
function_name="termination",
10597
prompt=f"""
106-
Examine the RESPONSE and determine whether the content has been deemed satisfactory.
107-
If content is satisfactory, respond with a single word without explanation: {TERMINATION_KEYWORD}.
108-
If specific suggestions are being provided, it is not satisfactory.
109-
If no correction is suggested, it is satisfactory.
110-
111-
RESPONSE:
112-
{{{{$history}}}}
113-
""",
98+
Examine the RESPONSE and determine whether the content has been deemed satisfactory.
99+
If the content is satisfactory, respond with a single word without explanation: {termination_keyword}.
100+
If specific suggestions are being provided, it is not satisfactory.
101+
If no correction is suggested, it is satisfactory.
102+
103+
RESPONSE:
104+
{{{{$lastmessage}}}}
105+
""",
114106
)
115107

108+
history_reducer = ChatHistoryTruncationReducer(target_count=5)
109+
110+
# Create the AgentGroupChat with selection and termination strategies.
116111
chat = AgentGroupChat(
117-
agents=[agent_writer, agent_reviewer],
112+
agents=[agent_reviewer, agent_writer],
118113
selection_strategy=KernelFunctionSelectionStrategy(
114+
initial_agent=agent_reviewer,
119115
function=selection_function,
120-
kernel=_create_kernel_with_chat_completion("selection"),
121-
result_parser=lambda result: str(result.value[0]) if result.value is not None else COPYWRITER_NAME,
122-
agent_variable_name="agents",
123-
history_variable_name="history",
116+
kernel=kernel,
117+
result_parser=lambda result: str(result.value[0]).strip() if result.value[0] is not None else WRITER_NAME,
118+
history_variable_name="lastmessage",
119+
history_reducer=history_reducer,
124120
),
125121
termination_strategy=KernelFunctionTerminationStrategy(
126122
agents=[agent_reviewer],
127123
function=termination_function,
128-
kernel=_create_kernel_with_chat_completion("termination"),
129-
result_parser=lambda result: TERMINATION_KEYWORD in str(result.value[0]).lower(),
130-
history_variable_name="history",
124+
kernel=kernel,
125+
result_parser=lambda result: termination_keyword in str(result.value[0]).lower(),
126+
history_variable_name="lastmessage",
131127
maximum_iterations=10,
128+
history_reducer=history_reducer,
132129
),
133130
)
134131

135-
is_complete: bool = False
132+
print(
133+
"Ready! Type your input, or 'exit' to quit, 'reset' to restart the conversation. "
134+
"You may pass in a file path using @<path_to_file>."
135+
)
136+
137+
is_complete = False
136138
while not is_complete:
137-
user_input = input("User:> ")
139+
print()
140+
user_input = input("User > ").strip()
138141
if not user_input:
139142
continue
140143

@@ -147,26 +150,35 @@ async def main():
147150
print("[Conversation has been reset]")
148151
continue
149152

150-
if user_input.startswith("@") and len(input) > 1:
151-
file_path = input[1:]
153+
# Try to grab files from the script's current directory
154+
if user_input.startswith("@") and len(user_input) > 1:
155+
file_name = user_input[1:]
156+
script_dir = os.path.dirname(os.path.abspath(__file__))
157+
file_path = os.path.join(script_dir, file_name)
152158
try:
153159
if not os.path.exists(file_path):
154160
print(f"Unable to access file: {file_path}")
155161
continue
156-
with open(file_path) as file:
162+
with open(file_path, encoding="utf-8") as file:
157163
user_input = file.read()
158164
except Exception:
159165
print(f"Unable to access file: {file_path}")
160166
continue
161167

168+
# Add the current user_input to the chat
162169
await chat.add_chat_message(ChatMessageContent(role=AuthorRole.USER, content=user_input))
163170

164-
async for response in chat.invoke():
165-
print(f"# {response.role} - {response.name or '*'}: '{response.content}'")
171+
try:
172+
async for response in chat.invoke():
173+
if response is None or not response.name:
174+
continue
175+
print()
176+
print(f"# {response.name.upper()}:\n{response.content}")
177+
except Exception as e:
178+
print(f"Error during chat invocation: {e}")
166179

167-
if chat.is_complete:
168-
is_complete = True
169-
break
180+
# Reset the chat's complete flag for the new conversation round.
181+
chat.is_complete = False
170182

171183

172184
if __name__ == "__main__":

‎python/samples/learn_resources/agent_docs/assistant_code.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,8 @@
44
import logging
55
import os
66

7-
from semantic_kernel.agents.open_ai.azure_assistant_agent import AzureAssistantAgent
8-
from semantic_kernel.contents.chat_message_content import ChatMessageContent
9-
from semantic_kernel.contents.streaming_file_reference_content import StreamingFileReferenceContent
10-
from semantic_kernel.contents.utils.author_role import AuthorRole
7+
from semantic_kernel.agents.open_ai import AzureAssistantAgent
8+
from semantic_kernel.contents import AuthorRole, ChatMessageContent, StreamingFileReferenceContent
119
from semantic_kernel.kernel import Kernel
1210

1311
logging.basicConfig(level=logging.ERROR)

‎python/samples/learn_resources/agent_docs/assistant_search.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@
33
import asyncio
44
import os
55

6-
from semantic_kernel.agents.open_ai.azure_assistant_agent import AzureAssistantAgent
7-
from semantic_kernel.contents.chat_message_content import ChatMessageContent
8-
from semantic_kernel.contents.streaming_annotation_content import StreamingAnnotationContent
9-
from semantic_kernel.contents.utils.author_role import AuthorRole
6+
from semantic_kernel.agents.open_ai import AzureAssistantAgent
7+
from semantic_kernel.contents import AuthorRole, ChatMessageContent, StreamingAnnotationContent
108
from semantic_kernel.kernel import Kernel
119

1210
###################################################################

‎python/samples/learn_resources/agent_docs/chat_agent.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@
88
from semantic_kernel.agents import ChatCompletionAgent
99
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
1010
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion
11-
from semantic_kernel.contents.chat_history import ChatHistory
12-
from semantic_kernel.contents.chat_message_content import ChatMessageContent
13-
from semantic_kernel.contents.utils.author_role import AuthorRole
11+
from semantic_kernel.contents import AuthorRole, ChatHistory, ChatMessageContent
1412
from semantic_kernel.functions.kernel_arguments import KernelArguments
1513
from semantic_kernel.kernel import Kernel
1614

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Women's suffrage is when women got the right to vote. A long time ago, only men could vote and make decisions. This was not fair because women should have the same rights as men. Women wanted to vote too, so they started asking for it. It took a long time, and they had to work very hard to make people listen to them. Many men did not think women should vote, and this made it very hard for the women.
2+
3+
The women who fought for voting were called suffragets. They did many things to show they wanted the right to vote. Some gave speeches, others made signs and marched in the streets. Some even went to jail because they refused to stop fighting for what they believed was right. It was scary for some of the women, but they knew how important it was to keep trying. They wanted to change the world so that it was more fair for everyone.
4+
5+
One of the most important suffragets was Susan B. Anthony. She worked very hard to help women get the right to vote. She gave speeches and wrote letters to the goverment to make them change the laws. Susan never gave up, even when people said mean things to her. Another important person was Elizabeth Cady Stanton. She also helped fight for women's rights and was friends with Susan B. Anthony. Together, they made a great team and helped make big changes.
6+
7+
Finally, in 1920, the 19th amendment was passed in the United States. This law gave women the right to vote. It was a huge victory for the suffragets, and they were very happy. Many women went to vote for the first time, and it felt like they were finally equal with men. It took many years and a lot of hard work, but the women never gave up. They kept fighting until they won.
8+
9+
Women's suffrage is very important because it shows that if you work hard and believe in something, you can make a change. The women who fought for the right to vote showed bravery and strengh, and they helped make the world a better place. Today, women can vote because of them, and it's important to remember their hard work. We should always stand up for what is right, just like the suffragets did.

‎python/semantic_kernel/agents/azure_ai/agent_content_generation.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -337,8 +337,8 @@ def generate_streaming_code_interpreter_content(
337337

338338
metadata: dict[str, bool] = {}
339339
for index, tool in enumerate(step_details.tool_calls):
340-
if isinstance(tool.type, RunStepDeltaCodeInterpreterToolCall):
341-
code_interpreter_tool_call: RunStepDeltaCodeInterpreterDetailItemObject = tool
340+
if isinstance(tool, RunStepDeltaCodeInterpreterDetailItemObject):
341+
code_interpreter_tool_call = tool
342342
if code_interpreter_tool_call.input:
343343
items.append(
344344
StreamingTextContent(
@@ -349,7 +349,11 @@ def generate_streaming_code_interpreter_content(
349349
metadata["code"] = True
350350
if code_interpreter_tool_call.outputs:
351351
for output in code_interpreter_tool_call.outputs:
352-
if isinstance(output, RunStepDeltaCodeInterpreterImageOutput) and output.image.file_id:
352+
if (
353+
isinstance(output, RunStepDeltaCodeInterpreterImageOutput)
354+
and output.image is not None
355+
and output.image.file_id
356+
):
353357
items.append(
354358
StreamingFileReferenceContent(
355359
file_id=output.image.file_id,

‎python/semantic_kernel/agents/azure_ai/agent_thread_actions.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -541,7 +541,8 @@ async def _stream_tool_outputs(
541541
if sub_event_type == AgentStreamEvent.THREAD_MESSAGE_DELTA:
542542
yield generate_streaming_message_content(agent.name, sub_event_data)
543543
elif sub_event_type == AgentStreamEvent.THREAD_RUN_COMPLETED:
544-
logger.info(f"Run completed with ID: {sub_event_data.id}")
544+
thread_run = cast(ThreadRun, sub_event_data)
545+
logger.info(f"Run completed with ID: {thread_run.id}")
545546
if active_messages:
546547
for msg_id, step in active_messages.items():
547548
message = await cls._retrieve_message(agent=agent, thread_id=thread_id, message_id=msg_id)
@@ -551,7 +552,7 @@ async def _stream_tool_outputs(
551552
messages.append(final_content)
552553
return
553554
elif sub_event_type == AgentStreamEvent.THREAD_RUN_FAILED:
554-
run_failed: ThreadRun = sub_event_data
555+
run_failed = cast(ThreadRun, sub_event_data)
555556
error_message = (
556557
run_failed.last_error.message if run_failed.last_error and run_failed.last_error.message else ""
557558
)

‎python/semantic_kernel/agents/bedrock/bedrock_agent.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from semantic_kernel.agents.bedrock.bedrock_agent_settings import BedrockAgentSettings
1919
from semantic_kernel.agents.bedrock.models.bedrock_agent_event_type import BedrockAgentEventType
2020
from semantic_kernel.agents.bedrock.models.bedrock_agent_model import BedrockAgentModel
21+
from semantic_kernel.agents.channels.agent_channel import AgentChannel
2122
from semantic_kernel.agents.channels.bedrock_agent_channel import BedrockAgentChannel
2223
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
2324
from semantic_kernel.contents.binary_content import BinaryContent
@@ -44,7 +45,7 @@ class BedrockAgent(BedrockAgentBase, Agent):
4445
Manages the interaction with Amazon Bedrock Agent Service.
4546
"""
4647

47-
channel_type: ClassVar[type[BedrockAgentChannel]] = BedrockAgentChannel
48+
channel_type: ClassVar[type[AgentChannel]] = BedrockAgentChannel
4849

4950
def __init__(
5051
self,
@@ -93,9 +94,9 @@ def __init__(
9394
raise AgentInitializationException("Failed to initialize the Amazon Bedrock Agent settings.") from e
9495

9596
bedrock_agent_model = BedrockAgentModel(
96-
agent_id=id,
97-
agent_name=name,
98-
foundation_model=bedrock_agent_settings.foundation_model,
97+
agentId=id,
98+
agentName=name,
99+
foundationModel=bedrock_agent_settings.foundation_model,
99100
)
100101

101102
prompt_template: PromptTemplateBase | None = None

0 commit comments

Comments
 (0)
Please sign in to comment.