Skip to content

Commit 1c9b166

Browse files
authored
Python: Fix issues of Gemini parallel function call (microsoft#10813)
### Motivation and Context <!-- 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. --> Closing microsoft#10597 ### Description <!-- Describe your changes, the overall approach, the underlying design. These notes will help understanding how your code works. Thanks! --> 1. Collapse consecutive tool call messages into a single tool message with multiple function result items before sending the chat history to the API. 2. Add unit test. ### 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 e1184ae commit 1c9b166

5 files changed

Lines changed: 80 additions & 1 deletion

File tree

python/semantic_kernel/connectors/ai/google/google_ai/services/google_ai_chat_completion.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
update_settings_from_function_choice_configuration,
3333
)
3434
from semantic_kernel.connectors.ai.google.shared_utils import (
35+
collapse_function_call_results_in_chat_history,
3536
filter_system_message,
3637
format_gemini_function_name_to_kernel_function_fully_qualified_name,
3738
)
@@ -134,6 +135,8 @@ async def _inner_get_chat_message_contents(
134135
system_instruction=filter_system_message(chat_history),
135136
)
136137

138+
collapse_function_call_results_in_chat_history(chat_history)
139+
137140
response: AsyncGenerateContentResponse = await model.generate_content_async(
138141
contents=self._prepare_chat_history_for_request(chat_history),
139142
generation_config=GenerationConfig(**settings.prepare_settings_dict()),
@@ -163,6 +166,8 @@ async def _inner_get_streaming_chat_message_contents(
163166
system_instruction=filter_system_message(chat_history),
164167
)
165168

169+
collapse_function_call_results_in_chat_history(chat_history)
170+
166171
response: AsyncGenerateContentResponse = await model.generate_content_async(
167172
contents=self._prepare_chat_history_for_request(chat_history),
168173
generation_config=GenerationConfig(**settings.prepare_settings_dict()),

python/semantic_kernel/connectors/ai/google/google_ai/services/utils.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,9 @@ def kernel_function_metadata_to_google_ai_function_call_format(metadata: KernelF
145145
"type": "object",
146146
"properties": {param.name: param.schema_data for param in metadata.parameters},
147147
"required": [p.name for p in metadata.parameters if p.is_required],
148-
},
148+
}
149+
if metadata.parameters
150+
else None,
149151
}
150152

151153

python/semantic_kernel/connectors/ai/google/shared_utils.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,27 @@ def format_gemini_function_name_to_kernel_function_fully_qualified_name(gemini_f
4747
plugin_name, function_name = gemini_function_name.split(GEMINI_FUNCTION_NAME_SEPARATOR, 1)
4848
return f"{plugin_name}{DEFAULT_FULLY_QUALIFIED_NAME_SEPARATOR}{function_name}"
4949
return gemini_function_name
50+
51+
52+
def collapse_function_call_results_in_chat_history(chat_history: ChatHistory):
53+
"""The Gemini API expects the results of parallel function calls to be contained in a single message to be returned.
54+
55+
This helper method collapses the results of parallel function calls in the chat history into a single Tool message.
56+
57+
Since this method in an internal method that is supposed to be called only by the Google AI and Vertex AI
58+
connectors, it is safe to assume that the chat history contains a correct sequence of messages, i.e. there won't be
59+
cases where the assistant wants to call 2 functions in parallel but there are more than 2 function results following
60+
the assistant message.
61+
"""
62+
if not chat_history.messages:
63+
return
64+
65+
current_idx = 1
66+
while current_idx < len(chat_history):
67+
previous_message = chat_history[current_idx - 1]
68+
current_message = chat_history[current_idx]
69+
if previous_message.role == AuthorRole.TOOL and current_message.role == AuthorRole.TOOL:
70+
previous_message.items.extend(current_message.items)
71+
chat_history.remove_message(current_message)
72+
else:
73+
current_idx += 1

python/semantic_kernel/connectors/ai/google/vertex_ai/services/vertex_ai_chat_completion.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from semantic_kernel.connectors.ai.completion_usage import CompletionUsage
1919
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceType
2020
from semantic_kernel.connectors.ai.google.shared_utils import (
21+
collapse_function_call_results_in_chat_history,
2122
filter_system_message,
2223
format_gemini_function_name_to_kernel_function_fully_qualified_name,
2324
)
@@ -129,6 +130,8 @@ async def _inner_get_chat_message_contents(
129130
system_instruction=filter_system_message(chat_history),
130131
)
131132

133+
collapse_function_call_results_in_chat_history(chat_history)
134+
132135
response: GenerationResponse = await model.generate_content_async(
133136
contents=self._prepare_chat_history_for_request(chat_history),
134137
generation_config=settings.prepare_settings_dict(),
@@ -156,6 +159,8 @@ async def _inner_get_streaming_chat_message_contents(
156159
system_instruction=filter_system_message(chat_history),
157160
)
158161

162+
collapse_function_call_results_in_chat_history(chat_history)
163+
159164
response: AsyncIterable[GenerationResponse] = await model.generate_content_async(
160165
contents=self._prepare_chat_history_for_request(chat_history),
161166
generation_config=settings.prepare_settings_dict(),

python/tests/unit/connectors/ai/google/test_shared_utils.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,15 @@
77
from semantic_kernel.connectors.ai.google.shared_utils import (
88
FUNCTION_CHOICE_TYPE_TO_GOOGLE_FUNCTION_CALLING_MODE,
99
GEMINI_FUNCTION_NAME_SEPARATOR,
10+
collapse_function_call_results_in_chat_history,
1011
filter_system_message,
1112
format_gemini_function_name_to_kernel_function_fully_qualified_name,
1213
)
1314
from semantic_kernel.contents.chat_history import ChatHistory
15+
from semantic_kernel.contents.chat_message_content import ChatMessageContent
16+
from semantic_kernel.contents.function_call_content import FunctionCallContent
17+
from semantic_kernel.contents.function_result_content import FunctionResultContent
18+
from semantic_kernel.contents.utils.author_role import AuthorRole
1419
from semantic_kernel.exceptions.service_exceptions import ServiceInvalidRequestError
1520

1621

@@ -51,3 +56,41 @@ def test_format_gemini_function_name_to_kernel_function_fully_qualified_name() -
5156
# Doesn't contain the separator
5257
gemini_function_name = "function"
5358
assert format_gemini_function_name_to_kernel_function_fully_qualified_name(gemini_function_name) == "function"
59+
60+
61+
def test_collapse_function_call_results_in_chat_history() -> None:
62+
chat_history = ChatHistory()
63+
chat_history.extend([
64+
ChatMessageContent(
65+
AuthorRole.ASSISTANT,
66+
items=[
67+
FunctionCallContent(id="function1", name="function1"),
68+
FunctionCallContent(id="function2", name="function2"),
69+
],
70+
),
71+
# The following two messages should be collapsed into a single message
72+
ChatMessageContent(
73+
AuthorRole.TOOL,
74+
items=[FunctionResultContent(id="function1", name="function1", result="result1")],
75+
),
76+
ChatMessageContent(
77+
AuthorRole.TOOL,
78+
items=[FunctionResultContent(id="function2", name="function2", result="result2")],
79+
),
80+
ChatMessageContent(AuthorRole.ASSISTANT, content="Assistant message"),
81+
ChatMessageContent(AuthorRole.USER, content="User message"),
82+
ChatMessageContent(
83+
AuthorRole.ASSISTANT,
84+
items=[FunctionCallContent(id="function3", name="function3")],
85+
),
86+
ChatMessageContent(
87+
AuthorRole.TOOL,
88+
items=[FunctionResultContent(id="function3", name="function3", result="result3")],
89+
),
90+
ChatMessageContent(AuthorRole.ASSISTANT, content="Assistant message"),
91+
])
92+
93+
assert len(chat_history.messages) == 8
94+
collapse_function_call_results_in_chat_history(chat_history)
95+
assert len(chat_history.messages) == 7
96+
assert len(chat_history.messages[1].items) == 2

0 commit comments

Comments
 (0)