tghamm / Anthropic.SDK

An unofficial C#/.NET SDK for accessing the Anthropic Claude API
https://www.nuget.org/packages/Anthropic.SDK
MIT License
55 stars 10 forks source link

Function tool construction with JsonNode parameters: MethodInfo is null #24

Closed sibbl closed 4 months ago

sibbl commented 4 months ago

Hi,

I'd like to create tools using the public Function(string name, string description = null, JsonNode parameters = null) constructor and then add them.

However, the internal method GenerateJsonToolsFromCommonTools fails as function.MethodInfo is then null and it cannot iterate over it in the foreach while preparing the request to send to the API.

Is this way of providing a function via its JSON Schema representation in a JsonNode even supported, or should I use the function approach?

Thanks a lot for the library and your help!

tghamm commented 4 months ago

Hey @sibbl I think this can be supported with a little more time, honestly was in a bit of a hurry, and Stephen's work is much more mature than this library is. In the meantime I'd suggest the function approach, but I'm going to actively work on exploring an approach under the hood that's more in-depth - I just didn't have time to flesh everything out I wanted to before getting to a point where I felt like it was usable enough to get some good value. Perhaps in hindsight it should have been a pre-release.

tghamm commented 4 months ago

@sibbl I've gone down the path of JsonNode for 3.1.0. This more what you had in mind? Keep in mind I uses classes (see #26 for a more advanced example that mirrors the docs) and serialized, but there are other ways to construct the JsonNode, and this implements the full spec pretty much.

[TestMethod]
public async Task TestBasicToolManual()
{
    var client = new AnthropicClient();
    var messages = new List<Message>
    {
        new Message()
        {
            Role = RoleType.User,
            Content = "What is the weather in San Francisco, CA in fahrenheit?"
        }
    };
    var inputschema = new InputSchema()
    {
        Type = "object",
        Properties = new Dictionary<string, Property>()
        {
            { "location", new Property() { Type = "string", Description = "The location of the weather" } },
            {
                "tempType", new Property()
                {
                    Type = "string", Enum = Enum.GetNames(typeof(TempType)),
                    Description = "The unit of temperature, celsius or fahrenheit"
                }
            }
        },
        Required = new List<string>() { "location", "tempType" }
    };
    JsonSerializerOptions JsonSerializationOptions  = new()
    {
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
        Converters = { new JsonStringEnumConverter() },
        ReferenceHandler = ReferenceHandler.IgnoreCycles,
    };
    string jsonString = JsonSerializer.Serialize(inputschema, JsonSerializationOptions);
    var tools = new List<Common.Tool>()
    {
        new Common.Tool(new Function("GetWeather", "This function returns the weather for a given location",
            JsonNode.Parse(jsonString)))
    };
    var parameters = new MessageParameters()
    {
        Messages = messages,
        MaxTokens = 2048,
        Model = AnthropicModels.Claude3Sonnet,
        Stream = false,
        Temperature = 1.0m
    };
    var res = await client.Messages.GetClaudeMessageAsync(parameters, tools);

    messages.Add(res.Content.AsAssistantMessage());

    var toolUse = res.Content.FirstOrDefault(c => c.Type == ContentType.tool_use) as ToolUseContent;
    var id = toolUse.Id;
    var input = toolUse.Input;
    var param1 = toolUse.Input["location"].ToString();
    var param2 = Enum.Parse<TempType>(toolUse.Input["tempType"].ToString());

    var weather = await GetWeather(param1, param2);

    messages.Add(new Message()
    {
        Role = RoleType.User,
        Content = new[] { new ToolResultContent()
        {
            ToolUseId = id,
            Content = weather
        }
    }});

    var finalResult = await client.Messages.GetClaudeMessageAsync(parameters);

    Assert.IsTrue(finalResult.FirstMessage.Text.Contains("72 degrees"));
}

Thoughts?

sibbl commented 4 months ago

@tghamm thanks a lot for coming back so quickly!

This goes very much into the direction which I'd like to use, awesome!

This would solve my two main usages:

  1. via JSON Schemas in strings (e.g. from user input in debug/test pages)
  2. code-generated JSON schemas which directly generate a JsonNode. For example via the JsonSchema.Net.Generation package.
tghamm commented 4 months ago

@sibbl Great! I want to do a few more checks today but I'll likely release 3.1.0 later today that includes all of this and a few other things I found along the way. Appreciate your input!

tghamm commented 4 months ago

Closing as Complete with the latest release, feel free to open any new issues with anything you find!