diff --git a/Claudia.sln b/Claudia.sln index d09a35d..1a7bfee 100644 --- a/Claudia.sln +++ b/Claudia.sln @@ -13,9 +13,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleApp1", "sandbox\Cons EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{1B4BD6F6-8528-4409-BA55-085DA5486D36}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Claudia.Tests", "tests\Claudia.Tests\Claudia.Tests.csproj", "{B4A34AB7-FD1B-4CC4-9859-603D26D7D0A4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Claudia.Tests", "tests\Claudia.Tests\Claudia.Tests.csproj", "{B4A34AB7-FD1B-4CC4-9859-603D26D7D0A4}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazorApp1", "sandbox\BlazorApp1\BlazorApp1.csproj", "{8EEB0F69-132B-4887-959D-25531588FCD2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorApp1", "sandbox\BlazorApp1\BlazorApp1.csproj", "{8EEB0F69-132B-4887-959D-25531588FCD2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Claudia.FunctionGenerator", "src\Claudia.FunctionGenerator\Claudia.FunctionGenerator.csproj", "{8C464111-AD67-4D2B-9AE2-0B52AB077EBD}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -39,6 +41,10 @@ Global {8EEB0F69-132B-4887-959D-25531588FCD2}.Debug|Any CPU.Build.0 = Debug|Any CPU {8EEB0F69-132B-4887-959D-25531588FCD2}.Release|Any CPU.ActiveCfg = Release|Any CPU {8EEB0F69-132B-4887-959D-25531588FCD2}.Release|Any CPU.Build.0 = Release|Any CPU + {8C464111-AD67-4D2B-9AE2-0B52AB077EBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8C464111-AD67-4D2B-9AE2-0B52AB077EBD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C464111-AD67-4D2B-9AE2-0B52AB077EBD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8C464111-AD67-4D2B-9AE2-0B52AB077EBD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -48,6 +54,7 @@ Global {7D6C6A91-7B2E-4616-A40F-ACA1A0BDBEB4} = {E61BFC87-2B96-4699-9B69-EE4B008AE0A0} {B4A34AB7-FD1B-4CC4-9859-603D26D7D0A4} = {1B4BD6F6-8528-4409-BA55-085DA5486D36} {8EEB0F69-132B-4887-959D-25531588FCD2} = {E61BFC87-2B96-4699-9B69-EE4B008AE0A0} + {8C464111-AD67-4D2B-9AE2-0B52AB077EBD} = {B54A8855-F8F0-4015-80AA-86974E65AC2D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B7CEBA02-BB0C-4102-AE58-DFD114C3192A} diff --git a/sandbox/ConsoleApp1/ConsoleApp1.csproj b/sandbox/ConsoleApp1/ConsoleApp1.csproj index 6a1e020..52d22f3 100644 --- a/sandbox/ConsoleApp1/ConsoleApp1.csproj +++ b/sandbox/ConsoleApp1/ConsoleApp1.csproj @@ -6,14 +6,19 @@ enable enable false + true - + + + Analyzer + false + diff --git a/sandbox/ConsoleApp1/Program.cs b/sandbox/ConsoleApp1/Program.cs index a76a542..63822fa 100644 --- a/sandbox/ConsoleApp1/Program.cs +++ b/sandbox/ConsoleApp1/Program.cs @@ -1,52 +1,278 @@ using Claudia; using R3; +using System.Xml.Linq; -var imageBytes = File.ReadAllBytes(@"dish.jpg"); +// function calling +// https://github.com/anthropics/anthropic-cookbook/blob/main/function_calling/function_calling.ipynb var anthropic = new Anthropic(); +var systemPrompt = """ +In this environment you have access to a set of tools you can use to answer the user's question. + +You may call them like this: + + + $TOOL_NAME + + <$PARAMETER_NAME>$PARAMETER_VALUE + ... + + + + +Here are the tools available: + + + DoPairwiseArithmetic + + Calculator function for doing basic arithmetic. + Supports addition, subtraction, multiplication + + + + first_operand + int + First operand (before the operator) + + + second_operand + int + Second operand (after the operator) + + + operator + str + The operation to perform. Must be either +, -, *, or / + + + + +"""; + +var userInput = "Multiply 1,984,135 by 9,343,116"; + +// Claude generate tool and parameters XML var message = await anthropic.Messages.CreateAsync(new() { - Model = "claude-3-opus-20240229", + Model = Models.Claude3Opus, MaxTokens = 1024, - Messages = [new() - { - Role = "user", - Content = [ - new() - { - Type = "image", - Source = new() - { - Type = "base64", - MediaType = "image/jpeg", - Data = imageBytes - } - }, - new() - { - Type = "text", - Text = "Describe this image." - } - ] - }], + System = systemPrompt, + StopSequences = [""], + Messages = [ + new() { Role = Roles.User, Content = userInput }, + ], }); -Console.WriteLine(message); -var simple = await anthropic.Messages.CreateAsync(new() +// Parse function_calls XML + +// Okay, let's break this down step-by-step: +// +// +// DoPairwiseArithmetic +// +// 1984135 +// 9343116 +// * +// +// +var text = message.Content[0].Text!; +var tagStart = text.IndexOf(""); +var xmlResult = XElement.Parse(text.Substring(tagStart) + message.StopSequence); +var parameters = xmlResult.Descendants("parameters").Elements(); + +var first = (double)parameters.First(x => x.Name == "first_operand"); +var second = (double)parameters.First(x => x.Name == "second_operand"); +var operation = (string)parameters.First(x => x.Name == "operator"); + +// Execute local function +var result = DoPairwiseArithmetic(first, second, operation); + +// Setup result message in XML +var partialAssistantMessage = $$""" + + + DoPairwiseArithmetic + + {{first}} + {{second}} + {{operation}} + + + + + + + DoPairwiseArithmetic + + {{result}} + + + +"""; + +var callResult = await anthropic.Messages.CreateAsync(new() { Model = Models.Claude3Opus, MaxTokens = 1024, - Messages = [new() - { - Role = Roles.User, - Content = [ - new(imageBytes, "image/jpeg"), - "Describe this image." - ] - }], + System = systemPrompt, + Messages = [ + new() { Role = Roles.User, Content = userInput }, + new() { Role = Roles.Assistant, Content = partialAssistantMessage }, + ], }); -Console.WriteLine(simple); + +// Show last result. +// Therefore, 1,984,135 multiplied by 9,343,116 equals 18,538,003,464,660. +Console.WriteLine(callResult); + +static double DoPairwiseArithmetic(double num1, double num2, string operation) +{ + return operation switch + { + "+" => num1 + num2, + "-" => num1 - num2, + "*" => num1 * num2, + "/" => num1 / num2, + _ => throw new ArgumentException("Operation not supported") + }; +} + + + + + + +//var systemPrompt = """ +//In this environment you have access to a set of tools you can use to answer the user's question. + +//You may call them like this: +// +// +// $TOOL_NAME +// +// <$PARAMETER_NAME>$PARAMETER_VALUE +// ... +// +// +// + +//Here are the tools available: +// +// +// calculator +// +// Calculator function for doing basic arithmetic. +// Supports addition, subtraction, multiplication +// +// +// +// first_operand +// int +// First operand (before the operator) +// +// +// second_operand +// int +// Second operand (after the operator) +// +// +// operator +// str +// The operation to perform. Must be either +, -, *, or / +// +// +// +// +//"""; + + +//var message = await anthropic.Messages.CreateAsync(new() +//{ +// Model = Models.Claude3Opus, +// MaxTokens = 1024, +// System = systemPrompt, +// StopSequences = ["\n\nHuman:", "\n\nAssistant", ""], +// Messages = [new() { Role = "user", Content = "Multiply 1,984,135 by 9,343,116" }], +//}); + +//// Result XML:: + + +//var text = message.Content[0].Text!; +//var tagStart = text.IndexOf(""); +//var xmlResult = XElement.Parse(text.Substring(tagStart) + message.StopSequence); +//var parameters = xmlResult.Descendants("parameters").Elements(); + +//var first = (double)parameters.First(x => x.Name == "first_operand"); +//var second = (double)parameters.First(x => x.Name == "second_operand"); +//var operation = (string)parameters.First(x => x.Name == "operator"); + +//var result = DoPairwiseArithmetic(first, second, operation); + +//Console.WriteLine(result); + + + + + + + + + + + + + + + + +//var imageBytes = File.ReadAllBytes(@"dish.jpg"); + +//var anthropic = new Anthropic(); + +//var message = await anthropic.Messages.CreateAsync(new() +//{ +// Model = "claude-3-opus-20240229", +// MaxTokens = 1024, +// Messages = [new() +// { +// Role = "user", +// Content = [ +// new() +// { +// Type = "image", +// Source = new() +// { +// Type = "base64", +// MediaType = "image/jpeg", +// Data = imageBytes +// } +// }, +// new() +// { +// Type = "text", +// Text = "Describe this image." +// } +// ] +// }], +//}); +//Console.WriteLine(message); + +//var simple = await anthropic.Messages.CreateAsync(new() +//{ +// Model = Models.Claude3Opus, +// MaxTokens = 1024, +// Messages = [new() +// { +// Role = Roles.User, +// Content = [ +// new(imageBytes, "image/jpeg"), +// "Describe this image." +// ] +// }], +//}); +//Console.WriteLine(simple); //// convert to array. //var array = await stream.ToObservable().ToArrayAsync(); @@ -153,4 +379,22 @@ //}, new() //{ // Timeout = TimeSpan.FromSeconds(5) -//}); \ No newline at end of file +//}); + + +/// +/// AIUEO! +/// +public static partial class FunctionTools +{ + /// + /// foobarbaz + /// + /// p1 + /// p2 + [ClaudiaFunction] + public static int Sum(int x, int y) + { + return x + y; + } +} \ No newline at end of file diff --git a/src/Claudia.FunctionGenerator/Claudia.FunctionGenerator.csproj b/src/Claudia.FunctionGenerator/Claudia.FunctionGenerator.csproj new file mode 100644 index 0000000..d30c299 --- /dev/null +++ b/src/Claudia.FunctionGenerator/Claudia.FunctionGenerator.csproj @@ -0,0 +1,32 @@ + + + + netstandard2.0 + 12 + enable + cs + enable + + true + true + + false + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/src/Claudia.FunctionGenerator/ClaudiaFunctionGenerator.cs b/src/Claudia.FunctionGenerator/ClaudiaFunctionGenerator.cs new file mode 100644 index 0000000..e8d39fc --- /dev/null +++ b/src/Claudia.FunctionGenerator/ClaudiaFunctionGenerator.cs @@ -0,0 +1,32 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System; +using System.Collections.Immutable; + +namespace Claudia.FunctionGenerator; + +[Generator(LanguageNames.CSharp)] +public partial class ClaudiaFunctionGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var source = context.SyntaxProvider.ForAttributeWithMetadataName( + "Claudia.ClaudiaFunctionAttribute", + static (node, token) => node is MethodDeclarationSyntax, + static (context, token) => context); + + context.RegisterSourceOutput(source.Collect(), Execute); + } + + static void Execute(SourceProductionContext context, ImmutableArray sources) + { + if (sources.Length == 0) return; + + var result = new Parser(context, sources).Parse(); + if (result.Length != 0) + { + var emitter = new Emitter(context, result); + emitter.Emit(); + } + } +} diff --git a/src/Claudia.FunctionGenerator/Emitter.cs b/src/Claudia.FunctionGenerator/Emitter.cs new file mode 100644 index 0000000..d71d941 --- /dev/null +++ b/src/Claudia.FunctionGenerator/Emitter.cs @@ -0,0 +1,61 @@ +using Microsoft.CodeAnalysis; +using System.Xml.Linq; + +namespace Claudia.FunctionGenerator; + +public class Emitter +{ + private SourceProductionContext context; + private ParseResult[] result; + + public Emitter(SourceProductionContext context, ParseResult[] result) + { + this.context = context; + this.result = result; + } + + internal void Emit() + { + foreach (var item in result.GroupBy(x => x.TypeSymbol, SymbolEqualityComparer.Default)) + { + var type = item.Key!; + + var typeDocComment = type.GetDocumentationCommentXml(); + + var name = type.Name; + var description = ((string)XElement.Parse(typeDocComment).Element("summary")).Trim(); + + + foreach (var method in item) + { + + var docComment = method.MethodSymbol.GetDocumentationCommentXml(); + + + + //new XElement("parameter", + // new XElement( + + } + + + + + new XElement("tool_description", + new XElement("tool_name", name), + new XElement("description", description), + new XElement("parameters", null)); + + } + + + + + + + + + + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/Claudia.FunctionGenerator/ParseResult.cs b/src/Claudia.FunctionGenerator/ParseResult.cs new file mode 100644 index 0000000..180e070 --- /dev/null +++ b/src/Claudia.FunctionGenerator/ParseResult.cs @@ -0,0 +1,13 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Claudia.FunctionGenerator; + +public record ParseResult +{ + public required TypeDeclarationSyntax TypeSyntax { get; set; } + public required INamedTypeSymbol TypeSymbol { get; set; } + public required MethodDeclarationSyntax MethodSyntax { get; set; } + public required IMethodSymbol MethodSymbol { get; set; } +} + diff --git a/src/Claudia.FunctionGenerator/Parser.cs b/src/Claudia.FunctionGenerator/Parser.cs new file mode 100644 index 0000000..4fb20e5 --- /dev/null +++ b/src/Claudia.FunctionGenerator/Parser.cs @@ -0,0 +1,124 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Text; + +namespace Claudia.FunctionGenerator; + +public class Parser +{ + private SourceProductionContext context; + private ImmutableArray sources; + + public Parser(SourceProductionContext context, ImmutableArray sources) + { + this.context = context; + this.sources = sources; + } + + internal ParseResult[] Parse() + { + var list = new List(); + + // grouping by type(TypeDeclarationSyntax) + foreach (var item in sources.GroupBy(x => x.TargetNode.Parent)) + { + if (item.Key == null) continue; + var targetType = (TypeDeclarationSyntax)item.Key; + var symbol = item.First().SemanticModel.GetDeclaredSymbol(targetType); + if (symbol == null) continue; + + // verify is partial + if (!IsPartial(targetType)) + { + // TODO: + // context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.MustBePartial, targetType.Identifier.GetLocation(), symbol.Name)); + continue; + } + + // nested is not allowed + if (IsNested(targetType)) + { + // TODO: + //context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.NestedNotAllow, targetType.Identifier.GetLocation(), symbol.Name)); + continue; + } + + // verify is generis type + if (symbol.TypeParameters.Length > 0) + { + // TODO: + //context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.GenericTypeNotSupported, targetType.Identifier.GetLocation(), symbol.Name)); + continue; + } + + // TODO:verify documentation somment of summary. + + foreach (var source in item) + { + // source.TargetNode + var method = (IMethodSymbol)source.TargetSymbol; + + // TODO:verify not static + // TODO:verify documentation somment of summary and parameters. + + //var (attr, setLogLevel) = GetAttribute(source); + //var msg = attr.Message; + + //// parse and verify + //if (!MessageParser.TryParseFormat(attr.Message, out var segments)) + //{ + // context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.MessageTemplateParseFailed, (source.TargetNode as MethodDeclarationSyntax)!.Identifier.GetLocation(), method.Name)); + // continue; + //} + + //var (parameters, foundLogLevel) = GetMethodParameters(method, setLogLevel); + + //// Set LinkedParameters + //foreach (var p in parameters.Where(x => x.IsParameter)) + //{ + // p.LinkedMessageSegment = segments + // .Where(x => x.Kind == MessageSegmentKind.NameParameter) + // .FirstOrDefault(x => x.NameParameter.Equals(p.Symbol.Name, StringComparison.OrdinalIgnoreCase)); + //} + + //var methodDecl = new LogMethodDeclaration( + // Attribute: attr, + // TargetMethod: (IMethodSymbol)source.TargetSymbol, + // TargetSyntax: (MethodDeclarationSyntax)source.TargetNode, + // MessageSegments: segments, + // MethodParameters: parameters); + + //if (!Verify(methodDecl, foundLogLevel, targetType, symbol)) + //{ + // continue; + //} + + //logMethods.Add(methodDecl); + + list.Add(new ParseResult + { + TypeSyntax = targetType, + TypeSymbol = symbol, + MethodSyntax = (MethodDeclarationSyntax)source.TargetNode, + MethodSymbol = method + }); + } + } + + return list.ToArray(); + } + + static bool IsPartial(TypeDeclarationSyntax typeDeclaration) + { + return typeDeclaration.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)); + } + + static bool IsNested(TypeDeclarationSyntax typeDeclaration) + { + return typeDeclaration.Parent is TypeDeclarationSyntax; + } +} diff --git a/src/Claudia.FunctionGenerator/Properties/launchSettings.json b/src/Claudia.FunctionGenerator/Properties/launchSettings.json new file mode 100644 index 0000000..14f2b60 --- /dev/null +++ b/src/Claudia.FunctionGenerator/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "プロファイル 1": { + "commandName": "DebugRoslynComponent", + "targetProject": "..\\..\\sandbox\\ConsoleApp1\\ConsoleApp1.csproj" + } + } +} \ No newline at end of file diff --git a/src/Claudia/ClaudiaFunctionAttribute.cs b/src/Claudia/ClaudiaFunctionAttribute.cs new file mode 100644 index 0000000..4ca4b33 --- /dev/null +++ b/src/Claudia/ClaudiaFunctionAttribute.cs @@ -0,0 +1,6 @@ +namespace Claudia; + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] +public class ClaudiaFunctionAttribute : Attribute +{ +}