Croway commented on code in PR #24473:
URL: https://github.com/apache/camel/pull/24473#discussion_r3543981379
##########
components/camel-ai/camel-ai-tool/src/main/docs/ai-tool-component.adoc:
##########
@@ -0,0 +1,241 @@
+= AI Tool Component
+:doctitle: AI Tool
+:shortname: ai-tool
+:artifactid: camel-ai-tool
+:description: Framework-agnostic consumer endpoint that registers a Camel
route as an LLM tool in the shared AiToolRegistry.
+:since: 4.22
+:supportlevel: Preview
+:tabs-sync-option:
+:component-header: Only consumer is supported
+//Manually maintained attributes
+:group: AI
+
+*Since Camel {since}*
+
+*{component-header}*
+
+The AI Tool component provides a framework-agnostic way to expose Camel routes
as tools that AI models can call.
+Tools registered via this component are stored in a shared `AiToolRegistry`
and can be discovered by any AI producer
+component (such as xref:langchain4j-agent-component.adoc[LangChain4j Agent] or
xref:spring-ai-chat-component.adoc[Spring AI Chat])
+using tag-based filtering.
+
+Maven users will need to add the following dependency to their `pom.xml`
+for this component:
+
+[source,xml]
+----
+<dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-ai-tool</artifactId>
+ <version>x.x.x</version>
+ <!-- use the same version as your Camel core version -->
+</dependency>
+----
+
+== URI format
+
+----
+ai-tool:toolName[?options]
+----
+
+Where *toolName* is the name the LLM sees and uses to invoke the tool.
+
+// component options: START
+include::partial$component-configure-options.adoc[]
+include::partial$component-endpoint-options.adoc[]
+include::partial$component-endpoint-headers.adoc[]
+// component options: END
+
+== Usage
+
+Define a tool by creating a consumer route with the `ai-tool:` scheme.
+The route body processes tool invocations and returns results to the AI model.
+
+=== Basic Tool Definition
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+from("ai-tool:weather?tags=weather&description=Get current weather for a city"
+
+ "¶meter.city=string¶meter.city.description=The city name")
+ .to("bean:weatherService");
+----
+
+XML::
++
+[source,xml]
+----
+<route>
+ <from uri="ai-tool:weather?tags=weather&description=Get current weather
for a city&parameter.city=string&parameter.city.description=The city
name"/>
+ <to uri="bean:weatherService"/>
+</route>
+----
+
+YAML::
++
+[source,yaml]
+----
+- route:
+ from:
+ uri: ai-tool:weather
+ parameters:
+ tags: weather
+ description: "Get current weather for a city"
+ parameter.city: string
+ parameter.city.description: "The city name"
+ steps:
+ - to:
+ uri: bean:weatherService
+----
+====
+
+=== Tool with Multiple Parameters
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+from("ai-tool:calculator?tags=math" +
+ "&description=Calculate a math expression" +
+ "¶meter.a=number" +
+ "¶meter.a.description=First operand" +
+ "¶meter.a.required=true" +
+ "¶meter.b=number" +
+ "¶meter.b.description=Second operand" +
+ "¶meter.b.required=true" +
+ "¶meter.operation=string" +
+ "¶meter.operation.description=The operation to perform" +
+ "¶meter.operation.enum=add,subtract,multiply,divide")
+ .to("direct:calculator");
+----
+
+XML::
++
+[source,xml]
+----
+<route>
+ <from uri="ai-tool:calculator?tags=math&description=Calculate a math
expression&parameter.a=number&parameter.a.description=First
operand&parameter.a.required=true&parameter.b=number&parameter.b.description=Second
operand&parameter.b.required=true&parameter.operation=string&parameter.operation.description=The
operation to
perform&parameter.operation.enum=add,subtract,multiply,divide"/>
+ <to uri="direct:calculator"/>
+</route>
+----
+
+YAML::
++
+[source,yaml]
+----
+- route:
+ from:
+ uri: ai-tool:calculator
+ parameters:
+ tags: math
+ description: "Calculate a math expression"
+ parameter.a: number
+ parameter.a.description: "First operand"
+ parameter.a.required: true
+ parameter.b: number
+ parameter.b.description: "Second operand"
+ parameter.b.required: true
+ parameter.operation: string
+ parameter.operation.description: "The operation to perform"
+ parameter.operation.enum: "add,subtract,multiply,divide"
+ steps:
+ - to:
+ uri: direct:calculator
+----
+====
+
+=== Tag-Based Discovery
+
+Tags group tools so that AI producers can select relevant subsets:
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+// Define tools with tags
+from("ai-tool:weather?tags=weather,external-api&description=Get weather for a
city" +
+ "¶meter.city=string¶meter.city.description=The city name")
+ .to("bean:weatherService");
+
+from("ai-tool:queryUser?tags=users&description=Query database by user ID" +
+ "¶meter.id=integer¶meter.id.description=The user
ID¶meter.id.required=true")
+ .to("sql:SELECT name FROM users WHERE id = :#id");
Review Comment:
This example won't work: `:#id` binds from headers/body, but tool arguments
are in the `AiToolArguments` exchange variable. Either use
`:#${variable.AiToolArguments.parameters[id]}` or add a processor step that
extracts `args.getInteger("id")` into a header first. Worth fixing since it's
the pattern users will copy (the XML and YAML tabs have the same issue).
##########
components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tools/AiToolExecutor.java:
##########
@@ -0,0 +1,143 @@
+/*
+ * 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.ai.tools;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.Processor;
+import org.apache.camel.support.DefaultConsumer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Framework-agnostic executor for Camel route tools. Handles the common logic
of resolving the route processor from the
+ * tool's consumer, populating an {@link Exchange} with tool arguments,
invoking the route, and returning the result.
+ * <p>
+ * AI framework adapters (LangChain4j, Spring AI, OpenAI) only need to parse
their native argument format into a
+ * {@code Map<String, Object>} and call this executor — they do not need to
know how routes are resolved or invoked.
+ * <p>
+ * Returns an {@link AiToolResult} that classifies the outcome without
deciding error handling policy. Framework
+ * adapters inspect the result type and decide whether to return the error
message as a string to the LLM, rethrow the
+ * cause so framework-level error handlers fire, or sanitize the message
before returning it.
+ * <p>
+ * This is an internal support class used by Camel AI framework adapters and
is not intended for direct use by end
+ * users.
+ *
+ * @since 4.22
+ */
+public final class AiToolExecutor {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(AiToolExecutor.class);
+
+ private AiToolExecutor() {
+ }
+
+ /**
+ * Executes a Camel route tool by resolving the route processor from the
spec's consumer, populating the exchange
+ * with the provided arguments, and invoking the route.
+ * <p>
+ * Arguments are validated against the tool's declared parameters
(undeclared arguments are warned about but not
+ * rejected, required arguments are checked) and then wrapped in an {@link
AiToolArguments} object set as an
+ * exchange variable under {@link AiTool#TOOL_ARGUMENTS}. This avoids
polluting the exchange header namespace and
+ * eliminates any risk of colliding with internal Camel headers.
+ * <p>
+ * The calling adapter owns the exchange lifecycle: it must create the
exchange before calling this method and
+ * release it afterwards (via {@code consumer.releaseExchange()}) in a
try-finally block.
+ * <p>
+ * All errors — validation failures and route execution errors — are
caught and returned as typed
+ * {@link AiToolResult} variants rather than propagated. Framework
adapters inspect the result type and decide how
+ * to handle errors (return to LLM, rethrow, sanitize).
+ *
+ * @param spec the tool specification containing the consumer and
declared parameters
+ * @param arguments the tool arguments as a name-value map; each
framework adapter is responsible for parsing its
+ * native format (JSON string, Map, etc.) into this map
before calling
+ * @param exchange the Camel exchange to populate with arguments and
execute
+ * @return an {@link AiToolResult} classifying the outcome;
never null
+ */
+ public static AiToolResult execute(AiToolSpec spec, Map<String, Object>
arguments, Exchange exchange) {
+ String toolName = spec.getName();
+
+ DefaultConsumer consumer = spec.getConsumer();
+ if (consumer == null) {
+ IllegalStateException cause = new IllegalStateException(
+ String.format("No consumer available for tool '%s'",
toolName));
+ return new AiToolResult.ExecutionError(cause.getMessage(), cause);
+ }
+
+ Processor routeProcessor = consumer.getProcessor();
+ if (routeProcessor == null) {
+ IllegalStateException cause = new IllegalStateException(
+ String.format("No route processor available for tool
'%s'", toolName));
+ return new AiToolResult.ExecutionError(cause.getMessage(), cause);
+ }
+
+ LOG.debug("Executing Camel route tool: '{}'", toolName);
+
+ // Warn about undeclared arguments but do not reject them -- LLMs
frequently
+ // hallucinate extra parameters and rejecting them would break valid
tool calls.
+ if (arguments != null && !arguments.isEmpty() &&
!spec.getParameterDefs().isEmpty()) {
+ Set<String> declaredParams = spec.getParameterDefs().keySet();
+
+ for (String name : arguments.keySet()) {
+ if (!declaredParams.contains(name)) {
+ LOG.warn("Undeclared tool argument '{}' for tool '{}' --
the LLM sent a parameter "
+ + "that is not declared in the tool
specification; ignoring it",
+ name, toolName);
+ }
+ }
+ }
+
+ for (Map.Entry<String, AiToolParameterHelper.ParameterDef> entry :
spec.getParameterDefs().entrySet()) {
+ if (entry.getValue().isRequired()
+ && (arguments == null ||
!arguments.containsKey(entry.getKey()))) {
+ LOG.warn("Missing required argument '{}' for tool '{}' -- the
LLM did not send "
+ + "a parameter that is declared as required in the
tool specification",
+ entry.getKey(), toolName);
+ IllegalArgumentException cause = new IllegalArgumentException(
+ String.format("Missing required argument '%s' for tool
'%s'", entry.getKey(), toolName));
+ return new AiToolResult.ArgumentError(cause.getMessage(),
cause);
+ }
+ }
+
+ // Defensive copy so callers cannot mutate arguments during route
execution
+ Map<String, Object> argsCopy = arguments != null ? new
HashMap<>(arguments) : Map.of();
+ exchange.setVariable(AiTool.TOOL_ARGUMENTS, new
AiToolArguments(toolName, argsCopy));
+
+ // Execute the route
+ try {
+ routeProcessor.process(exchange);
+ } catch (Exception e) {
+ LOG.error("Error executing tool '{}': {}", toolName,
e.getMessage(), e);
+ return new AiToolResult.ExecutionError(
+ String.format("Error executing tool '%s': %s", toolName,
e.getMessage()), e);
+ }
+
+ if (exchange.getException() != null) {
+ Exception routeError = exchange.getException();
+ LOG.error("Error executing tool '{}': {}", toolName,
routeError.getMessage(), routeError);
+ return new AiToolResult.ExecutionError(
+ String.format("Error executing tool '%s': %s", toolName,
routeError.getMessage()), routeError);
+ }
+
+ String result = exchange.getMessage().getBody(String.class);
+ LOG.debug("Tool '{}' execution completed successfully", toolName);
Review Comment:
This sits outside the try/catch, so a `TypeConversionException` (route
returns a non-convertible body) propagates to the adapter — contradicting the
documented contract that all errors come back as typed `AiToolResult` variants.
Suggest wrapping it and returning an `ExecutionError`.
##########
components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tools/AiToolConsumer.java:
##########
@@ -0,0 +1,100 @@
+/*
+ * 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.ai.tools;
+
+import java.util.Map;
+
+import org.apache.camel.Processor;
+import org.apache.camel.support.DefaultConsumer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Consumer that registers a Camel route as an AI tool in the {@link
AiToolRegistry} on start and deregisters on stop.
+ *
+ * @since 4.22
+ */
+public class AiToolConsumer extends DefaultConsumer {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(AiToolConsumer.class);
+
+ private final String toolName;
+ private final AiToolConfiguration configuration;
+ private AiToolSpec registeredSpec;
+ private String[] registeredTags;
+ private boolean registeredInDefaultPool;
+
+ public AiToolConsumer(AiToolEndpoint endpoint, Processor processor) {
+ super(endpoint, processor);
+ this.toolName = endpoint.getToolName();
+ this.configuration = endpoint.getConfiguration();
+ }
+
+ @Override
+ protected void doStart() throws Exception {
+ super.doStart();
+
+ Map<String, String> params = configuration.getParameters();
+ Map<String, AiToolParameterHelper.ParameterDef> parameterDefs
+ = (params != null && !params.isEmpty())
+ ? AiToolParameterHelper.parseParameterMetadata(params)
+ : Map.of();
+
+ String jsonSchema = !parameterDefs.isEmpty()
+ ? AiToolParameterHelper.buildJsonSchemaFromDefs(parameterDefs)
+ : null;
+
+ registeredSpec = new AiToolSpec(
+ toolName, configuration.getDescription(), parameterDefs,
jsonSchema, this);
+
+ AiToolRegistry registry =
AiToolRegistry.getOrCreate(getEndpoint().getCamelContext());
+ String tags = configuration.getTags();
+ if (tags != null && !tags.isBlank()) {
+ registeredTags = AiToolParameterHelper.splitTags(tags);
+ registeredInDefaultPool = false;
+ for (String tag : registeredTags) {
+ LOG.debug("Registering tool '{}' with tag '{}'", toolName,
tag);
+ registry.put(tag, registeredSpec);
+ }
+ } else {
+ registeredTags = null;
+ registeredInDefaultPool = true;
+ LOG.debug("Registering tool '{}' in default pool (no tags)",
toolName);
+ registry.putDefault(registeredSpec);
+ }
+ }
+
+ @Override
+ protected void doStop() throws Exception {
Review Comment:
Route **suspend** doesn't deregister the tool (no `doSuspend` override), so
a suspended route stays in the registry and the executor will still dispatch
into its processor. Either deregister on suspend or have `AiToolExecutor` check
the consumer's state before dispatching — and document whichever is chosen.
##########
components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tools/AiToolConfiguration.java:
##########
@@ -0,0 +1,94 @@
+/*
+ * 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.ai.tools;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.camel.RuntimeCamelException;
+import org.apache.camel.spi.Configurer;
+import org.apache.camel.spi.Metadata;
+import org.apache.camel.spi.UriParam;
+import org.apache.camel.spi.UriParams;
+
+/**
+ * Configuration for the {@link AiToolComponent}: tool description, tags, and
parameter metadata.
+ *
+ * @since 4.22
+ */
+@Configurer
+@UriParams
+public class AiToolConfiguration implements Cloneable {
+
+ @UriParam(description = "Comma-separated list of tags used to group tools.
"
+ + "Producers filter the registry by these tags to
select which tools to expose to the LLM. "
+ + "When omitted, the tool goes into a default pool
available to all producers.")
+ private String tags;
Review Comment:
Missing `@Metadata(label = "consumer")` — the generated catalog JSON shows
`"label": ""` for `tags` while `description`/`parameters` have `"consumer"`.
##########
components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tools/AiToolRegistry.java:
##########
@@ -0,0 +1,171 @@
+/*
+ * 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.ai.tools;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.camel.CamelContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * CamelContext-scoped registry mapping tags to {@link AiToolSpec} instances.
AI components (LangChain4j, Spring AI)
+ * read from this registry to discover tools registered via the {@code
ai-tool} consumer endpoint.
+ * <p>
+ * Each {@link CamelContext} gets its own registry instance, registered as a
context plugin. Use
+ * {@link #getOrCreate(CamelContext)} to obtain the instance for a given
context.
+ * <p>
+ * Replaces the duplicated {@code CamelToolExecutorCache} singletons from
{@code camel-langchain4j-tools} and
+ * {@code camel-spring-ai-tools}.
+ *
+ * @since 4.22
+ */
+public final class AiToolRegistry {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(AiToolRegistry.class);
+
+ private final Map<String, Set<AiToolSpec>> tools;
+ private final Set<AiToolSpec> defaultTools;
+
+ AiToolRegistry() {
+ tools = new ConcurrentHashMap<>();
+ defaultTools = Collections.synchronizedSet(new LinkedHashSet<>());
+ }
+
+ /**
+ * Returns the {@link AiToolRegistry} for the given {@link CamelContext},
creating and registering one as a context
+ * plugin if it does not yet exist.
+ */
+ public static AiToolRegistry getOrCreate(CamelContext context) {
+ synchronized (context) {
+ AiToolRegistry registry = context.getCamelContextExtension()
+ .getContextPlugin(AiToolRegistry.class);
+ if (registry == null) {
+ registry = new AiToolRegistry();
+ context.getCamelContextExtension()
+ .addContextPlugin(AiToolRegistry.class, registry);
+ }
+ return registry;
+ }
+ }
+
+ public void put(String tag, AiToolSpec spec) {
+ tools.compute(tag, (k, set) -> {
+ if (set == null) {
+ set = Collections.synchronizedSet(new LinkedHashSet<>());
+ }
+ synchronized (set) {
+ for (AiToolSpec existing : set) {
+ if (existing.getName().equals(spec.getName()) && existing
!= spec) {
+ LOG.warn("Duplicate toolName '{}' under tag '{}' --
the LLM adapter will see both "
Review Comment:
Warn-only on duplicate tool names means both specs stay registered, and most
LLM APIs (OpenAI included) reject duplicate function names — so the failure
surfaces later and far from the cause. Since this registry is the foundation
for steps 2–6, consider failing fast at registration, or documenting last-wins
semantics.
##########
components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tools/AiToolComponent.java:
##########
@@ -0,0 +1,83 @@
+/*
+ * 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.ai.tools;
+
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Endpoint;
+import org.apache.camel.spi.Metadata;
+import org.apache.camel.spi.annotations.Component;
+import org.apache.camel.support.DefaultComponent;
+import org.apache.camel.util.ObjectHelper;
+import org.apache.camel.util.PropertiesHelper;
+import org.apache.camel.util.StringHelper;
+
+import static org.apache.camel.component.ai.tools.AiTool.SCHEME;
+
+/**
+ * Camel component that registers routes as LLM-callable tools in the shared
{@link AiToolRegistry}.
+ *
+ * @since 4.22
+ */
+@Component(SCHEME)
+public class AiToolComponent extends DefaultComponent {
+
+ @Metadata(description = "The component configuration")
+ private AiToolConfiguration configuration;
+
+ public AiToolComponent() {
+ this(null);
+ }
+
+ public AiToolComponent(CamelContext context) {
+ super(context);
+ this.configuration = new AiToolConfiguration();
+ }
+
+ @Override
+ protected Endpoint createEndpoint(String uri, String remaining,
Map<String, Object> parameters) throws Exception {
+ if (ObjectHelper.isEmpty(remaining)) {
+ throw new IllegalArgumentException(
+ "A toolName must be provided:
ai-tool:<toolName>?description=<desc>");
+ }
+
+ final String toolName = StringHelper.before(remaining, "/", remaining);
Review Comment:
`ai-tool:myTool/anything` silently drops `/anything`. Since the syntax is
`ai-tool:toolName` with no path segments, better to fail on a `/` (or use
`remaining` as-is) so typos surface instead of registering a truncated tool
name.
##########
components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tools/AiToolRegistry.java:
##########
@@ -0,0 +1,171 @@
+/*
+ * 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.ai.tools;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.camel.CamelContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * CamelContext-scoped registry mapping tags to {@link AiToolSpec} instances.
AI components (LangChain4j, Spring AI)
+ * read from this registry to discover tools registered via the {@code
ai-tool} consumer endpoint.
+ * <p>
+ * Each {@link CamelContext} gets its own registry instance, registered as a
context plugin. Use
+ * {@link #getOrCreate(CamelContext)} to obtain the instance for a given
context.
+ * <p>
+ * Replaces the duplicated {@code CamelToolExecutorCache} singletons from
{@code camel-langchain4j-tools} and
+ * {@code camel-spring-ai-tools}.
+ *
+ * @since 4.22
+ */
+public final class AiToolRegistry {
Review Comment:
This class relies on `synchronized` throughout — `synchronized (context)` in
`getOrCreate`, `Collections.synchronizedSet`, and `synchronized (set)` blocks
in `put`/`getToolsByTag`/`getAllTools`/`getTools`/`getDefaultTools`. Camel
supports virtual threads, and the project has been systematically replacing
`synchronized` with `ReentrantLock` for virtual-thread compatibility
(CAMEL-20199, e.g. #21703) — new code shouldn't reintroduce it. A
`ReentrantLock`-guarded `LinkedHashSet` keeps the insertion ordering, or
`CopyOnWriteArraySet` would fit this read-heavy registry with no locking at all.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]