Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@
<artifactId>camel-support</artifactId>
</dependency>

<!-- Camel Spring AI Tools for function calling -->
<!-- Camel AI Tool for unified AiToolRegistry -->
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-spring-ai-tools</artifactId>
<artifactId>camel-ai-tool</artifactId>
<version>${project.version}</version>
<optional>true</optional>
</dependency>
Expand Down Expand Up @@ -98,6 +98,12 @@
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>${awaitility-version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -501,9 +501,9 @@ NOTE: The component automatically configures conversation isolation using metada

=== Function Calling / Tool Integration

The component integrates with the xref:spring-ai-tools-component.adoc[Spring AI Tools Component] to enable LLMs to call Camel routes as functions/tools. This extends the LLM's capabilities with custom logic, external APIs, database queries, and more.
The component integrates with the xref:ai-tool-component.adoc[AI Tool Component] to enable LLMs to call Camel routes as functions/tools. This extends the LLM's capabilities with custom logic, external APIs, database queries, and more.

When you configure the `tags` parameter, the chat component discovers all matching tools from `spring-ai-tools` routes, registers them with Spring AI's ChatClient, and allows the LLM to call them during conversation.
When you configure the `tags` parameter, the chat component discovers all matching tools from `ai-tool` routes, registers them with Spring AI's ChatClient, and allows the LLM to call them during conversation.

[tabs]
====
Expand All @@ -512,13 +512,13 @@ Java::
[source,java]
----
// Define tools
from("spring-ai-tools:weather?tags=weather&description=Get current weather for a location")
from("ai-tool:weather?tags=weather&description=Get current weather for a location")
.setBody(constant("Sunny"));

from("spring-ai-tools:calculator?tags=math&description=Evaluate mathematical expressions")
from("ai-tool:calculator?tags=math&description=Evaluate mathematical expressions")
.setBody(constant("42"));

from("spring-ai-tools:stock?tags=finance&description=Get current stock price")
from("ai-tool:stock?tags=finance&description=Get current stock price")
.setBody(constant("42"));

// Chat endpoints with different tool combinations
Expand All @@ -538,17 +538,17 @@ XML::
----
<!-- Define tools -->
<route>
<from uri="spring-ai-tools:weather?tags=weather&amp;description=Get current weather for a location"/>
<from uri="ai-tool:weather?tags=weather&amp;description=Get current weather for a location"/>
<setBody><constant>Sunny</constant></setBody>
</route>

<route>
<from uri="spring-ai-tools:calculator?tags=math&amp;description=Evaluate mathematical expressions"/>
<from uri="ai-tool:calculator?tags=math&amp;description=Evaluate mathematical expressions"/>
<setBody><constant>42</constant></setBody>
</route>

<route>
<from uri="spring-ai-tools:stock?tags=finance&amp;description=Get current stock price"/>
<from uri="ai-tool:stock?tags=finance&amp;description=Get current stock price"/>
<setBody><constant>42</constant></setBody>
</route>

Expand Down Expand Up @@ -576,7 +576,7 @@ YAML::
# Define tools
- route:
from:
uri: spring-ai-tools:weather
uri: ai-tool:weather
parameters:
tags: weather
description: Get current weather for a location
Expand All @@ -586,7 +586,7 @@ YAML::

- route:
from:
uri: spring-ai-tools:calculator
uri: ai-tool:calculator
parameters:
tags: math
description: Evaluate mathematical expressions
Expand All @@ -596,7 +596,7 @@ YAML::

- route:
from:
uri: spring-ai-tools:stock
uri: ai-tool:stock
parameters:
tags: finance
description: Get current stock price
Expand Down Expand Up @@ -1028,7 +1028,7 @@ template.request("direct:chat", e -> {
----

NOTE: Tool context works with Spring AI `@Tool` methods and MCP tools.
Camel route tools (defined via `spring-ai-tools` consumer) do not receive the `ToolContext`.
Camel route tools (defined via `ai-tool` consumer) do not receive the `ToolContext`.

=== Structured Output Validation

Expand Down Expand Up @@ -1384,7 +1384,7 @@ logging.level.org.springframework.ai.chat.client.advisor=DEBUG

* https://docs.spring.io/spring-ai/reference/[Spring AI Documentation]
* https://docs.spring.io/spring-ai/reference/api/tools.html[Spring AI Function Calling]
* xref:spring-ai-tools-component.adoc[Spring AI Tools Component]
* xref:ai-tool-component.adoc[AI Tool Component]
* xref:spring-ai-embeddings-component.adoc[Spring AI Embeddings Component]
* xref:spring-ai-vector-store-component.adoc[Spring AI VectorStore Component]
* xref:spring-ai-image-component.adoc[Spring AI Image Component]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.camel.component.springai.chat;

import java.util.Map;
import java.util.function.Function;

import org.apache.camel.Exchange;
import org.apache.camel.component.ai.tool.AiToolExecutor;
import org.apache.camel.component.ai.tool.AiToolResult;
import org.apache.camel.component.ai.tool.AiToolSpec;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.function.FunctionToolCallback;

/**
* Converts a framework-agnostic {@link AiToolSpec} into a Spring AI {@link ToolCallback}. Delegates tool execution to
* {@link AiToolExecutor} so that argument filtering, required-parameter validation, and route invocation are handled
* uniformly across all AI framework adapters.
*/
final class AiToolSpecToSpringAi {

private static final Logger LOG = LoggerFactory.getLogger(AiToolSpecToSpringAi.class);

private AiToolSpecToSpringAi() {
}

static ToolCallback toToolCallback(AiToolSpec spec) {
Function<Map<String, Object>, String> function = args -> {
Exchange toolExchange = spec.getConsumer().getEndpoint().createExchange();
try {
AiToolResult result = AiToolExecutor.execute(spec, args, toolExchange);
if (result instanceof AiToolResult.Success success) {
return success.value();
} else if (result instanceof AiToolResult.ArgumentError argErr) {
return "Tool execution failed: " + argErr.message();
} else if (result instanceof AiToolResult.ExecutionError execErr) {
LOG.warn("Tool '{}' execution failed: {}", spec.getName(), execErr.message(), execErr.cause());
return "Tool execution failed";
}
return "Tool execution failed";
} finally {
spec.getConsumer().releaseExchange(toolExchange, false);
}
};

FunctionToolCallback.Builder builder = FunctionToolCallback
.builder(spec.getName(), function)
.description(spec.getDescription())
.inputType(Map.class);

if (spec.getParametersJsonSchema() != null) {
builder.inputSchema(spec.getParametersJsonSchema());
}

return builder.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ public String getTags() {

/**
* Tags for discovering and calling Camel route tools. When provided, the chat component will automatically register
* tools from camel-spring-ai-tools routes matching these tags.
* tools from ai-tool routes matching these tags.
*/
public void setTags(String tags) {
this.tags = tags;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
Expand All @@ -31,10 +32,10 @@
import org.apache.camel.InvalidPayloadException;
import org.apache.camel.NoSuchHeaderException;
import org.apache.camel.WrappedFile;
import org.apache.camel.component.ai.tool.AiToolParameterHelper;
import org.apache.camel.component.ai.tool.AiToolRegistry;
import org.apache.camel.component.ai.tool.AiToolSpec;
import org.apache.camel.component.springai.chat.mcp.SpringAiChatMcpManager;
import org.apache.camel.component.springai.tools.TagsHelper;
import org.apache.camel.component.springai.tools.spec.CamelToolExecutorCache;
import org.apache.camel.component.springai.tools.spec.CamelToolSpecification;
import org.apache.camel.support.DefaultProducer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -958,7 +959,7 @@ private <T> void processStructuredOutputRequest(
}

/**
* Register tools from camel-spring-ai-tools routes based on configured tags
* Register tools from ai-tool routes based on configured tags
*
* Tools are registered via ChatOptions.toolCallbacks() which is the correct way to pass ToolCallback instances in
* Spring AI. The tools() method expects objects with @Tool annotated methods, not ToolCallback instances.
Expand All @@ -969,8 +970,8 @@ private List<ToolCallback> getToolCallbacksForTags(String tags) {
return List.of();
}

// Discover tools from Camel Spring AI Tools routes
List<ToolCallback> toolCallbacks = discoverTools(tags);
// Discover tools from the unified AiToolRegistry
List<ToolCallback> toolCallbacks = discoverAiRegistryTools(tags);

if (!toolCallbacks.isEmpty()) {
// Collect tool names for enhanced logging
Expand All @@ -994,21 +995,22 @@ private List<ToolCallback> getToolCallbacksForTags(String tags) {
}

/**
* Discover tools by tags and return a list of ToolCallback instances
* Discover tools registered via {@code ai-tool:} consumer endpoints in the shared {@link AiToolRegistry}. Converts
* each {@link AiToolSpec} to a Spring AI {@link ToolCallback} via {@link AiToolSpecToSpringAi}.
*/
private List<ToolCallback> discoverTools(String tags) {
final CamelToolExecutorCache toolCache = CamelToolExecutorCache.getInstance();
final Map<String, Set<CamelToolSpecification>> tools = toolCache.getTools();
final String[] tagArray = TagsHelper.splitTags(tags);

final List<ToolCallback> toolCallbacks = Arrays.stream(tagArray)
.flatMap(tag -> tools.entrySet().stream()
.filter(entry -> entry.getKey().equals(tag))
.flatMap(entry -> entry.getValue().stream()))
.map(CamelToolSpecification::getToolCallback)
private List<ToolCallback> discoverAiRegistryTools(String tags) {
final AiToolRegistry registry = AiToolRegistry.getOrCreate(getEndpoint().getCamelContext());
final String[] tagArray = AiToolParameterHelper.splitTags(tags);

final Set<AiToolSpec> uniqueSpecs = new LinkedHashSet<>();
for (String tag : tagArray) {
uniqueSpecs.addAll(registry.getToolsByTag(tag));
}
final List<ToolCallback> toolCallbacks = uniqueSpecs.stream()
.map(AiToolSpecToSpringAi::toToolCallback)
.collect(Collectors.toList());

LOG.debug("Discovered {} unique tools for tags: {}", toolCallbacks.size(), tags);
LOG.debug("Discovered {} tools from AiToolRegistry for tags: {}", toolCallbacks.size(), tags);
return toolCallbacks;
}

Expand Down
Loading