Skip to content

Commit fe135d9

Browse files
gunpal5Gunpal Jain
andauthored
feat: Create Tools with Individual Methods (#23)
* fix: Json Schema Generation, used System.Text.Json JsonSchema generation. * fix: warning suppressions * feat: Added FunctionTool attribute, which can be used to convert individual methods to Tools. * feat: Added GoogleFunctionTool optional parameters in GenerateJsonSchemaAttribute.cs and FunctionToolAttribute.cs * Updated README.md --------- Co-authored-by: Gunpal Jain <[email protected]>
1 parent 3c867bd commit fe135d9

File tree

19 files changed

+977
-175
lines changed

19 files changed

+977
-175
lines changed

README.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
[![Discord](https://img.shields.io/discord/1115206893015662663?label=Discord&logo=discord&logoColor=white&color=d82679)](https://discord.gg/Ca2xhfBf3v)
77

88
## Features 🔥
9-
- Source generator to define functions natively through C# interfaces
9+
- Source generator to define functions natively through C# interfaces and individual methods
1010
- Doesn't use Reflection
1111
- All modern .NET features - nullability, trimming, NativeAOT, etc.
1212
- Tested for compatibility with OpenAI/Ollama/Anthropic/LangChain/Gemini
1313

1414
## Usage
15+
16+
### Interface
1517
```csharp
1618
using CSharpToJsonSchema;
1719

@@ -56,6 +58,32 @@ public class WeatherService : IWeatherFunctions
5658
var tools = service.AsTools();
5759
```
5860

61+
### Methods
62+
63+
```csharp
64+
65+
[FunctionTool]
66+
public Task<Weather> GetCurrentWeatherAsync(string location, Unit unit = Unit.Celsius, CancellationToken cancellationToken = default)
67+
{
68+
return Task.FromResult(new Weather
69+
{
70+
Location = location,
71+
Temperature = 22.0,
72+
Unit = unit,
73+
Description = "Sunny",
74+
});
75+
}
76+
77+
var tools = new Tools([GetCurrentWeatherAsync])
78+
79+
//Access list of CSharpToJsonSchema.Tool
80+
var myTools = tools.AvailableTools
81+
82+
//Implicit Conversion to list of CSharpToJsonSchema.Tool
83+
List<Tool> myTools = tools
84+
```
85+
86+
5987
## Support
6088

6189
Priority place for bugs: https://github.com/tryAGI/CSharpToJsonSchema/issues

src/libs/CSharpToJsonSchema.Generators/Conversion/SymbolGenerator.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,59 @@ public static class SymbolGenerator
9696
var classSymbol = semanticModel.GetDeclaredSymbol(classNode) as ITypeSymbol;
9797
return classSymbol;
9898
}
99+
100+
public static INamedTypeSymbol? GenerateToolJsonSerializerContext(
101+
string rootNamespace,
102+
Compilation originalCompilation)
103+
{
104+
105+
106+
// Example: we create a class name
107+
var className = $"ToolsJsonSerializerContext";
108+
109+
110+
// Build a class declaration
111+
var classDecl = SyntaxFactory.ClassDeclaration(className)
112+
.AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword))
113+
.AddModifiers(SyntaxFactory.Token(SyntaxKind.PartialKeyword));
114+
115+
// We create a compilation unit holding our new class
116+
117+
118+
119+
var namespaceName =rootNamespace; // choose your own
120+
var ns = SyntaxFactory.NamespaceDeclaration(SyntaxFactory.IdentifierName(namespaceName))
121+
.AddMembers(classDecl);
122+
123+
var compilationUnit = SyntaxFactory.CompilationUnit()
124+
.AddMembers(ns) // if ns is a NamespaceDeclarationSyntax
125+
.NormalizeWhitespace();
126+
127+
var parseOptions = CSharpParseOptions.Default.WithLanguageVersion(originalCompilation.GetLanguageVersion()?? LanguageVersion.Default);
128+
var syntaxTree =CSharpSyntaxTree.Create(compilationUnit,parseOptions);
129+
//CSharpSyntaxTree.Create(ns.NormalizeWhitespace());
130+
131+
// Now we need to add this syntax tree to a new or existing compilation
132+
var assemblyName = "TemporaryAssembly";
133+
var compilation = originalCompilation
134+
.AddSyntaxTrees(syntaxTree);
135+
//.WithAssemblyName(assemblyName);
136+
137+
138+
// Get the semantic model for our newly added syntax tree
139+
var semanticModel = compilation.GetSemanticModel(syntaxTree);
140+
141+
// Find the class syntax node in the syntax tree
142+
var classNode = syntaxTree.GetRoot().DescendantNodes()
143+
.OfType<ClassDeclarationSyntax>()
144+
.FirstOrDefault();
145+
146+
if (classNode == null) return null;
147+
148+
// Retrieve the ITypeSymbol from the semantic model
149+
var classSymbol = semanticModel.GetDeclaredSymbol(classNode);
150+
return classSymbol;
151+
}
99152

100153
public static AttributeSyntax GetConverter(string propertyType)
101154
{

src/libs/CSharpToJsonSchema.Generators/Conversion/ToModels.cs

Lines changed: 78 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ public static InterfaceData PrepareData(
1616

1717
var isStrict = attributeData.NamedArguments.FirstOrDefault(x => x.Key == "Strict").Value.Value is bool strict &&
1818
strict;
19+
var generateGoogleFunctionTool = attributeData.NamedArguments.FirstOrDefault(x => x.Key == "GoogleFunctionTool").Value.Value is bool googleFunctionTool &&
20+
googleFunctionTool;
1921
var methods = interfaceSymbol
2022
.GetMembers()
2123
.OfType<IMethodSymbol>()
@@ -43,42 +45,93 @@ public static InterfaceData PrepareData(
4345
return new InterfaceData(
4446
Namespace: interfaceSymbol.ContainingNamespace.ToDisplayString(),
4547
Name: interfaceSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat),
48+
GoogleFunctionTool:generateGoogleFunctionTool,
4649
Methods: methods);
4750
}
4851

4952
public static InterfaceData PrepareMethodData(
50-
this IMethodSymbol interfaceSymbol,
51-
AttributeData attributeData)
53+
List<(IMethodSymbol, AttributeData)> list)
5254
{
53-
interfaceSymbol = interfaceSymbol ?? throw new ArgumentNullException(nameof(interfaceSymbol));
54-
attributeData = attributeData ?? throw new ArgumentNullException(nameof(attributeData));
55+
//interfaceSymbol = interfaceSymbol ?? throw new ArgumentNullException(nameof(interfaceSymbol));
56+
list = list ?? throw new ArgumentNullException(nameof(list));
57+
58+
var namespaceName = "CSharpToJsonSchema";
59+
var className = "Tools";
60+
List<MethodData> methodList = new();
61+
List<string> namespaces = new();
62+
bool generateGoogleFunctionTools = false;
63+
foreach (var l in list)
64+
{
65+
var (interfaceSymbol, attributeData) = l;
66+
var isStrict = attributeData.NamedArguments.FirstOrDefault(x => x.Key == "Strict").Value.Value is bool strict &&
67+
strict;
68+
var ggft = attributeData.NamedArguments.FirstOrDefault(x => x.Key == "GoogleFunctionTool").Value.Value is bool googleFunctionTool &&
69+
googleFunctionTool;
70+
if(ggft)
71+
generateGoogleFunctionTools = true;
72+
73+
var x = interfaceSymbol;
74+
var parameters = x.Parameters
75+
//.Where(static x => x.Type.MetadataName != "CancellationToken")
76+
.ToArray();
77+
78+
var methodData = new MethodData(
79+
Name: x.Name,
80+
Description: GetDescription(x),
81+
IsAsync: x.IsAsync || x.ReturnType.Name == "Task",
82+
IsVoid: x.ReturnsVoid || x.ReturnType.MetadataName == "Task",
83+
IsStrict: isStrict,
84+
Parameters: parameters.Select(static y => y).ToArray(),
85+
Descriptions: parameters.Select(static l => GetParameterDescriptions(l)).SelectMany(s => s)
86+
.ToDictionary(s => s.Key, s => s.Value),
87+
ReturnType:x.ReturnType
88+
);
89+
methodList.Add(methodData);
90+
namespaces.Add(interfaceSymbol.ContainingNamespace.ToDisplayString());
91+
}
5592

56-
var isStrict = attributeData.NamedArguments.FirstOrDefault(x => x.Key == "Strict").Value.Value is bool strict &&
57-
strict;
58-
var x = interfaceSymbol;
59-
var parameters = x.Parameters
60-
//.Where(static x => x.Type.MetadataName != "CancellationToken")
61-
.ToArray();
93+
return new InterfaceData(
94+
Namespace: GetCommonRootNamespace(namespaces)??namespaceName,
95+
Name: className,
96+
GoogleFunctionTool: generateGoogleFunctionTools,
97+
Methods: methodList.ToArray());
98+
}
99+
public static string? GetCommonRootNamespace(IEnumerable<string> namespaces)
100+
{
101+
// Convert the list of namespaces to a list of arrays split by "."
102+
var splitNamespaces = namespaces
103+
.Select(ns => ns.Split('.'))
104+
.ToList();
105+
106+
if (!splitNamespaces.Any() || !splitNamespaces[0].Any())
107+
{
108+
return null;
109+
}
62110

63-
var methodData = new MethodData(
64-
Name: x.Name,
65-
Description: GetDescription(x),
66-
IsAsync: x.IsAsync || x.ReturnType.Name == "Task",
67-
IsVoid: x.ReturnsVoid || x.ReturnType.MetadataName == "Task",
68-
IsStrict: isStrict,
69-
Parameters: parameters.Select(static y => y).ToArray(),
70-
Descriptions: parameters.Select(static l => GetParameterDescriptions(l)).SelectMany(s => s)
71-
.ToDictionary(s => s.Key, s => s.Value),
72-
ReturnType:x.ReturnType
73-
);
111+
// Start with the first namespace parts
112+
var firstNsParts = splitNamespaces[0];
113+
var commonParts = new List<string>();
74114

115+
// For each index in the first namespace
116+
for (int i = 0; i < firstNsParts.Length; i++)
117+
{
118+
// Check if every other namespace has the same part at this index
119+
string currentPart = firstNsParts[i];
120+
if (splitNamespaces.All(nsArr => nsArr.Length > i && nsArr[i] == currentPart))
121+
{
122+
commonParts.Add(currentPart);
123+
}
124+
else
125+
{
126+
// Stop the moment there is a mismatch
127+
break;
128+
}
129+
}
75130

76-
return new InterfaceData(
77-
Namespace: interfaceSymbol.ContainingNamespace.ToDisplayString(),
78-
Name: "I"+interfaceSymbol.Name,
79-
Methods: [methodData]);
131+
return string.Join(".", commonParts);
80132
}
81133

134+
82135
// private static Dictionary<string, bool> GetIsRequired(IParameterSymbol[] parameters, Dictionary<string, bool>? dics = null)
83136
// {
84137
// dics ??= new Dictionary<string, bool>();

0 commit comments

Comments
 (0)