Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
7 changes: 7 additions & 0 deletions CSharpToJsonSchema.sln
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSharpToJsonSchema.Integrat
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSharpToJsonSchema.AotTests", "src\tests\CSharpToJsonSchema.AotTests\CSharpToJsonSchema.AotTests.csproj", "{6167F915-83EB-42F9-929B-AD4719A55811}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSharpToJsonSchema.MeaiTests", "src\tests\CSharpToJsonSchema.MeaiTests\CSharpToJsonSchema.MeaiTests.csproj", "{DC07C90E-A58C-44B3-82D2-E2EB8F777B92}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -76,6 +78,10 @@ Global
{6167F915-83EB-42F9-929B-AD4719A55811}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6167F915-83EB-42F9-929B-AD4719A55811}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6167F915-83EB-42F9-929B-AD4719A55811}.Release|Any CPU.Build.0 = Release|Any CPU
{DC07C90E-A58C-44B3-82D2-E2EB8F777B92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DC07C90E-A58C-44B3-82D2-E2EB8F777B92}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DC07C90E-A58C-44B3-82D2-E2EB8F777B92}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DC07C90E-A58C-44B3-82D2-E2EB8F777B92}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -89,6 +95,7 @@ Global
{8AFCC30C-C59D-498D-BE68-9A328B3E5599} = {AAA11B78-2764-4520-A97E-46AA7089A588}
{247C813A-9072-4DF3-B403-B35E3688DB4B} = {AAA11B78-2764-4520-A97E-46AA7089A588}
{6167F915-83EB-42F9-929B-AD4719A55811} = {AAA11B78-2764-4520-A97E-46AA7089A588}
{DC07C90E-A58C-44B3-82D2-E2EB8F777B92} = {AAA11B78-2764-4520-A97E-46AA7089A588}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {CED9A020-DBA5-4BE6-8096-75E528648EC1}
Expand Down
10 changes: 10 additions & 0 deletions src/libs/CSharpToJsonSchema.Generators/Conversion/ToModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ public static InterfaceData PrepareData(
strict;
var generateGoogleFunctionTool = attributeData.NamedArguments.FirstOrDefault(x => x.Key == "GoogleFunctionTool").Value.Value is bool googleFunctionTool &&
googleFunctionTool;
var meaiFunctionTool= attributeData.NamedArguments.FirstOrDefault(x => x.Key == "MeaiFunctionTool").Value.Value is bool meaift &&
meaift;
var methods = interfaceSymbol
.GetMembers()
.OfType<IMethodSymbol>()
Expand Down Expand Up @@ -46,6 +48,7 @@ public static InterfaceData PrepareData(
Namespace: interfaceSymbol.ContainingNamespace.ToDisplayString(),
Name: interfaceSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat),
GoogleFunctionTool:generateGoogleFunctionTool,
MeaiFunctionTool:meaiFunctionTool,
Methods: methods);
}

Expand All @@ -60,6 +63,7 @@ public static InterfaceData PrepareMethodData(
List<MethodData> methodList = new();
List<string> namespaces = new();
bool generateGoogleFunctionTools = false;
bool meaiFunctionTools = false;
foreach (var l in list)
{
var (interfaceSymbol, attributeData) = l;
Expand All @@ -70,6 +74,11 @@ public static InterfaceData PrepareMethodData(
if(ggft)
generateGoogleFunctionTools = true;

var meai = attributeData.NamedArguments.FirstOrDefault(x => x.Key == "MeaiFunctionTool").Value.Value is bool meaift &&
meaift;
if(meai)
meaiFunctionTools = true;

var x = interfaceSymbol;
var parameters = x.Parameters
//.Where(static x => x.Type.MetadataName != "CancellationToken")
Expand All @@ -94,6 +103,7 @@ public static InterfaceData PrepareMethodData(
Namespace: GetCommonRootNamespace(namespaces)??namespaceName,
Name: className,
GoogleFunctionTool: generateGoogleFunctionTools,
MeaiFunctionTool:meaiFunctionTools,
Methods: methodList.ToArray());
}
public static string? GetCommonRootNamespace(IEnumerable<string> namespaces)
Expand Down
22 changes: 21 additions & 1 deletion src/libs/CSharpToJsonSchema.Generators/JsonSchemaGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ private void ProcessMethods(IncrementalGeneratorInitializationContext context)
attributes
.SelectAndReportExceptions(AsGoogleFunctionToolsForMethods, context, Id)
.AddSource(context);
attributes
.SelectAndReportExceptions(AsMeaiFunctionToolsForMethods, context, Id)
.AddSource(context);

var generator = new JsonSourceGenerator();
generator.InitializeForFunctionTools(context);
Expand All @@ -73,6 +76,9 @@ private void ProcessInterfaces(IncrementalGeneratorInitializationContext context
attributes
.SelectAndReportExceptions(AsGoogleFunctionToolsForInterface, context, Id)
.AddSource(context);
attributes
.SelectAndReportExceptions(AsMeaiFunctionToolsForInterface, context, Id)
.AddSource(context);

var generator = new JsonSourceGenerator();
generator.Initialize2(context);
Expand Down Expand Up @@ -119,14 +125,28 @@ private static FileWithName AsFunctionCalls(InterfaceData @interface)
Name: $"{@interface.Name}.FunctionCalls.generated.cs",
Text: Sources.GenerateFunctionCalls(@interface));
}

private static FileWithName AsGoogleFunctionToolsForMethods(InterfaceData @interface)
{
return new FileWithName(
Name: $"{@interface.Name}.GoogleFunctionTools.generated.cs",
Text: Sources.GenerateGoogleFunctionToolForMethods(@interface));
}

private static FileWithName AsMeaiFunctionToolsForMethods(InterfaceData @interface)
{
return new FileWithName(
Name: $"{@interface.Name}.MeaiTools.generated.cs",
Text: Sources.GenerateMeaiFunctionToolForMethods(@interface));
}

private static FileWithName AsMeaiFunctionToolsForInterface(InterfaceData @interface)
{
return new FileWithName(
Name: $"{@interface.Name}.MeaiToolsExtensions.generated.cs",
Text: Sources.GenerateMeaiFunctionToolForInterface(@interface));
}

private static FileWithName AsGoogleFunctionToolsForInterface(InterfaceData @interface)
{
return new FileWithName(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ public readonly record struct InterfaceData(
string Namespace,
string Name,
bool GoogleFunctionTool,
bool MeaiFunctionTool,
IReadOnlyCollection<MethodData> Methods);
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ public static string GenerateFunctionCalls(InterfaceData @interface)

namespace {@interface.Namespace}
{{








{@interface.Methods.Select(static method => $@"
public class {method.Name}Args
{{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ public static string GenerateGoogleFunctionToolForInterface(InterfaceData @inter

namespace {@interface.Namespace}
{{
public partial class {extensionsClassName}
public static partial class {extensionsClassName}
{{
public global::GenerativeAI.Core.IFunctionTool AsGoogleFunctionTool(this {@interface.Name} service)
public static global::GenerativeAI.Core.IFunctionTool AsGoogleFunctionTool(this {@interface.Name} service)
{{
return new global::GenerativeAI.Tools.GenericFunctionTool(service.AsTools(), service.AsCalls());
}}
Expand Down
69 changes: 69 additions & 0 deletions src/libs/CSharpToJsonSchema.Generators/Sources.Method.MeaiTools.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using CSharpToJsonSchema.Generators.Models;

namespace CSharpToJsonSchema.Generators;

internal static partial class Sources
{
public static string GenerateMeaiFunctionToolForMethods(InterfaceData @interface)
{
if(@interface.Methods.Count == 0 || [email protected])
return string.Empty;
var extensionsClassName = @interface.Name;

return @$"
#nullable enable

namespace {@interface.Namespace}
{{
public partial class {extensionsClassName}
{{
public static implicit operator global::System.Collections.Generic.List<global::Microsoft.Extensions.AI.AITool>? ({@interface.Namespace}.{extensionsClassName} tools)
{{
return tools.AsMeaiTools();
}}

public global::System.Collections.Generic.List<global::Microsoft.Extensions.AI.AITool> AsMeaiTools()
{{
var lst = new global::System.Collections.Generic.List<global::Microsoft.Extensions.AI.AITool>();
var tools = this.AsTools();
var calls = this.AsCalls();
foreach (var tool in tools)
{{
var call = calls[tool.Name];
lst.Add(new global::CSharpToJsonSchema.MeaiFunction(tool, call));
}}
return lst;
}}
}}
}}";
}

public static string GenerateMeaiFunctionToolForInterface(InterfaceData @interface)
{
if([email protected])
return string.Empty;
var extensionsClassName = @interface.Name.Substring(startIndex: 1) + "Extensions";

return @$"
#nullable enable

namespace {@interface.Namespace}
{{
public partial class {extensionsClassName}
{{
public static global::System.Collections.Generic.List<global::Microsoft.Extensions.AI.AITool> AsMeaiTools(this {@interface.Name} service)
{{
var lst = new global::System.Collections.Generic.List<global::Microsoft.Extensions.AI.AITool>();
var tools = service.AsTools();
var calls = service.AsCalls();
foreach (var tool in tools)
{{
var call = calls[tool.Name];
lst.Add(new global::CSharpToJsonSchema.MeaiFunction(tool, call));
}}
return lst;
}}
}}
}}";
}
}
1 change: 1 addition & 0 deletions src/libs/CSharpToJsonSchema/CSharpToJsonSchema.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="9.3.0-preview.1.25114.11" />
<PackageReference Include="System.Text.Json" Version="9.0.0" />
</ItemGroup>

Expand Down
5 changes: 5 additions & 0 deletions src/libs/CSharpToJsonSchema/FunctionToolAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,9 @@ public sealed class FunctionToolAttribute : Attribute
/// Generate Google Function Tools extensions for Google_GenerativeAI SDK
/// </summary>
public bool GoogleFunctionTool { get; set; }

/// <summary>
/// Generate Microsoft.Extension.AI compatible function tools
/// </summary>
public bool MeaiFunctionTool { get; set; }
}
5 changes: 5 additions & 0 deletions src/libs/CSharpToJsonSchema/GenerateJsonSchemaAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,9 @@ public sealed class GenerateJsonSchemaAttribute : Attribute
/// Generate Google Function Tools extensions for Google_GenerativeAI SDK
/// </summary>
public bool GoogleFunctionTool { get; set; }

/// <summary>
/// Generate Microsoft.Extension.AI compatible function tools
/// </summary>
public bool MeaiFunctionTool { get; set; }
}
61 changes: 61 additions & 0 deletions src/libs/CSharpToJsonSchema/MeaiFunction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Extensions.AI;

namespace CSharpToJsonSchema;

public partial class MeaiFunction : AIFunction
{
private readonly Tool _tool;
private readonly Func<string, CancellationToken, Task<string>> _call;

private JsonElement _jsonSchema;
public override JsonElement JsonSchema => _jsonSchema;
public override string Name => _tool.Name;
public override string Description => _tool.Description;

public MeaiFunction(Tool tool, Func<string, CancellationToken, Task<string>> call)
{
this._tool = tool;
this._call = call;

_jsonSchema = JsonSerializer.Deserialize<JsonElement>(JsonSerializer.Serialize(tool.Parameters, OpenApiSchemaJsonContext.Default.OpenApiSchema),OpenApiSchemaJsonContext.Default.JsonElement);
}
protected override async Task<object?> InvokeCoreAsync(IEnumerable<KeyValuePair<string, object?>> arguments,
CancellationToken cancellationToken)
{
try
{
var json = GetArgsString(arguments);

var call = await _call(json, cancellationToken);

return JsonSerializer.Deserialize(call, OpenApiSchemaJsonContext.Default.JsonElement);
}catch(Exception e)
{
return e.Message;
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider unifying or clarifying the return type of InvokeCoreAsync.

Currently, when the call succeeds, the method returns a JsonElement, but in the catch block, it returns a string holding the exception message. Mixing these return types (both raw JSON and a string) might create ambiguity for consumers of this method if they expect a consistent data structure. Additionally, returning the full exception message could expose internal information.

-protected override async Task<object?> InvokeCoreAsync(IEnumerable<KeyValuePair<string, object?>> arguments,
-    CancellationToken cancellationToken)
+protected override async Task<JsonElement> InvokeCoreAsync(
+    IEnumerable<KeyValuePair<string, object?>> arguments,
+    CancellationToken cancellationToken)
{
    try
    {
        var json = GetArgsString(arguments);
        var call = await _call(json, cancellationToken);
        return JsonSerializer.Deserialize<JsonElement>(
            call, OpenApiSchemaJsonContext.Default.JsonElement);
    }
    catch (Exception e)
    {
-        return e.Message;
+        // Optionally: wrap error info within a JSON structure or throw a custom exception
+        throw new InvalidOperationException("Invocation failed.", e);
    }
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
protected override async Task<object?> InvokeCoreAsync(IEnumerable<KeyValuePair<string, object?>> arguments,
CancellationToken cancellationToken)
{
try
{
var json = GetArgsString(arguments);
var call = await _call(json, cancellationToken);
return JsonSerializer.Deserialize(call, OpenApiSchemaJsonContext.Default.JsonElement);
}catch(Exception e)
{
return e.Message;
}
}
protected override async Task<JsonElement> InvokeCoreAsync(
IEnumerable<KeyValuePair<string, object?>> arguments,
CancellationToken cancellationToken)
{
try
{
var json = GetArgsString(arguments);
var call = await _call(json, cancellationToken);
return JsonSerializer.Deserialize<JsonElement>(
call, OpenApiSchemaJsonContext.Default.JsonElement);
}
catch (Exception e)
{
// Optionally: wrap error info within a JSON structure or throw a custom exception
throw new InvalidOperationException("Invocation failed.", e);
}
}


private string GetArgsString(IEnumerable<KeyValuePair<string, object?>> arguments)
{
var jsonObject = new JsonObject();

foreach (var args in arguments)
{
if (args.Value is JsonElement element)
{
if(element.ValueKind == JsonValueKind.Array)
jsonObject[args.Key] = JsonArray.Create(element);
else if(element.ValueKind == JsonValueKind.Object)
jsonObject[args.Key] = JsonObject.Create(element);
}
else if (args.Value is JsonNode node)
{
jsonObject[args.Key] = node;
}
}

return jsonObject.ToJsonString();
}
}
4 changes: 4 additions & 0 deletions src/libs/CSharpToJsonSchema/OpenApiSchemaJsonContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ namespace CSharpToJsonSchema;
[JsonSerializable(typeof(OpenApiSchema))]
[JsonSerializable(typeof(IDictionary<string, OpenApiSchema>))]
[JsonSerializable(typeof(IDictionary<string, string>))]
[JsonSerializable(typeof(Tool))]
[JsonSerializable(typeof(List<Tool>))]
Comment on lines +8 to +9
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Missing import for Tool class

The code adds JSON serialization for Tool and List<Tool> types, but there's no import statement for the Tool class. This could cause compilation errors.

1
using System.Text.Json.Serialization;
2
+using System.Collections.Generic;
+// Add the appropriate namespace for the Tool class
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
[JsonSerializable(typeof(Tool))]
[JsonSerializable(typeof(List<Tool>))]
using System.Text.Json.Serialization;
using System.Collections.Generic;
// Add the appropriate namespace for the Tool class
[JsonSerializable(typeof(Tool))]
[JsonSerializable(typeof(List<Tool>))]

[JsonSerializable(typeof(JsonElement))]
[JsonSourceGenerationOptions(NumberHandling = JsonNumberHandling.Strict, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,WriteIndented = false)]
public partial class OpenApiSchemaJsonContext:JsonSerializerContext
{

Expand Down
46 changes: 28 additions & 18 deletions src/libs/CSharpToJsonSchema/SchemaSubsetHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,39 +110,49 @@ public static OpenApiSchema ConvertToSchema<T>(JsonSerializerOptions? jsonOption
public static OpenApiSchema ConvertToSchema(JsonTypeInfo type, string descriptionString)
{
var typeInfo = type;

var dics = JsonSerializer.Deserialize(descriptionString,OpenApiSchemaJsonContext.Default.IDictionaryStringString);

var dics = JsonSerializer.Deserialize(descriptionString,
OpenApiSchemaJsonContext.Default.IDictionaryStringString);
List<string> required = new List<string>();
var x = ConvertToCompatibleSchemaSubset(typeInfo.GetJsonSchemaAsNode(exporterOptions:new JsonSchemaExporterOptions()
{
TransformSchemaNode = (a, b) =>
var x = ConvertToCompatibleSchemaSubset(typeInfo.GetJsonSchemaAsNode(
exporterOptions: new JsonSchemaExporterOptions()
{
if (a.PropertyInfo == null)
return b;
var propName = ToCamelCase(a.PropertyInfo.Name);
if (dics.ContainsKey(propName))
TransformSchemaNode = (a, b) =>
{
b["description"] = dics[propName];
}
return b;
},
}));
if (a.TypeInfo.Type.IsEnum)
{
b["type"] = "string";
}

if (a.PropertyInfo == null)
return b;
var propName = ToCamelCase(a.PropertyInfo.Name);
if (dics.ContainsKey(propName))
{
b["description"] = dics[propName];
}

return b;
},
}));



foreach (var re in x.Properties)
{
required.Add(re.Key);
}

var mainDescription =x.Description ?? dics["mainFunction_Desc"];

var mainDescription = x.Description ?? dics["mainFunction_Desc"];
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Missing error handling for dictionary key lookup

The code assumes that the 'dics' dictionary contains a "mainFunction_Desc" key, but doesn't handle the case where this key might be missing. This could lead to a KeyNotFoundException at runtime.

-var mainDescription = x.Description ?? dics["mainFunction_Desc"];
+var mainDescription = x.Description ?? (dics.TryGetValue("mainFunction_Desc", out var desc) ? desc : "");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var mainDescription = x.Description ?? dics["mainFunction_Desc"];
var mainDescription = x.Description ?? (dics.TryGetValue("mainFunction_Desc", out var desc) ? desc : "");

return new OpenApiSchema()
{
Description = mainDescription,
Properties = x.Properties,
Required = required
Required = required,
Type = "object"
};
}

public static string ToCamelCase(string str)
{
if (!string.IsNullOrEmpty(str) && str.Length > 1)
Expand Down
Loading
Loading