jamesrochabrun / SwiftAnthropic

An open-source Swift package for interacting with Anthropic's public API.
90 stars 29 forks source link

Tools with complex parameter schemas don't work #7

Closed bcherry closed 6 months ago

bcherry commented 6 months ago

Love the quick update to support new tools / functions!

I ran into an issue integrating it when using complex function types. I have functions that have arrays and nested object types. All are supported by JSONSchema, and thus by Claude 3 in the new beta release.

However, the Content type in this library hardcodes function responses as [String: String] - that is, a single layer dictionary of string values. If your function call has a more complex return type, it won't parse.

The solution I've found required a deep change - instead of decoding the input key to [String: String], I just pass the raw container up to the caller and let it parse the input key with its own type definition. This works but it's super messy.

You can see what I mean over here in this fork, but it's not suitable for submitting a PR: https://github.com/bcherry/SwiftAnthropic/commit/624aec8916cc4b039a7efca1efae9e4941c16c2a

I'm not an expert on Swift encoding/decoding nor generics so maybe there's a cleaner way to accomplish this?

jamesrochabrun commented 6 months ago

This should take care of what you need, now you can retrieve the content you want, see the Demo example on how I can get the string value of the JSON https://github.com/jamesrochabrun/SwiftAnthropic/pull/9 Let me know how that goes!

bcherry commented 6 months ago

Ooh looks good, that should be perfect. I will try this out soon!

bcherry commented 6 months ago

@jamesrochabrun for our use case we are hoping to get rich typed outputs rather than a more free-form dictionary. Luckily what you did works if you also make DynamicContent encodable. This seems to work just fine. I tested it with arrays of dictionaries and everything.

       public func encode(to encoder: any Encoder) throws {
              var container = encoder.singleValueContainer()
              switch self {
              case .string(let val):
                  try container.encode(val)
              case .integer(let val):
                  try container.encode(val)
              case .double(let val):
                  try container.encode(val)
              case .dictionary(let val):
                  try container.encode(val)
              case .array(let val):
                  try container.encode(val)
              case .bool(let val):
                  try container.encode(val)
              case .null:
                  try container.encodeNil()
              }
          }

Then we can easily get what we need in our code by turning it back to JSON with JSONEncoder().encode(input) then can reparse with our type.

jamesrochabrun commented 6 months ago

@bcherry do you want to open a PR with your example? Also make sure to test your changes against the demo app, both for messages and function calling. I will take a look later today. thanks.

bcherry commented 6 months ago

I'm going to leave it alone today - it's now integrated into our app like that and working but i still need to use my fork anyways to use a basePath that has a partial path component so I'll wait til we sort that out and revisit!