Skip to content

Python: Support a unified agent thread #11104

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 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
80 changes: 80 additions & 0 deletions python/samples/concepts/agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,83 @@

Concept samples can be run in an IDE or via the command line. After setting up the required api key or token authentication
for your AI connector, the samples run without any extra command line arguments.

## Managing Conversation Threads with AgentThread

This section explains how to manage conversation context using the `AgentThread` base class. Each agent has its own thread implementation that preserves the context of a conversation. If you invoke an agent without specifying a thread, a new one is created automatically and returned as part of the `AgentItemResponse` object—which includes both the message (of type `ChatMessageContent`) and the thread (`AgentThread`). You also have the option to create a custom thread for a specific agent by providing a unique `thread_id`.

## Overview

**Automatic Thread Creation:**
When an agent is invoked without a provided thread, it creates a new thread to manage the conversation context automatically.

**Manual Thread Management:**
You can explicitly create a specific implementation for the desired `Agent` that derives from the base clas `AgentThread`. You have the option to assign a `thread_id` to manage the conversation session. This is particularly useful in complex scenarios or multi-user environments.

Check warning on line 54 in python/samples/concepts/agents/README.md

View workflow job for this annotation

GitHub Actions / Spell Check with Typos

"clas" should be "class".

## Code Example

Below is a sample code snippet demonstrating thread management:

```python
USER_INPUTS = [
"Why is the sky blue?",
]

# 1. Create the agent by specifying the service
agent = ChatCompletionAgent(
service=AzureChatCompletion(),
name="Assistant",
instructions="Answer the user's questions.",
)

# 2. Create a thread to hold the conversation
# If no thread is provided, a new thread will be
# created and returned with the initial response
thread: ChatCompletionAgentThread = None

for user_input in USER_INPUTS:
print(f"# User: {user_input}")
# 3. Invoke the agent for a response
response = await agent.get_response(
message=user_input,
thread=thread,
)
print(f"# {response.message.name}: {response.message}")
thread = response.thread

# 4. Cleanup: Clear the thread
await thread.end() if thread else None

"""
Sample output:
# User: Hello, I am John Doe.
# Assistant: Hello, John Doe! How can I assist you today?
# User: What is your name?
# Assistant: I don't have a personal name like a human does, but you can call me Assistant.?
# User: What is my name?
# Assistant: You mentioned that your name is John Doe. How can I assist you further, John?
"""
```

## Detailed Explanation

**Thread Initialization:**
The thread is initially set to `None`. If no thread is provided, the agent creates a new one and includes it in the response.

**Processing User Inputs:**
A list of `user_inputs` simulates a conversation. For each input:
- The code prints the user's message.
- The agent is invoked using the `get_response` method, which returns the response asynchronously.

**Handling Responses:**
- The thread is updated with each response to maintain the conversation context.

**Cleanup:**
The code safely ends the thread if it exists.

## Conclusion

By leveraging the `AgentThread`, you ensure that each conversation maintains its context seamlessly -- whether the thread is automatically created or manually managed with a custom `thread_id`. This approach is crucial for developing agents that deliver coherent and context-aware interactions.



Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from autogen import ConversableAgent
from autogen.coding import LocalCommandLineCodeExecutor

from semantic_kernel.agents.autogen.autogen_conversable_agent import AutoGenConversableAgent
from semantic_kernel.agents.autogen import AutoGenConversableAgent, AutoGenConversableAgentThread

"""
The following sample demonstrates how to use the AutoGenConversableAgent to create a reply from an agent
Expand All @@ -17,6 +17,8 @@


async def main():
thread: AutoGenConversableAgentThread = None

# Create a temporary directory to store the code files.
import os

Expand All @@ -42,19 +44,27 @@ async def main():
message_with_code_block = """This is a message with code block.
The code block is below:
```python
import numpy as np
import matplotlib.pyplot as plt
x = np.random.randint(0, 100, 100)
y = np.random.randint(0, 100, 100)
plt.scatter(x, y)
plt.savefig('scatter.png')
print('Scatter plot saved to scatter.png')
def generate_fibonacci(max_val):
a, b = 0, 1
fibonacci_numbers = []
while a <= max_val:
fibonacci_numbers.append(a)
a, b = b, a + b
return fibonacci_numbers

if __name__ == "__main__":
fib_numbers = generate_fibonacci(101)
print(fib_numbers)
```
This is the end of the message.
"""

async for content in autogen_agent.invoke(message=message_with_code_block):
print(f"# {content.role} - {content.name or '*'}: '{content.content}'")
async for response in autogen_agent.invoke(message=message_with_code_block, thread=thread):
print(f"# {response.message.role} - {response.message.name or '*'}: '{response.message}'")
thread = response.thread

# Cleanup: Delete the thread and agent
await thread.end() if thread else None


if __name__ == "__main__":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from autogen import ConversableAgent, register_function

from semantic_kernel.agents.autogen.autogen_conversable_agent import AutoGenConversableAgent
from semantic_kernel.agents.autogen import AutoGenConversableAgent, AutoGenConversableAgentThread
from semantic_kernel.contents.function_call_content import FunctionCallContent
from semantic_kernel.contents.function_result_content import FunctionResultContent

Expand Down Expand Up @@ -50,6 +50,9 @@ def calculator(a: int, b: int, operator: Annotated[Operator, "operator"]) -> int
},
)

# Create a thread for use with the agent.
thread: AutoGenConversableAgentThread = None

# Create a Semantic Kernel AutoGenConversableAgent based on the AutoGen ConversableAgent.
assistant_agent = AutoGenConversableAgent(conversable_agent=assistant)

Expand All @@ -76,19 +79,26 @@ def calculator(a: int, b: int, operator: Annotated[Operator, "operator"]) -> int
# Create a Semantic Kernel AutoGenConversableAgent based on the AutoGen ConversableAgent.
user_proxy_agent = AutoGenConversableAgent(conversable_agent=user_proxy)

async for content in user_proxy_agent.invoke(
async for response in user_proxy_agent.invoke(
thread=thread,
recipient=assistant_agent,
message="What is (44232 + 13312 / (232 - 32)) * 5?",
max_turns=10,
):
for item in content.items:
for item in response.message.items:
match item:
case FunctionResultContent(result=r):
print(f"# {content.role} - {content.name or '*'}: '{r}'")
print(f"# {response.message.role} - {response.message.name or '*'}: '{r}'")
case FunctionCallContent(function_name=fn, arguments=arguments):
print(f"# {content.role} - {content.name or '*'}: Function Name: '{fn}', Arguments: '{arguments}'")
print(
f"# {response.message.role} - {response.message.name or '*'}: Function Name: '{fn}', Arguments: '{arguments}'" # noqa: E501
)
case _:
print(f"# {content.role} - {content.name or '*'}: '{content.content}'")
print(f"# {response.message.role} - {response.message.name or '*'}: '{response.message}'")
thread = response.thread

# Cleanup: Delete the thread and agent
await thread.end() if thread else None


if __name__ == "__main__":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from autogen import ConversableAgent

from semantic_kernel.agents.autogen.autogen_conversable_agent import AutoGenConversableAgent
from semantic_kernel.agents.autogen import AutoGenConversableAgent, AutoGenConversableAgentThread

"""
The following sample demonstrates how to use the AutoGenConversableAgent to create a conversation between two agents
Expand All @@ -17,6 +17,8 @@


async def main():
thread: AutoGenConversableAgentThread = None

cathy = ConversableAgent(
"cathy",
system_message="Your name is Cathy and you are a part of a duo of comedians.",
Expand Down Expand Up @@ -51,10 +53,14 @@ async def main():

joe_autogen_agent = AutoGenConversableAgent(conversable_agent=joe)

async for content in cathy_autogen_agent.invoke(
recipient=joe_autogen_agent, message="Tell me a joke about the stock market.", max_turns=3
async for response in cathy_autogen_agent.invoke(
recipient=joe_autogen_agent, message="Tell me a joke about the stock market.", thread=thread, max_turns=3
):
print(f"# {content.role} - {content.name or '*'}: '{content.content}'")
print(f"# {response.message.role} - {response.message.name or '*'}: '{response.message}'")
thread = response.thread

# Cleanup: Delete the thread and agent
await thread.end() if thread else None


if __name__ == "__main__":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from azure.ai.projects.models import AzureAISearchTool, ConnectionType
from azure.identity.aio import DefaultAzureCredential

from semantic_kernel.agents.azure_ai import AzureAIAgent, AzureAIAgentSettings
from semantic_kernel.agents.azure_ai import AzureAIAgent, AzureAIAgentSettings, AzureAIAgentThread

logging.basicConfig(level=logging.WARNING)

Expand Down Expand Up @@ -69,8 +69,10 @@ async def main() -> None:
definition=agent_definition,
)

# Create a new thread
thread = await client.agents.create_thread()
# Create a thread for the agent
# If no thread is provided, a new thread will be
# created and returned with the initial response
thread: AzureAIAgentThread = None

user_inputs = [
"Which hotels are available with full-sized kitchens in Nashville, TN?",
Expand All @@ -79,17 +81,14 @@ async def main() -> None:

try:
for user_input in user_inputs:
# Add the user input as a chat message
await agent.add_chat_message(
thread_id=thread.id,
message=user_input,
)
print(f"# User: '{user_input}'\n")
# Invoke the agent for the specified thread
async for content in agent.invoke(thread_id=thread.id):
print(f"# Agent: {content.content}\n")
async for response in agent.invoke(message=user_input, thread=thread):
print(f"# Agent: {response.message}\n")
thread = response.thread
finally:
await client.agents.delete_thread(thread.id)
# Cleanup: Delete the thread and agent
await thread.end() if thread else None
await client.agents.delete_agent(agent.id)

"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from azure.ai.projects.models import CodeInterpreterTool, FilePurpose
from azure.identity.aio import DefaultAzureCredential

from semantic_kernel.agents.azure_ai import AzureAIAgent, AzureAIAgentSettings
from semantic_kernel.agents.azure_ai import AzureAIAgent, AzureAIAgentSettings, AzureAIAgentThread
from semantic_kernel.contents.annotation_content import AnnotationContent
from semantic_kernel.contents.utils.author_role import AuthorRole

Expand Down Expand Up @@ -51,8 +51,10 @@ async def main() -> None:
definition=agent_definition,
)

# Create a new thread
thread = await client.agents.create_thread()
# Create a thread for the agent
# If no thread is provided, a new thread will be
# created and returned with the initial response
thread: AzureAIAgentThread = None

user_inputs = [
"Which segment had the most sales?",
Expand All @@ -62,18 +64,14 @@ async def main() -> None:

try:
for user_input in user_inputs:
# Add the user input as a chat message
await agent.add_chat_message(
thread_id=thread.id,
message=user_input,
)
print(f"# User: '{user_input}'")
# Invoke the agent for the specified thread
async for content in agent.invoke(thread_id=thread.id):
if content.role != AuthorRole.TOOL:
print(f"# Agent: {content.content}")
if len(content.items) > 0:
for item in content.items:
# Invoke the agent for the specified user input
async for response in agent.invoke(message=user_input, thread=thread):
if response.message.role != AuthorRole.TOOL:
print(f"# Agent: {response.message}")
if len(response.message.items) > 0:
for item in response.message.items:
# Show Annotation Content if it exist
if isinstance(item, AnnotationContent):
print(f"\n`{item.quote}` => {item.file_id}")
response_content = await client.agents.get_file_content(file_id=item.file_id)
Expand All @@ -82,8 +80,10 @@ async def main() -> None:
content_bytes.extend(chunk)
tab_delimited_text = content_bytes.decode("utf-8")
print(tab_delimited_text)
thread = response.thread
finally:
await client.agents.delete_thread(thread.id)
# Cleanup: Delete the thread and agent
await thread.end() if thread else None
await client.agents.delete_agent(agent.id)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from azure.identity.aio import DefaultAzureCredential

from semantic_kernel.agents.azure_ai import AzureAIAgent, AzureAIAgentSettings
from semantic_kernel.agents.azure_ai import AzureAIAgent, AzureAIAgentSettings, AzureAIAgentThread
from semantic_kernel.functions import kernel_function


Expand Down Expand Up @@ -55,8 +55,10 @@ async def main() -> None:
plugins=[MenuPlugin()], # add the sample plugin to the agent
)

# Create a new thread
thread = await client.agents.create_thread()
# Create a thread for the agent
# If no thread is provided, a new thread will be
# created and returned with the initial response
thread: AzureAIAgentThread = None

user_inputs = [
"Hello",
Expand All @@ -67,21 +69,18 @@ async def main() -> None:

try:
for user_input in user_inputs:
# Add the user input as a chat message
await agent.add_chat_message(
thread_id=thread.id,
message=user_input,
)
print(f"# User: '{user_input}'")
first_chunk = True
async for content in agent.invoke_stream(thread_id=thread.id):
async for response in agent.invoke_stream(message=user_input, thread=thread):
if first_chunk:
print(f"# {content.role}: ", end="", flush=True)
print(f"# {response.message.role}: ", end="", flush=True)
first_chunk = False
print(content.content, end="", flush=True)
print(response.message.content, end="", flush=True)
thread = response.thread
print()
finally:
await client.agents.delete_thread(thread.id)
# Cleanup: Delete the thread and agent
await thread.end() if thread else None
await client.agents.delete_agent(agent.id)


Expand Down
Loading
Loading