Christophe Willemsen
Christophe Willemsen

Reputation: 20185

Provide tools programatically with quarkus-langchain4j

In vanilla langchain4j we can provide tools programatically https://docs.langchain4j.dev/tutorials/tools#specifying-tools-programmatically

I don't seem to find a way to do the same with quarkus-langchain4j, the only examples are for dynamic tools, which is different.

I have my ToolSpecification builder

    private ToolSpecification createToolFromAction(Action action) {
        var toolname = action.name();
        var toolDescription = action.description();
        Map<String, JsonSchemaElement> params = new HashMap<>();
        action.parameters().forEach(parameter -> {
            params.put(parameter.name(), JsonStringSchema.builder().description(parameter.name()).build());
        });

        return ToolSpecification.builder()
                .name(toolname)
                .description(toolDescription)
                .parameters(JsonObjectSchema.builder().properties(params).build())
                .build();
    }

    public List<ToolSpecification> createToolSpecificationsFromActions() {
        return actionsRepository.listActions().stream()
                .map(this::createToolFromAction)
                .toList();
    }

And I can also provide the ToolExecutor, however I have no idea how to provide it along with the AiService with the @RegisterAiService annotation.

UPDATE

Allright, after some deep dive into the codebase, I found a test which shows how to use your own ToolSpecification and ToolExecutor.

        @Override
        public ToolProviderResult provideTools(ToolProviderRequest request) {
            assertNotNull(myServiceWithoutTools);

            ToolSpecification toolSpecification = ToolSpecification.builder()
                    .name("get_booking_details")
                    .description("Returns booking details")
                    .build();
            ToolExecutor toolExecutor = (t, m) -> "0";
            return ToolProviderResult.builder()
                    .add(toolSpecification, toolExecutor)
                    .build();
        }

https://github.com/quarkiverse/quarkus-langchain4j/blob/main/core/deployment/src/test/java/io/quarkiverse/langchain4j/test/ToolProviderTest.java#L63C9-L75C10

However, it breaks all my other interfaces annotated with @RegisterAiService with the following error :

java.lang.IllegalArgumentException: Either the tools or the tool provider can be configured, but not both!

Upvotes: 1

Views: 79

Answers (1)

Christophe Willemsen
Christophe Willemsen

Reputation: 20185

So, it seems you can basically achieve the same using DynamicTools, up to you to make it dynamic or not.

Here is a full example :

package com.ikwattro;

import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.service.tool.ToolExecutor;
import dev.langchain4j.service.tool.ToolProvider;
import dev.langchain4j.service.tool.ToolProviderRequest;
import dev.langchain4j.service.tool.ToolProviderResult;
import io.quarkiverse.langchain4j.RegisterAiService;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.jboss.resteasy.reactive.RestQuery;

import java.util.function.Supplier;

@Path("/assistant")
public class AiResource {

    @Inject FirstAiService firstAiService;

    @Inject SecondAiService secondAiService;

    @GET
    @Path("/one")
    public String chatOne(@RestQuery String text) {
        return firstAiService.chatOne(text);
    }

    @GET
    @Path("/two")
    public String chatTwo(@RestQuery String text) {
        return secondAiService.chatTwo(text);
    }


    @Singleton
    public static class Counter {

        @Tool("Counts the number of characters in a string")
        public int countCharacters(String s) {
            return s.length();
        }
    }

    @Singleton
    public static class MyToolsProvider implements Supplier<ToolProvider> {

        @Override
        public ToolProvider get() {
            return new ToolsProviderBuilder();
        }

        public static class ToolsProviderBuilder implements ToolProvider {
            @Override
            public ToolProviderResult provideTools(ToolProviderRequest toolProviderRequest) {

                var spec = ToolSpecification.builder()
                        .name("return_one")
                        .description("return the number 1 as string")
                        .build();
                ToolExecutor executor = (toolExecutionRequest, memoryId) -> "one";

                return ToolProviderResult.builder().add(spec, executor).build();
            }
        }
    }

    @RegisterAiService(tools = {Counter.class})
    public interface FirstAiService {

        String chatOne(String text);
    }

    @RegisterAiService(toolProviderSupplier = MyToolsProvider.class)
    public interface SecondAiService {
        String chatTwo(String text);
    }


}

Regarding the issue in the question, it proved to be a bug in quarkus-langchain4j https://github.com/quarkiverse/quarkus-langchain4j/issues/1191

Upvotes: 1

Related Questions