Class design has gotten a bit confusing
Closed this issue · 4 comments
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>.
@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.
@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);
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 ?