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
@@ -1,10 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Text.Json;

using Json.Schema;

using OpenAI.Chat;

namespace Microsoft.Teams.AI.Models.OpenAI;
Expand All @@ -18,7 +14,7 @@ public static IFunction ToTeams(this ChatTool tool)
return new Function(
tool.FunctionName,
tool.FunctionDescription,
JsonSchema.FromText(parameters == string.Empty ? "{}" : parameters),
JsonSchemaWrapper.FromJson(parameters == string.Empty ? "{}" : parameters),
() => Task.FromResult<object?>(null)
);
}
Expand All @@ -28,7 +24,7 @@ public static ChatTool ToOpenAI(this IFunction function)
return ChatTool.CreateFunctionTool(
function.Name,
function.Description,
function.Parameters is null ? null : BinaryData.FromString(JsonSerializer.Serialize(function.Parameters)),
function.Parameters is null ? null : BinaryData.FromString(function.Parameters.ToJson()),
false
);
}
Expand Down
30 changes: 11 additions & 19 deletions Libraries/Microsoft.Teams.AI/Function.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,8 @@

using System.Reflection;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;

using Json.Schema;
using Json.Schema.Generation;

using Microsoft.Teams.AI.Annotations;
using Microsoft.Teams.AI.Messages;
using Microsoft.Teams.Common.Extensions;
Expand Down Expand Up @@ -38,7 +34,7 @@ public interface IFunction
/// the Json Schema representing what
/// parameters the function accepts
/// </summary>
public JsonSchema? Parameters { get; }
public IJsonSchema? Parameters { get; }
}

/// <summary>
Expand All @@ -57,7 +53,7 @@ public class Function : IFunction

[JsonPropertyName("parameters")]
[JsonPropertyOrder(2)]
public JsonSchema? Parameters { get; set; }
public IJsonSchema? Parameters { get; set; }
Comment on lines 54 to +56
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The IJsonSchema interface and JsonSchemaWrapper class lack a JsonConverter attribute or implementation. When Function.ToString() is called (line 118), it serializes the Parameters property which is of type IJsonSchema. Without a custom JsonConverter, the default serializer may not properly serialize the internal NJsonSchema.JsonSchema object, potentially resulting in incomplete or incorrect JSON output. Consider adding a JsonConverter for IJsonSchema that calls ToJson() on the instance.

Copilot uses AI. Check for mistakes.

[JsonIgnore]
public Delegate Handler { get; set; }
Expand All @@ -70,7 +66,7 @@ public Function(string name, string? description, Delegate handler)
Parameters = GenerateParametersSchema(handler);
}

public Function(string name, string? description, JsonSchema parameters, Delegate handler)
public Function(string name, string? description, IJsonSchema parameters, Delegate handler)
{
Name = name;
Description = description;
Expand All @@ -82,13 +78,13 @@ public Function(string name, string? description, JsonSchema parameters, Delegat
{
if (call.Arguments is not null && Parameters is not null)
{
var valid = Parameters.Evaluate(JsonNode.Parse(call.Arguments), new() { EvaluateAs = SpecVersion.DraftNext });
var result = Parameters.Validate(call.Arguments);

if (!valid.IsValid)
if (!result.IsValid)
{
Console.WriteLine(JsonSerializer.Serialize(valid));
Console.WriteLine(string.Join("\n", result.Errors.Select(e => $"{e.Path} => {e.Message}")));
throw new ArgumentException(
string.Join("\n", valid.Errors?.Select(e => $"{e.Key} => {e.Value}") ?? [])
string.Join("\n", result.Errors.Select(e => $"{e.Path} => {e.Message}"))
);
}
}
Expand Down Expand Up @@ -128,7 +124,7 @@ public override string ToString()
/// <summary>
/// Generates a JsonSchema for the parameters of a delegate handler using reflection
/// </summary>
private static JsonSchema? GenerateParametersSchema(Delegate handler)
private static IJsonSchema? GenerateParametersSchema(Delegate handler)
{
var method = handler.GetMethodInfo();
var methodParams = method.GetParameters();
Expand All @@ -141,15 +137,11 @@ public override string ToString()
var parameters = methodParams.Select(p =>
{
var paramName = p.GetCustomAttribute<ParamAttribute>()?.Name ?? p.Name ?? p.Position.ToString();
var schema = new JsonSchemaBuilder().FromType(p.ParameterType).Build();
var schema = JsonSchemaWrapper.FromType(p.ParameterType);
var required = !p.IsOptional;
return (paramName, schema, required);
});
}).ToArray();

return new JsonSchemaBuilder()
.Type(SchemaValueType.Object)
.Properties(parameters.Select(item => (item.paramName, item.schema)).ToArray())
.Required(parameters.Where(item => item.required).Select(item => item.paramName))
.Build();
return JsonSchemaWrapper.CreateObjectSchema(parameters);
}
}
4 changes: 2 additions & 2 deletions Libraries/Microsoft.Teams.AI/Microsoft.Teams.AI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="JsonSchema.Net" Version="7.3.4" />
<PackageReference Include="JsonSchema.Net.Generation" Version="5.0.0" />
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="NJsonSchema" Version="11.1.0" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Json.Schema;

namespace Microsoft.Teams.AI.Prompts;

public partial class ChatPrompt<TOptions>
Expand All @@ -17,8 +15,8 @@ public ChatPrompt<TOptions> Chain(IChatPrompt<TOptions> prompt)
Functions.Add(new Function(
prompt.Name,
prompt.Description,
new JsonSchemaBuilder().Properties(
("text", new JsonSchemaBuilder().Type(SchemaValueType.String).Description("text to send"))
JsonSchemaWrapper.CreateObjectSchema(
("text", JsonSchemaWrapper.String("text to send"), true)
),
async (string text) =>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@

using Humanizer;

using Json.Schema;

using Microsoft.Teams.AI.Messages;

namespace Microsoft.Teams.AI.Prompts;
Expand All @@ -26,7 +24,7 @@ public ChatPrompt<TOptions> Function(string name, string? description, Delegate
return this;
}

public ChatPrompt<TOptions> Function(string name, string? description, JsonSchema parameters, Delegate handler)
public ChatPrompt<TOptions> Function(string name, string? description, IJsonSchema parameters, Delegate handler)
{
var func = new Function(name, description, parameters, handler);
Functions.Add(func);
Expand Down
43 changes: 43 additions & 0 deletions Libraries/Microsoft.Teams.AI/Schema/IJsonSchema.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

namespace Microsoft.Teams.AI;

/// <summary>
/// Abstraction for JSON schema validation, decoupling from specific schema library implementations.
/// </summary>
public interface IJsonSchema
{
/// <summary>
/// Validates a JSON string against this schema.
/// </summary>
JsonSchemaValidationResult Validate(string json);

/// <summary>
/// Serializes this schema to a JSON string.
/// </summary>
string ToJson();
}

/// <summary>
/// Result of validating JSON against a schema.
/// </summary>
public class JsonSchemaValidationResult
{
/// <summary>
/// Whether the JSON is valid against the schema.
/// </summary>
public bool IsValid { get; init; }

/// <summary>
/// List of validation errors, if any.
/// </summary>
public IReadOnlyList<JsonSchemaValidationError> Errors { get; init; } = [];
}

/// <summary>
/// Represents a validation error from schema validation.
/// </summary>
/// <param name="Path">The JSON path where the error occurred.</param>
/// <param name="Message">The error message.</param>
public record JsonSchemaValidationError(string Path, string Message);
89 changes: 89 additions & 0 deletions Libraries/Microsoft.Teams.AI/Schema/JsonSchemaWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using NJsonSchema;

namespace Microsoft.Teams.AI;

/// <summary>
/// NJsonSchema-based implementation of IJsonSchema.
/// </summary>
public class JsonSchemaWrapper : IJsonSchema
{
private readonly JsonSchema _schema;

private JsonSchemaWrapper(JsonSchema schema) => _schema = schema;

/// <summary>
/// Creates a schema from a .NET type using reflection.
/// </summary>
public static IJsonSchema FromType(Type type)
{
var schema = JsonSchema.FromType(type);
return new JsonSchemaWrapper(schema);
}

/// <summary>
/// Creates a schema from a JSON schema string.
/// </summary>
public static IJsonSchema FromJson(string json)
{
var schema = JsonSchema.FromJsonAsync(json).GetAwaiter().GetResult();
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using GetAwaiter().GetResult() can cause deadlocks in certain synchronization contexts (e.g., UI or ASP.NET contexts before .NET Core). Consider making this method async and returning Task<IJsonSchema>, or use ConfigureAwait(false) before GetAwaiter() to reduce the risk of deadlocks. This pattern is used elsewhere in the codebase for consistency, but the deadlock risk should be considered.

Suggested change
var schema = JsonSchema.FromJsonAsync(json).GetAwaiter().GetResult();
var schema = JsonSchema.FromJsonAsync(json).ConfigureAwait(false).GetAwaiter().GetResult();

Copilot uses AI. Check for mistakes.
return new JsonSchemaWrapper(schema);
}

/// <summary>
/// Creates an object schema with the specified properties.
/// </summary>
public static IJsonSchema CreateObjectSchema(params (string name, IJsonSchema schema, bool required)[] properties)
{
var resultSchema = new JsonSchema { Type = JsonObjectType.Object };

foreach (var (name, propSchema, required) in properties)
{
if (propSchema is JsonSchemaWrapper wrapper)
{
var property = new JsonSchemaProperty
{
Type = wrapper._schema.Type,
Description = wrapper._schema.Description
};
Comment on lines +49 to +50
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JsonSchemaProperty creation only copies Type and Description from the wrapped schema, but it doesn't copy other potentially important properties like Format, Pattern, MinLength, MaxLength, Minimum, Maximum, Enum values, etc. This could result in incomplete validation when complex schemas are used as properties. Consider either using the entire wrapped schema directly or copying all relevant schema properties.

Suggested change
Description = wrapper._schema.Description
};
Description = wrapper._schema.Description,
Format = wrapper._schema.Format,
Pattern = wrapper._schema.Pattern,
MinLength = wrapper._schema.MinLength,
MaxLength = wrapper._schema.MaxLength,
Minimum = wrapper._schema.Minimum,
Maximum = wrapper._schema.Maximum
};
foreach (var enumValue in wrapper._schema.Enumeration)
{
property.Enumeration.Add(enumValue);
}

Copilot uses AI. Check for mistakes.
resultSchema.Properties.Add(name, property);
}
Comment on lines +42 to +52
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CreateObjectSchema method silently skips properties when propSchema is not a JsonSchemaWrapper instance. This could lead to incomplete schema definitions if an implementation of IJsonSchema other than JsonSchemaWrapper is passed. Consider either throwing an exception when propSchema is not a JsonSchemaWrapper, or handling all IJsonSchema implementations properly. Alternatively, document this limitation in the method's XML documentation.

Copilot uses AI. Check for mistakes.

if (required)
{
resultSchema.RequiredProperties.Add(name);
Comment on lines +52 to +56
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CreateObjectSchema method adds properties to RequiredProperties regardless of whether the property was successfully added to Properties. If a property's schema is not a JsonSchemaWrapper (and is thus skipped at lines 44-52), it will still be added to RequiredProperties at line 56. This inconsistency could lead to validation errors where the schema requires properties that don't exist in the schema definition.

Suggested change
}
if (required)
{
resultSchema.RequiredProperties.Add(name);
if (required)
{
resultSchema.RequiredProperties.Add(name);
}

Copilot uses AI. Check for mistakes.
}
}

return new JsonSchemaWrapper(resultSchema);
}

/// <summary>
/// Creates a string schema.
/// </summary>
public static IJsonSchema String(string? description = null)
{
var schema = new JsonSchema
{
Type = JsonObjectType.String,
Description = description
};
return new JsonSchemaWrapper(schema);
}

/// <inheritdoc/>
public JsonSchemaValidationResult Validate(string json)
{
var errors = _schema.Validate(json);
return new JsonSchemaValidationResult
{
IsValid = errors.Count == 0,
Errors = errors.Select(e => new JsonSchemaValidationError(e.Path ?? string.Empty, e.Kind.ToString())).ToList()
};
}

/// <inheritdoc/>
public string ToJson() => _schema.ToJson();
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
using Microsoft.Teams.Api.Auth;
using Microsoft.Teams.Api.Clients;
using Microsoft.Teams.Apps.Plugins;
using Microsoft.Teams.Common.Extensions;
using Microsoft.Teams.Common.Logging;
using Microsoft.Teams.Common.Storage;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
using Json.Schema;

using Microsoft.Teams.AI;
using Microsoft.Teams.AI.Prompts;
using Microsoft.Teams.Common.Logging;
Expand Down Expand Up @@ -220,7 +218,7 @@ internal AI.Function CreateFunctionFromTool(Uri url, McpToolDetails tool, McpCli
return new AI.Function(
tool.Name,
tool.Description,
JsonSchema.FromText(tool.InputSchema?.GetRawText() ?? "{}"),
JsonSchemaWrapper.FromJson(tool.InputSchema?.GetRawText() ?? "{}"),
async (IDictionary<string, object?> args) =>
{
try
Expand Down
2 changes: 1 addition & 1 deletion Samples/Samples.BotBuilder/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@
await context.Send("hi from teams...");
});

app.Run();
app.Run();
3 changes: 1 addition & 2 deletions Samples/Samples.Cards/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.Text.Json;

using Microsoft.Teams.Api.Activities.Invokes;
using Microsoft.Teams.Api.AdaptiveCards;
using Microsoft.Teams.Apps.Activities;
using Microsoft.Teams.Apps.Activities.Invokes;
Expand Down Expand Up @@ -355,4 +354,4 @@ static Microsoft.Teams.Cards.AdaptiveCard CreateCardFromJson()
}
};
}
}
}
2 changes: 1 addition & 1 deletion Samples/Samples.Dialogs/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -310,4 +310,4 @@ static Microsoft.Teams.Api.TaskModules.Response CreateMixedExampleDialog()
};

return new Microsoft.Teams.Api.TaskModules.Response(new Microsoft.Teams.Api.TaskModules.ContinueTask(taskInfo));
}
}
2 changes: 1 addition & 1 deletion Samples/Samples.Lights/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@
state.Save(context);
});

app.Run();
app.Run();
4 changes: 2 additions & 2 deletions Samples/Samples.Meetings/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
{
await context.Next();
}
catch(Exception e)
catch (Exception e)
{
context.Log.Error(e);
context.Log.Error("error occurred during activity processing");
Expand Down Expand Up @@ -127,4 +127,4 @@
await context.Send($"you said '{context.Activity.Text}'");
});

app.Run();
app.Run();
2 changes: 1 addition & 1 deletion Samples/Samples.MessageExtensions/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -507,4 +507,4 @@ function cancelSettings() {
</body>
</html>
""";
}
}
Loading
Loading