Files
wehub-resource-sync b957a53def
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:21:23 +08:00

567 lines
21 KiB
Python

# Copyright (c) Microsoft. All rights reserved.
from collections.abc import Sequence
from typing import TYPE_CHECKING, Any
from openai import AsyncOpenAI
from openai.types.beta.threads.file_citation_annotation import FileCitationAnnotation
from openai.types.beta.threads.file_citation_delta_annotation import FileCitationDeltaAnnotation
from openai.types.beta.threads.file_path_annotation import FilePathAnnotation
from openai.types.beta.threads.file_path_delta_annotation import FilePathDeltaAnnotation
from openai.types.beta.threads.image_file_content_block import ImageFileContentBlock
from openai.types.beta.threads.image_file_delta_block import ImageFileDeltaBlock
from openai.types.beta.threads.message_delta_event import MessageDeltaEvent
from openai.types.beta.threads.runs import CodeInterpreterLogs
from openai.types.beta.threads.runs.code_interpreter_tool_call import CodeInterpreterOutputImage
from openai.types.beta.threads.text_content_block import TextContentBlock
from openai.types.beta.threads.text_delta_block import TextDeltaBlock
from semantic_kernel.contents.annotation_content import AnnotationContent
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.file_reference_content import FileReferenceContent
from semantic_kernel.contents.function_call_content import FunctionCallContent
from semantic_kernel.contents.function_result_content import FunctionResultContent
from semantic_kernel.contents.image_content import ImageContent
from semantic_kernel.contents.streaming_annotation_content import StreamingAnnotationContent
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
from semantic_kernel.contents.streaming_file_reference_content import StreamingFileReferenceContent
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.exceptions.agent_exceptions import AgentExecutionException
from semantic_kernel.utils.feature_stage_decorator import experimental
if TYPE_CHECKING:
from openai.types.beta.threads.message import Message
from openai.types.beta.threads.run import Run
from openai.types.beta.threads.runs import RunStep
from openai.types.beta.threads.runs.tool_call import ToolCall
from openai.types.beta.threads.runs.tool_calls_step_details import ToolCallsStepDetails
###################################################################
# The methods in this file are used with OpenAIAssistantAgent #
# related code. They are used to create chat messages, or #
# generate message content. #
###################################################################
@experimental
async def create_chat_message(
client: AsyncOpenAI,
thread_id: str,
message: "ChatMessageContent",
allowed_message_roles: Sequence[str] | None = None,
) -> "Message":
"""Class method to add a chat message, callable from class or instance.
Args:
client: The client to use for creating the message.
thread_id: The thread id.
message: The chat message.
allowed_message_roles: The allowed message roles.
Defaults to [AuthorRole.USER, AuthorRole.ASSISTANT] if None.
Providing an empty list will disallow all message roles.
Returns:
Message: The message.
"""
# Set the default allowed message roles if not provided
if allowed_message_roles is None:
allowed_message_roles = [AuthorRole.USER, AuthorRole.ASSISTANT]
if message.role.value not in allowed_message_roles and message.role != AuthorRole.TOOL:
raise AgentExecutionException(
f"Invalid message role `{message.role.value}`. Allowed roles are {allowed_message_roles}."
)
message_contents: list[dict[str, Any]] = get_message_contents(message=message)
return await client.beta.threads.messages.create(
thread_id=thread_id,
role="assistant" if message.role == AuthorRole.TOOL else message.role.value, # type: ignore
content=message_contents, # type: ignore
)
@experimental
def get_message_contents(message: "ChatMessageContent") -> list[dict[str, Any]]:
"""Get the message contents.
Args:
message: The message.
"""
contents: list[dict[str, Any]] = []
for content in message.items:
match content:
case TextContent():
# Make sure text is a string
final_text = content.text
if not isinstance(final_text, str):
if isinstance(final_text, (list, tuple)):
final_text = " ".join(map(str, final_text))
else:
final_text = str(final_text)
contents.append({"type": "text", "text": final_text})
case ImageContent():
if content.uri:
contents.append(content.to_dict())
case FileReferenceContent():
contents.append({
"type": "image_file",
"image_file": {"file_id": content.file_id},
})
case FunctionResultContent():
final_result = content.result
match final_result:
case str():
contents.append({"type": "text", "text": final_result})
case list() | tuple():
contents.append({"type": "text", "text": " ".join(map(str, final_result))})
case _:
contents.append({"type": "text", "text": str(final_result)})
return contents
@experimental
def generate_message_content(
assistant_name: str, message: "Message", completed_step: "RunStep | None" = None
) -> ChatMessageContent:
"""Generate message content."""
role = AuthorRole(message.role)
metadata = (
{
"created_at": completed_step.created_at,
"message_id": message.id, # message needs to be defined in context
"step_id": completed_step.id,
"run_id": completed_step.run_id,
"thread_id": completed_step.thread_id,
"assistant_id": completed_step.assistant_id,
"usage": completed_step.usage,
}
if completed_step is not None
else None
)
content: ChatMessageContent = ChatMessageContent(role=role, name=assistant_name, metadata=metadata) # type: ignore
for item_content in message.content:
if item_content.type == "text":
assert isinstance(item_content, TextContentBlock) # nosec
content.items.append(
TextContent(
text=item_content.text.value,
)
)
for annotation in item_content.text.annotations:
content.items.append(generate_annotation_content(annotation))
elif item_content.type == "image_file":
assert isinstance(item_content, ImageFileContentBlock) # nosec
content.items.append(
FileReferenceContent(
file_id=item_content.image_file.file_id,
)
)
return content
@experimental
def generate_streaming_message_content(
assistant_name: str,
message_delta_event: "MessageDeltaEvent",
completed_step: "RunStep | None" = None,
) -> StreamingChatMessageContent:
"""Generate streaming message content from a MessageDeltaEvent."""
delta = message_delta_event.delta
metadata = (
{
"created_at": completed_step.created_at,
"message_id": message_delta_event.id, # message needs to be defined in context
"step_id": completed_step.id,
"run_id": completed_step.run_id,
"thread_id": completed_step.thread_id,
"assistant_id": completed_step.assistant_id,
"usage": completed_step.usage,
}
if completed_step is not None
else None
)
# Determine the role
role = AuthorRole(delta.role) if delta.role is not None else AuthorRole("assistant")
items: list[StreamingTextContent | StreamingAnnotationContent | StreamingFileReferenceContent] = []
# Process each content block in the delta
for delta_block in delta.content or []:
if delta_block.type == "text":
assert isinstance(delta_block, TextDeltaBlock) # nosec
if delta_block.text and delta_block.text.value: # Ensure text is not None
text_value = delta_block.text.value
items.append(
StreamingTextContent(
text=text_value,
choice_index=delta_block.index,
)
)
# Process annotations if any
if delta_block.text.annotations:
for annotation in delta_block.text.annotations or []:
if isinstance(annotation, (FileCitationDeltaAnnotation, FilePathDeltaAnnotation)):
items.append(generate_streaming_annotation_content(annotation))
elif delta_block.type == "image_file":
assert isinstance(delta_block, ImageFileDeltaBlock) # nosec
if delta_block.image_file and delta_block.image_file.file_id:
file_id = delta_block.image_file.file_id
items.append(
StreamingFileReferenceContent(
file_id=file_id,
)
)
return StreamingChatMessageContent(role=role, name=assistant_name, items=items, choice_index=0, metadata=metadata) # type: ignore
@experimental
def generate_final_streaming_message_content(
assistant_name: str,
message: "Message",
completed_step: "RunStep | None" = None,
) -> StreamingChatMessageContent:
"""Generate streaming message content from a MessageDeltaEvent."""
metadata = (
{
"created_at": completed_step.created_at,
"message_id": message.id, # message needs to be defined in context
"step_id": completed_step.id,
"run_id": completed_step.run_id,
"thread_id": completed_step.thread_id,
"assistant_id": completed_step.assistant_id,
"usage": completed_step.usage,
}
if completed_step is not None
else None
)
# Determine the role
role = AuthorRole(message.role) if message.role is not None else AuthorRole("assistant")
items: list[StreamingTextContent | StreamingAnnotationContent | StreamingFileReferenceContent] = []
# Process each content block in the delta
for item_content in message.content:
if item_content.type == "text":
assert isinstance(item_content, TextContentBlock) # nosec
items.append(StreamingTextContent(text=item_content.text.value, choice_index=0))
for annotation in item_content.text.annotations:
items.append(generate_streaming_annotation_content(annotation))
elif item_content.type == "image_file":
assert isinstance(item_content, ImageFileContentBlock) # nosec
items.append(
StreamingFileReferenceContent(
file_id=item_content.image_file.file_id,
)
)
return StreamingChatMessageContent(role=role, name=assistant_name, items=items, choice_index=0, metadata=metadata) # type: ignore
@experimental
def merge_function_results(messages: list["ChatMessageContent"], name: str) -> "ChatMessageContent":
"""Combine multiple function result content types to one chat message content type.
This method combines the FunctionResultContent items from separate ChatMessageContent messages,
and is used in the event that the `context.terminate = True` condition is met.
Args:
messages: The list of chat messages.
name: The name of the agent.
Returns:
list[ChatMessageContent]: The combined chat message content.
"""
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.function_result_content import FunctionResultContent
items: list[Any] = []
for message in messages:
items.extend([item for item in message.items if isinstance(item, FunctionResultContent)])
return ChatMessageContent(
role=AuthorRole.TOOL,
items=items,
name=name,
)
@experimental
def merge_streaming_function_results(
messages: list["ChatMessageContent | StreamingChatMessageContent"],
name: str,
ai_model_id: str | None = None,
function_invoke_attempt: int | None = None,
) -> "StreamingChatMessageContent":
"""Combine multiple streaming function result content types to one streaming chat message content type.
This method combines the FunctionResultContent items from separate StreamingChatMessageContent messages,
and is used in the event that the `context.terminate = True` condition is met.
Args:
messages: The list of streaming chat message content types.
name: The name of the agent.
ai_model_id: The AI model ID.
function_invoke_attempt: The function invoke attempt.
Returns:
The combined streaming chat message content type.
"""
from semantic_kernel.contents.function_result_content import FunctionResultContent
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
items: list[Any] = []
for message in messages:
items.extend([item for item in message.items if isinstance(item, FunctionResultContent)])
return StreamingChatMessageContent(
name=name,
role=AuthorRole.TOOL,
items=items,
choice_index=0,
ai_model_id=ai_model_id,
function_invoke_attempt=function_invoke_attempt,
)
@experimental
def generate_function_call_content(agent_name: str, fccs: list[FunctionCallContent]) -> ChatMessageContent:
"""Generate function call content.
Args:
agent_name: The agent name.
fccs: The function call contents.
Returns:
ChatMessageContent: The chat message content containing the function call content as the items.
"""
return ChatMessageContent(role=AuthorRole.ASSISTANT, name=agent_name, items=fccs) # type: ignore
@experimental
def generate_function_result_content(
agent_name: str, function_step: FunctionCallContent, tool_call: "ToolCall"
) -> ChatMessageContent:
"""Generate function result content."""
function_call_content: ChatMessageContent = ChatMessageContent(role=AuthorRole.TOOL, name=agent_name) # type: ignore
function_call_content.items.append(
FunctionResultContent(
function_name=function_step.function_name,
plugin_name=function_step.plugin_name,
id=function_step.id,
result=tool_call.function.output, # type: ignore
)
)
return function_call_content
@experimental
def get_function_call_contents(run: "Run", function_steps: dict[str, FunctionCallContent]) -> list[FunctionCallContent]:
"""Extract function call contents from the run.
Args:
run: The run.
function_steps: The function steps
Returns:
The list of function call contents.
"""
function_call_contents: list[FunctionCallContent] = []
required_action = getattr(run, "required_action", None)
if not required_action or not getattr(required_action, "submit_tool_outputs", False):
return function_call_contents
for tool in required_action.submit_tool_outputs.tool_calls:
fcc = FunctionCallContent(
id=tool.id,
index=getattr(tool, "index", None),
name=tool.function.name,
arguments=tool.function.arguments,
)
function_call_contents.append(fcc)
function_steps[tool.id] = fcc
return function_call_contents
@experimental
def generate_code_interpreter_content(agent_name: str, code: str) -> "ChatMessageContent":
"""Generate code interpreter content.
Args:
agent_name: The agent name.
code: The code.
Returns:
ChatMessageContent: The chat message content.
"""
return ChatMessageContent(
role=AuthorRole.ASSISTANT,
content=code,
name=agent_name,
metadata={"code": True},
)
@experimental
def generate_streaming_function_content(
agent_name: str, step_details: "ToolCallsStepDetails"
) -> "StreamingChatMessageContent":
"""Generate streaming function content.
Args:
agent_name: The agent name.
step_details: The function step.
Returns:
StreamingChatMessageContent: The chat message content.
"""
items: list[FunctionCallContent] = []
for tool in step_details.tool_calls:
if tool.type == "function":
items.append(
FunctionCallContent(
id=tool.id,
index=getattr(tool, "index", None),
name=tool.function.name,
arguments=tool.function.arguments,
)
)
return (
StreamingChatMessageContent(
role=AuthorRole.ASSISTANT,
name=agent_name,
items=items, # type: ignore
choice_index=0,
)
if len(items) > 0
else None
)
@experimental
def generate_streaming_code_interpreter_content(
agent_name: str, step_details: "ToolCallsStepDetails"
) -> "StreamingChatMessageContent | None":
"""Generate code interpreter content.
Args:
agent_name: The agent name.
step_details: The current step details.
Returns:
StreamingChatMessageContent: The chat message content.
"""
items: list[StreamingTextContent | StreamingFileReferenceContent] = []
metadata: dict[str, bool] = {}
for index, tool in enumerate(step_details.tool_calls):
if tool.type == "code_interpreter":
if tool.code_interpreter.input:
items.append(
StreamingTextContent(
choice_index=index,
text=tool.code_interpreter.input,
)
)
metadata["code"] = True
if tool.code_interpreter.outputs:
for output in tool.code_interpreter.outputs:
if isinstance(output, CodeInterpreterOutputImage) and output.image.file_id:
items.append(
StreamingFileReferenceContent(
file_id=output.image.file_id,
)
)
if isinstance(output, CodeInterpreterLogs) and output.logs:
items.append(
StreamingTextContent(
choice_index=index,
text=output.logs,
)
)
return (
StreamingChatMessageContent(
role=AuthorRole.ASSISTANT,
name=agent_name,
items=items, # type: ignore
choice_index=0,
metadata=metadata if metadata else None,
)
if len(items) > 0
else None
)
@experimental
def generate_annotation_content(annotation: FileCitationAnnotation | FilePathAnnotation) -> AnnotationContent:
"""Generate annotation content."""
file_id = None
match annotation:
case FilePathAnnotation():
file_id = annotation.file_path.file_id
case FileCitationAnnotation():
file_id = annotation.file_citation.file_id
return AnnotationContent(
file_id=file_id,
quote=annotation.text,
start_index=annotation.start_index,
end_index=annotation.end_index,
)
@experimental
def generate_streaming_annotation_content(
annotation: FileCitationAnnotation | FilePathAnnotation | FilePathDeltaAnnotation | FileCitationDeltaAnnotation,
) -> StreamingAnnotationContent:
"""Generate streaming annotation content."""
file_id = None
match annotation:
case FilePathAnnotation():
file_id = annotation.file_path.file_id
case FileCitationAnnotation():
file_id = annotation.file_citation.file_id
case FilePathDeltaAnnotation():
file_id = annotation.file_path.file_id if annotation.file_path is not None else None
case FileCitationDeltaAnnotation():
file_id = annotation.file_citation.file_id if annotation.file_citation is not None else None
return StreamingAnnotationContent(
file_id=file_id,
quote=annotation.text,
start_index=annotation.start_index,
end_index=annotation.end_index,
)
@experimental
def generate_function_call_streaming_content(
agent_name: str,
fccs: list[FunctionCallContent],
) -> StreamingChatMessageContent:
"""Generate function call content.
Args:
agent_name: The agent name.
fccs: The function call contents.
Returns:
StreamingChatMessageContent: The chat message content containing the function call content as the items.
"""
return StreamingChatMessageContent(role=AuthorRole.ASSISTANT, choice_index=0, name=agent_name, items=fccs) # type: ignore