iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🗂

Trying out Microsoft Agent Framework with Local LLMs: Part 5 (AIFunction)

に公開

Series Overview

List

Introduction

In Part 4 of trying Microsoft Agent Framework with a local LLM, I implemented a Human-in-the-loop configuration. In Part 5, I will try creating a custom AIFunction.

Creating a Custom AIFunction

public class GetSampleDataFunction : AIFunction
{
    private string jsonString = """
        {
            "type": "object",
            "properties": {
                "inputData": {
                    "type": "string",
                    "enum": ["small", "medium", "large"]
                }
            },
            "required": ["inputData"]
        }
        """;

    private string returnJsonString = """
        {
            "type": "object",
            "properties": {
                "items": {
                    "type": "array",
                    "items": {
                        "type": "string"
                    }
                }
            },
            "required": ["items"]
        }
        """;

    public override string Name => "GetSampleData";
    public override string Description => "Retrieves sample data.";

    public override JsonElement JsonSchema => JsonDocument.Parse(jsonString).RootElement;

    public override JsonElement? ReturnJsonSchema => JsonDocument.Parse(returnJsonString).RootElement;

    protected override ValueTask<object?> InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken)
    {
        string? inputData = null;

        if (arguments.TryGetValue("inputData", out var value))
        {
            if (value is JsonElement jsonElement)
            {
                inputData = jsonElement.GetString();
            }
            else if (value is string str)
            {
                inputData = str;
            }
        }

        if (string.IsNullOrEmpty(inputData))
        {
            return new ValueTask<object?>("error");
        }

        var items = inputData switch
        {
            "small" => new[] { "small_item_1", "small_item_2", "small_item_3" },
            "medium" => new[] { "medium_item_1", "medium_item_2", "medium_item_3", "medium_item_4" },
            "large" => new[] { "large_item_1", "large_item_2", "large_item_3", "large_item_4", "large_item_5" },
            _ => new[] { "default_item_1", "default_item_2" }
        };

        var result = new { items = items };
        var json = JsonSerializer.Serialize(result);

        return new ValueTask<object?>(json);
    }
}
  • I define the input and output schemas using JsonSchema and ReturnJsonSchema.
    • The JsonSchema and ReturnJsonSchema in Microsoft.Extensions.AI represent JSON Schema.
    • They are used to define the input and output formats based on OpenAPI specifications.
  • In the InvokeCoreAsync method, I return different sample data depending on the value of inputData.
  • I use TryGetValue to retrieve arguments, handling cases where values might be missing or have different types.
  • When the agent calls this function as a tool, it is expected to pass one of "small", "medium", or "large" to inputData.
  • The return value is an object containing items, which is serialized into JSON and passed to the model as the result of the AIFunction.

Registering as a Tool in AIAgent

AIAgent agent = chatClient.AsAIAgent(
    tools: [
    new GetSampleDataFunction()
  ]);

AgentSession session = await agent.CreateSessionAsync();
AgentResponse response = await agent.RunAsync("Show me some sample data", session);
Console.WriteLine(response);
  • Just like with standard AIFunctions, you create an AIAgent by registering the custom GetSampleDataFunction in the tools list.
  • By passing the session created with CreateSessionAsync to RunAsync, you can maintain the conversation history across subsequent turns.

Request to LM Studio

1st Turn

 {
  "messages": [
    {
      "role": "user",
      "content": "Show me some sample data"
    }
  ],
  "model": "openai/gpt-oss-20b",
  "tools": [
    {
      "type": "function",
      "function": {
        "description": "Retrieves sample data.",
        "name": "GetSampleData",
        "parameters": {
          "type": "object",
          "required": [
            "inputData"
          ],
          "properties": {
            "inputData": {
              "type": "string",
              "enum": [
                "small",
                "medium",
                "large"
              ]
            }
          },
          "additionalProperties": false
        }
      }
    }
  ],
  "tool_choice": "auto"
}
  • You can see that the custom AIFunction is correctly recognized as a tool.
    • The Name, Description, and JsonSchema are correctly reflected.
    • The schema defined in JsonSchema is included in parameters.
      • The enum choices defined are also included in the parameters.

2nd Turn

 {
  "messages": [
    {
      "role": "user",
      "content": "Show me some sample data"
    },
    {
      "role": "assistant",
      "content": "",
      "tool_calls": [
        {
          "id": "980090291",
          "type": "function",
          "function": {
            "name": "GetSampleData",
            "arguments": "{\r\n  \"inputData\": \"medium\"\r\n}"
          }
        }
      ]
    },
    {
      "role": "tool",
      "tool_call_id": "980090291",
      "content": "{\"items\":[\"medium_item_1\",\"medium_item_2\",\"medium_item_3\",\"medium_item_4\"]}"
    }
  ],
  "model": "openai/gpt-oss-20b",
  "tools": [
    {
      "type": "function",
      "function": {
        "description": "Retrieves sample data.",
        "name": "GetSampleData",
        "parameters": {
          "type": "object",
          "required": [
            "inputData"
          ],
          "properties": {
            "inputData": {
              "type": "string",
              "enum": [
                "small",
                "medium",
                "large"
              ]
            }
          },
          "additionalProperties": false
        }
      }
    }
  ],
  "tool_choice": "auto"
}
  • You can see that the tool call request and response are performed correctly.
    • The assistant's tool_calls includes the call to the GetSampleData function.
    • The tool's content contains the return value of the GetSampleData function.
  • The tool's input parameters are passed correctly.
    • You can see that "medium" is passed as the value for inputData in arguments.
  • While I return an object in the code, you can see that the tool result passed to LM Studio is represented as JSON text.

Output

Here is the retrieved sample data for the "medium" size.

| Index | Item Name |
|--------------|----------------------|
| 1 | medium_item_1 |
| 2 | medium_item_2 |
| 3 | medium_item_3 |
| 4 | medium_item_4 |

This is the sample data returned from the `GetSampleData` function. Please let me know if you would like to see other sizes (small/large) or have it displayed in a different format!
  • The tool execution result is correctly reflected in the agent's response.
    • Based on the JSON representation passed as the tool result, the contents of items are displayed as a Markdown table.
  • It is clear that the agent understands the choices defined in the enum.

Summary

  • I implemented a custom tool by inheriting from AIFunction.
  • I defined the tool's input format using JsonSchema.
  • I registered it as a tool in AIAgent and confirmed that tool calling works by inspecting the requests to LM Studio.
  • Next time, I would like to delve into the LLM input parameters or the chat history mechanism.

Discussion