Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

using System.Collections.Generic;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.AI;

namespace Microsoft.Agents.AI.LocalCodeAct.Internal;
Expand Down Expand Up @@ -40,6 +41,24 @@ public static string BuildExecuteCodeDescription(
}

sb.AppendLine();

JsonElement schema = tool.JsonSchema;
if (schema.ValueKind is JsonValueKind.Object
&& schema.TryGetProperty("properties", out var properties)
&& properties.ValueKind is JsonValueKind.Object)
{
if (properties.EnumerateObject().MoveNext())
{
sb.AppendLine(" Parameters (JSON Schema):");
sb.Append(" ");
sb.Append(schema.GetRawText());
sb.AppendLine();
}
else
{
sb.AppendLine(" Parameters: none.");
}
}
}
}

Expand Down
16 changes: 16 additions & 0 deletions dotnet/src/Microsoft.Agents.AI.LocalCodeAct/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,22 @@ total = await call_tool("add", a=2, b=3)
print(total)
```

The `execute_code` description lists each registered tool with its existing
`AIFunction.JsonSchema`, so the model can see parameter names, requiredness,
descriptions, enums, and nested shapes and call `call_tool(...)` correctly.
Tools that do not describe their input are listed by name only.

Two things to keep in mind:

* Parameter names, descriptions, defaults, and enum values are **model-visible**.
Do not put credentials, tenant identifiers, or other secrets in them.
* C# enums serialize as integers by default, so their member names do not reach
the schema. Apply `JsonStringEnumConverter` to the enum if the model should see
the allowed values.

This does not change `execute_code`'s own input schema, which remains the single
`code` parameter.

## Tool Approval

Generated code reaches registered tools through `call_tool(...)`, which invokes
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Collections.Generic;
using System.ComponentModel;
using Microsoft.Agents.AI.LocalCodeAct.Internal;
using Microsoft.Extensions.AI;

Expand All @@ -24,6 +25,48 @@ public void BuildExecuteCodeDescription_MentionsToolsWhenProvided()
Assert.Contains("get_weather", description);
}

[Fact]
public void BuildExecuteCodeDescription_WithToolParameters_IncludesParameterMetadata()
{
static string Lookup(
[Description("Search text")] string query,
[Description("Maximum results")] int limit = 10) => $"{query}:{limit}";

var tool = AIFunctionFactory.Create(Lookup, name: "lookup", description: "Look up an item.");
var description = InstructionBuilder.BuildExecuteCodeDescription(
new List<AIFunction> { tool },
new List<FileMount>());

Assert.Contains("lookup", description);
Assert.Contains("query", description);
Assert.Contains("Search text", description);
Assert.Contains("Maximum results", description);
Assert.Contains("\"required\":[\"query\"]", description);
Assert.Contains("\"default\":10", description);
}

[Fact]
public void BuildExecuteCodeDescription_WithZeroParameterTool_ReportsNoParameters()
{
var tool = AIFunctionFactory.Create(() => "ok", name: "ping", description: "Pings.");
var description = InstructionBuilder.BuildExecuteCodeDescription(
new List<AIFunction> { tool },
new List<FileMount>());

Assert.Contains("Parameters: none.", description);
Assert.DoesNotContain("Parameters (JSON Schema)", description);
}

[Fact]
public void BuildExecuteCodeDescription_WithUnconstrainedSchema_OmitsParameterBlock()
{
var tools = new List<AIFunction> { new TestTool("legacy_tool", "Does not describe its input.") };
var description = InstructionBuilder.BuildExecuteCodeDescription(tools, new List<FileMount>());

Assert.Contains("legacy_tool", description);
Assert.DoesNotContain("Parameters", description);
}

[Fact]
public void BuildExecuteCodeDescription_MentionsMountsWhenProvided()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,36 @@ public async Task ProvideAIContextAsync_ReturnsExecuteCodeToolAndInstructionsAsy
Assert.False(string.IsNullOrWhiteSpace(context.Instructions));
}

[Fact]
public async Task ProvideAIContextAsync_WithParameterizedTool_CarriesHostToolSchemaAsync()
{
var tool = AIFunctionFactory.Create((string query) => "ok", name: "lookup");
var options = Options();
options.Tools = [tool];
using var provider = new LocalCodeActProvider("/usr/bin/python3", options);

var context = await provider.InvokingAsync(NewInvokingContext());
var function = Assert.IsAssignableFrom<AIFunction>(context!.Tools!.First());

Assert.Contains("query", function.Description);
}

[Fact]
public async Task ProvideAIContextAsync_WithParameterizedTool_KeepsCodeOnlyInputSchemaAsync()
{
var tool = AIFunctionFactory.Create((string query) => "ok", name: "lookup");
var options = Options();
options.Tools = [tool];
using var provider = new LocalCodeActProvider("/usr/bin/python3", options);

var context = await provider.InvokingAsync(NewInvokingContext());
var function = Assert.IsAssignableFrom<AIFunction>(context!.Tools!.First());
var inputSchema = function.JsonSchema.GetRawText();

Assert.Contains("\"code\"", inputSchema);
Assert.DoesNotContain("query", inputSchema);
}

[Fact]
public void AddAndRemoveTools_RoundTrips()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) Microsoft. All rights reserved.

using Microsoft.Extensions.AI;

namespace Microsoft.Agents.AI.LocalCodeAct.UnitTests;

public sealed class LocalExecuteCodeFunctionTests
{
private static LocalCodeActProviderOptions Options(AIFunction tool) =>
new()
{
ValidationDisabled = true, // No subprocess will be launched in these tests
Tools = [tool],
};

[Fact]
public void Description_WithParameterizedTool_CarriesHostToolSchema()
{
var tool = AIFunctionFactory.Create((string query) => "ok", name: "lookup");

var function = new LocalExecuteCodeFunction("/usr/bin/python3", Options(tool));

Assert.Contains("query", function.Description);
}

[Fact]
public void JsonSchema_WithParameterizedTool_StaysCodeOnly()
{
var tool = AIFunctionFactory.Create((string query) => "ok", name: "lookup");

var function = new LocalExecuteCodeFunction("/usr/bin/python3", Options(tool));
var inputSchema = function.JsonSchema.GetRawText();

Assert.Contains("\"code\"", inputSchema);
Assert.DoesNotContain("query", inputSchema);
}
}
Loading