Skip to content

Commit 1c11258

Browse files
Python: added af_tool bridge for kernel_functions (#13227)
### 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. --> Adds a new method to KernelFunction that creates a Agent Framework Tool from that function. Does require the user to install agent-framework-core themselves. ### Description <!-- 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 2651307 commit 1c11258

File tree

5 files changed

+1705
-1708
lines changed

5 files changed

+1705
-1708
lines changed

python/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ dependencies = [
3737
"numpy >= 1.25.0; python_version < '3.12'",
3838
"numpy >= 1.26.0; python_version >= '3.12'",
3939
# openai connector
40-
"openai >= 1.98.0, < 2.0.0",
40+
"openai >= 1.98.0,<2",
4141
# openapi and swagger
4242
"openapi_core >= 0.18,<0.20",
4343
"websockets >= 13, < 16",
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Copyright (c) Microsoft. All rights reserved.
2+
3+
import asyncio
4+
5+
from agent_framework.openai import OpenAIResponsesClient
6+
7+
from semantic_kernel import Kernel
8+
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion, OpenAIChatPromptExecutionSettings
9+
from semantic_kernel.core_plugins import TimePlugin
10+
from semantic_kernel.functions import KernelFunctionFromPrompt
11+
from semantic_kernel.prompt_template import KernelPromptTemplate, PromptTemplateConfig
12+
13+
"""
14+
This example demonstrates how to create an agent framework tool from a kernel function
15+
that uses a prompt template with plugin functions. The tool is then used by an Agent
16+
Framework Agent to answer a question about the current time and date.
17+
18+
19+
This sample requires manually installing the `agent-framework-core` package.
20+
21+
```bash
22+
pip install agent-framework-core --pre
23+
```
24+
or with uv:
25+
```bash
26+
uv pip install agent-framework-core --prerelease=allow
27+
```
28+
"""
29+
30+
31+
async def main():
32+
kernel = Kernel()
33+
34+
service_id = "template_language"
35+
kernel.add_service(
36+
OpenAIChatCompletion(service_id=service_id),
37+
)
38+
39+
kernel.add_plugin(TimePlugin(), "time")
40+
41+
function_definition = """
42+
Today is: {{time.date}}
43+
Current time is: {{time.time}}
44+
45+
Answer to the following questions using JSON syntax, including the data used.
46+
Is it morning, afternoon, evening, or night (morning/afternoon/evening/night)?
47+
Is it weekend time (weekend/not weekend)?
48+
"""
49+
50+
print("--- Rendered Prompt ---")
51+
prompt_template_config = PromptTemplateConfig(template=function_definition)
52+
prompt_template = KernelPromptTemplate(prompt_template_config=prompt_template_config)
53+
rendered_prompt = await prompt_template.render(kernel, arguments=None)
54+
print(rendered_prompt)
55+
56+
function = KernelFunctionFromPrompt(
57+
description="Determine the kind of day based on the current time and date.",
58+
plugin_name="TimePlugin",
59+
prompt_execution_settings=OpenAIChatPromptExecutionSettings(service_id=service_id, max_tokens=100),
60+
function_name="kind_of_day",
61+
prompt_template=prompt_template,
62+
).as_agent_framework_tool(kernel=kernel)
63+
64+
print("--- Prompt Function Result ---")
65+
response = await (
66+
OpenAIResponsesClient(model_id="gpt-5-nano").create_agent(tools=function).run("What kind of day is it?")
67+
)
68+
print(response.text)
69+
70+
71+
if __name__ == "__main__":
72+
asyncio.run(main())

python/semantic_kernel/connectors/ai/open_ai/services/open_ai_handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ async def _send_image_edit_request(
150150
try:
151151
response: ImagesResponse = await self.client.images.edit(
152152
image=image,
153-
mask=mask,
153+
mask=mask, # type: ignore
154154
**settings.prepare_settings_dict(),
155155
)
156156
self.store_usage(response)

python/semantic_kernel/functions/kernel_function.py

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from opentelemetry import metrics, trace
1313
from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
14-
from pydantic import Field
14+
from pydantic import BaseModel, Field
1515

1616
from semantic_kernel.filters.filter_types import FilterTypes
1717
from semantic_kernel.filters.functions.function_invocation_context import FunctionInvocationContext
@@ -35,6 +35,9 @@
3535
from semantic_kernel.utils.telemetry.model_diagnostics import function_tracer
3636
from semantic_kernel.utils.telemetry.model_diagnostics.gen_ai_attributes import TOOL_CALL_ARGUMENTS, TOOL_CALL_RESULT
3737

38+
from ..contents.chat_message_content import ChatMessageContent
39+
from ..contents.text_content import TextContent
40+
3841
if TYPE_CHECKING:
3942
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
4043
from semantic_kernel.contents.streaming_content_mixin import StreamingContentMixin
@@ -405,3 +408,76 @@ def _handle_exception(self, current_span: trace.Span, exception: Exception, attr
405408
current_span.set_status(trace.StatusCode.ERROR, description=str(exception))
406409

407410
KernelFunctionLogMessages.log_function_error(logger, exception)
411+
412+
def as_agent_framework_tool(
413+
self,
414+
*,
415+
name: str | None = None,
416+
description: str | None = None,
417+
kernel: "Kernel | None" = None,
418+
) -> Any:
419+
"""Convert the function to an agent framework tool.
420+
421+
Args:
422+
name: The name of the tool, if None, the function name is used.
423+
description: The description of the tool, if None, the tool description is used.
424+
kernel: The kernel to use, if None, a kernel is created.
425+
426+
Returns:
427+
AIFunction: The agent framework tool.
428+
"""
429+
import json
430+
431+
from pydantic import Field, create_model
432+
433+
from semantic_kernel.kernel import Kernel
434+
435+
try:
436+
from agent_framework import AIFunction
437+
438+
except ImportError as e:
439+
raise ImportError(
440+
"agent_framework is not installed. Please install it with 'pip install agent-framework-core'"
441+
) from e
442+
443+
if not kernel:
444+
kernel = Kernel()
445+
name = name or self.name
446+
description = description or self.description
447+
fields = {}
448+
for param in self.parameters:
449+
if param.include_in_function_choices:
450+
if param.default_value is not None:
451+
fields[param.name] = (
452+
param.type_,
453+
Field(description=param.description, default=param.default_value),
454+
)
455+
fields[param.name] = (param.type_, Field(description=param.description))
456+
input_model = create_model("InputModel", **fields) # type: ignore
457+
458+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
459+
result = await self.invoke(kernel, *args, **kwargs)
460+
if result and result.value is not None:
461+
if isinstance(result.value, list):
462+
results: list[Any] = []
463+
for value in result.value:
464+
if isinstance(value, ChatMessageContent):
465+
results.append(str(value))
466+
continue
467+
if isinstance(value, TextContent):
468+
results.append(value.text)
469+
continue
470+
if isinstance(value, BaseModel):
471+
results.append(value.model_dump())
472+
continue
473+
results.append(value)
474+
return json.dumps(results) if len(results) > 1 else json.dumps(results[0])
475+
return json.dumps(result.value)
476+
return "The function did not return a result."
477+
478+
return AIFunction(
479+
name=name,
480+
description=description,
481+
input_model=input_model,
482+
func=wrapper,
483+
)

0 commit comments

Comments
 (0)