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

Implement Tool macro #3

Closed
wants to merge 10 commits into from
Closed

Implement Tool macro #3

wants to merge 10 commits into from

Conversation

mattt
Copy link
Owner

@mattt mattt commented Oct 27, 2024

Related to #1

/// Add two numbers together
/// - Parameter x: The first number
/// - Parameter y: The second number
@Tool
func add(x: Int, y: Int) -> Int {
    return x + y
}

Expands to:

enum Tool_add: Tool {
    struct Input: Codable {
        let x: Int
        let y: Int
    }
    typealias Output = Int

    static var schema: [String: Value] {
        [
            "name": "add",
            "description": "Add two numbers together",
            "parameters": [
            "x": [
                "type": "number",
                "description": "The first number"
            ],
            "y": [
                "type": "number",
                "description": "The second number"
            ]
        ]
        ]
    }

    static func call(_ input: Input) -> Output {
        add(x: input.x, y: input.y)
    }
}

@kylehowells
Copy link

kylehowells commented Oct 28, 2024

On the one hand, I love the reduction in boilerplate involved.
But on the other hand as an actual consumer of the API, closures are much easier to actually use in practice than pure functions.

To do anything meaningful you'll need to connect it to other APIs, include references to database, other API (weather API for example), etc.. all of which you likely have accessible and initialised when you make the llm call. (where you'd assign the closure or tools).

So as a user of this API, the other approach has more boiler plate to deal with, but otherwise is much more useable and easier to manage.

On the other hand this approach could probably be adapted to methods and just skip the first self property, I think.

The parameter and return type creation also likely need a conversion step where they are converted from the Swift type to a JSON type, as Int doesn't exist in json, for example.

@mattt
Copy link
Owner Author

mattt commented Oct 28, 2024

@kylehowells Thanks for sharing your thoughts. That's helpful feedback.

To do anything meaningful you'll need to connect it to other APIs, include references to database, other API (weather API for example), etc.. all of which you likely have accessible and initialised when you make the llm call. (where you'd assign the closure or tools).

I totally agree. The Tool macro is intended as a convenience for top-level functions. For instance methods, consumers can define their own Tool type manually like so:

class WeatherService {
    static var `default`: WeatherService = WeatherService()

    func getCurrentWeather(latitude: Double, longitude: Double) async throws -> String {
        // ...
    }
}

enum ToolGetCurrentWeather: Tool {
    struct Input: Codable {
        let latitude: Double
        let longitude: Double
    }
    typealias Output = String
    
    static var weatherService: WeatherService!
    
    static var schema: [String: Value] {
        [
            "name": "get_current_weather",
            "description": "Gets the current weather for a location",
            "parameters": [
                "latitude": [
                    "type": "number"
                ],
                "longitude": [
                    "type": "number"
                ]
            ]
        ]
    }
    
    static func call(_ input: Input) async throws -> Output {
        return await WeatherService.default.getCurrentWeather(latitude: input.latitude, 
                                                              longitude: input.longitude)
    }
}

I'm also working on a #createTool macro, but that seems to be crashing the Swift compiler at the moment 😅

let weatherTool = #createTool(
    name: "getWeather",
    description: "Get weather for location",
    parameters: ["location": ["type": "string"]],
    { (latitude: Double, longitude: Double) async throws -> String in
        return await WeatherService.default.getCurrentWeather(latitude: input.latitude, 
                                                              longitude: input.longitude)
    }
)

The parameter and return type creation also likely need a conversion step where they are converted from the Swift type to a JSON type, as Int doesn't exist in json, for example.

That's right. This was missing in the first pass, but I've since added this. Custom struct types are dicey, but they should work as expected if they conform to Codable.

@mattt
Copy link
Owner Author

mattt commented Feb 15, 2025

Adopting Swift macros currently has a significant impact on compilation times. This should be fixed by a recent PR that allows Swift Package Manager to use a pre-built distribution of swift-syntax. This is scheduled to go out with Swift 6.1, likely in March with the Spring Xcode release.

Holding in draft at least until then.

@mattt mattt mentioned this pull request Feb 15, 2025
@mattt
Copy link
Owner Author

mattt commented Feb 15, 2025

Closing in favor of #7

@mattt mattt closed this Feb 15, 2025
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

Successfully merging this pull request may close these issues.

2 participants