Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Class design has gotten a bit confusing #28

Closed
nganju98 opened this issue Apr 18, 2024 · 4 comments
Closed

Class design has gotten a bit confusing #28

nganju98 opened this issue Apr 18, 2024 · 4 comments

Comments

@nganju98
Copy link
Contributor

nganju98 commented Apr 18, 2024

Lots of dynamic types now, MessageResponse has no Message in it, instead you have to call an extension method to convert to a Message. Message has a constructor that takes a Function and a dynamic? I'm not sure what that does.

I get that you want to have Message.Content always have the right thing in it, so it has to be dynamic. But if it's dynamic we can't use it generically anyway. List<Message>.ForEach(m => Console.WriteLine(m.Content)) would print real text sometimes and write the type name System.Collections.Generic.List1[Anthropic.SDK.Messaging.ContentBase] other times. So we have to already check the type, you might as well distinguish them.

I would suggest having Message.Content always be a List<ContentBase>. If a response comes back with just a simple message, the list has one element of type TextContent. You can have a convenience property called Message.Text or something. Writing to Message.Text can create a single TextContent in the list, and reading it back just returns the only TextContent.Text in the list. Reading or writing Message.Text when the list has multiple elements should just throw. So we have to check, but like I said above, we already have to check if it's dynamic.

Message can have subclasses UserMessage and AssistantMessage. If you cast to UserMessage you know you can be safe calling Message.Text. AssistantMessage can also be safe with .Text if you never ask for function calls. If you're asking for function calls you should be sophisticated enough to user the List located at Message.Content instead of Message.Text.

MessageResponse should have MessageResponse.Message which is an AssistantMessage.

A List<Message> can then have UserMessages and AssistantMessages. Beginners can make simple lists and use .Text. The rest of us can make generic methods that always use the List<ContentBase>.

@tghamm
Copy link
Owner

tghamm commented Apr 18, 2024

@nganju98 Good feedback. Still ironing out how to mesh tools with messages...it's a bit complicated under the hood. FWIW, there IS a FirstMessage that can be used on MessageResponse for non Tool related calls that will cast to TextContent, so you can do something like res.FirstMessage.Text. That said, will work on cleaning this up some, I agree it's still a bit unwieldy.

@tghamm
Copy link
Owner

tghamm commented Apr 20, 2024

@nganju98 would something like this be cleaner for beginners?

var client = new AnthropicClient();
var messages = new List<Message>()
{
    new Message(RoleType.User, "Who won the world series in 2020?"),
    new Message(RoleType.Assistant, "The Los Angeles Dodgers won the World Series in 2020."),
    new Message(RoleType.User, "Where was it played?"),
};

var parameters = new MessageParameters()
{
    Messages = messages,
    MaxTokens = 1024,
    Model = AnthropicModels.Claude3Opus,
    Stream = false,
    Temperature = 1.0m,
};
var res = await client.Messages.GetClaudeMessageAsync(parameters);
Console.WriteLine(res.FirstMessage.Text);
messages.Add(res.Message);
messages.Add(new Message(RoleType.User,"Who was the starting pitcher for the Dodgers?"));
var res2 = await client.Messages.GetClaudeMessageAsync(parameters);
Console.WriteLine(res2.FirstMessage.Text);

@nganju98
Copy link
Contributor Author

nganju98 commented Apr 21, 2024 via email

@tghamm
Copy link
Owner

tghamm commented Apr 21, 2024

Gotcha. So, yes and no. dynamic is gone. I've replaced Content with a List<ContentBase> as you suggested, and further simplified the simple case for beginners like so:

var client = new AnthropicClient();
var messages = new List<Message>()
{
    new Message(RoleType.User, "Who won the world series in 2020?"),
    new Message(RoleType.Assistant, "The Los Angeles Dodgers won the World Series in 2020."),
    new Message(RoleType.User, "Where was it played?"),
};

var parameters = new MessageParameters()
{
    Messages = messages,
    MaxTokens = 1024,
    Model = AnthropicModels.Claude3Opus,
    Stream = false,
    Temperature = 1.0m,
};

var res = await client.Messages.GetClaudeMessageAsync(parameters);
//either of these will work and print the text
Console.WriteLine(res.Message);
Console.WriteLine(res.Message.ToString());

messages.Add(res.Message);

messages.Add(new Message(RoleType.User,"Who was the starting pitcher for the Dodgers?"));

var res2 = await client.Messages.GetClaudeMessageAsync(parameters);
Console.WriteLine(res2.Message);

For a more advanced scenario that requires Tools, it's designed to work in a variety of ways, but here's one way:

string resourceName = "Anthropic.SDK.Tests.Red_Apple.jpg";

Assembly assembly = Assembly.GetExecutingAssembly();

await using Stream stream = assembly.GetManifestResourceStream(resourceName);
byte[] imageBytes;
using (var memoryStream = new MemoryStream())
{
    await stream.CopyToAsync(memoryStream);
    imageBytes = memoryStream.ToArray();
}

string base64String = Convert.ToBase64String(imageBytes);

var client = new AnthropicClient();

var messages = new List<Message>();

messages.Add(new Message()
{
    Role = RoleType.User,
    //note: Content is of type List<ContentBase>
    Content = new List<ContentBase>()
    {
        new ImageContent()
        {
            Source = new ImageSource()
            {
                MediaType = "image/jpeg",
                Data = base64String
            }
        },
        new TextContent()
        {
            Text = "Use `record_summary` to describe this image."
        }
    }
});

var imageSchema = new ImageSchema
{
    Type = "object",
    Required = new string[] { "key_colors", "description"},
    Properties = new Properties()
    {
        KeyColors = new KeyColorsProperty
        {
        Items = new ItemProperty
        {
            Properties = new Dictionary<string, ColorProperty>
            {
                { "r", new ColorProperty { Type = "number", Description = "red value [0.0, 1.0]" } },
                { "g", new ColorProperty { Type = "number", Description = "green value [0.0, 1.0]" } },
                { "b", new ColorProperty { Type = "number", Description = "blue value [0.0, 1.0]" } },
                { "name", new ColorProperty { Type = "string", Description = "Human-readable color name in snake_case, e.g. 'olive_green' or 'turquoise'" } }
            }
        }
    },
        Description = new DescriptionDetail { Type = "string", Description = "Image description. One to two sentences max." },
        EstimatedYear = new EstimatedYear { Type = "number", Description = "Estimated year that the images was taken, if is it a photo. Only set this if the image appears to be non-fictional. Rough estimates are okay!" }
    }
    
};

JsonSerializerOptions jsonSerializationOptions = new()
{
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
    Converters = { new JsonStringEnumConverter() },
    ReferenceHandler = ReferenceHandler.IgnoreCycles,
};
string jsonString = JsonSerializer.Serialize(imageSchema, jsonSerializationOptions);
var tools = new List<Common.Tool>()
{
    new Common.Tool(new Function("record_summary", "Record summary of an image into well-structured JSON.",
        JsonNode.Parse(jsonString)))
};




var parameters = new MessageParameters()
{
    Messages = messages,
    MaxTokens = 1024,
    Model = AnthropicModels.Claude3Sonnet,
    Stream = false,
    Temperature = 1.0m,
};
var res = await client.Messages.GetClaudeMessageAsync(parameters, tools);
//ToolUseContent can be extracted by type
var toolResult = res.Content.OfType<ToolUseContent>().First();
//Input is a JsonNode, so you can work with it however you like
var json = toolResult.Input.ToJsonString();
{
  "description": "This image shows a close-up view of a ripe, red apple with shades of yellow and orange. The apple has a shiny, waxy surface with water droplets visible, giving it a fresh appearance.",
  "estimated_year": 2020,
  "key_colors": [
    {
      "r": 1,
      "g": 0.2,
      "b": 0.2,
      "name": "red"
    },
    {
      "r": 1,
      "g": 0.6,
      "b": 0.2,
      "name": "orange"
    },
    {
      "r": 0.8,
      "g": 0.8,
      "b": 0.2,
      "name": "yellow"
    }
  ]
}

This is the example from JSON Mode in the Claude docs at:
https://docs.anthropic.com/claude/docs/tool-use-examples#json-mode

This follows a pattern of a library called OpenAI-DotNet pretty closely. If you check the README, there's like 5 other ways you can construct a tool but this is probably the most verbose. But per your point, they all boil down to non-dynamics now, so they'll be easier to work with.

Thoughts @nganju98 ?

tghamm added a commit that referenced this issue Apr 21, 2024
@tghamm tghamm closed this as completed Apr 24, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants