oscerd commented on code in PR #24473:
URL: https://github.com/apache/camel/pull/24473#discussion_r3552011296


##########
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" 
+
+    "&parameter.city=string&parameter.city.description=The city name")
+    .to("bean:weatherService");
+----
+
+XML::
++
+[source,xml]
+----
+<route>
+  <from uri="ai-tool:weather?tags=weather&amp;description=Get current weather 
for a city&amp;parameter.city=string&amp;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" +
+    "&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("direct:calculator");
+----
+
+XML::
++
+[source,xml]
+----
+<route>
+  <from uri="ai-tool:calculator?tags=math&amp;description=Calculate a math 
expression&amp;parameter.a=number&amp;parameter.a.description=First 
operand&amp;parameter.a.required=true&amp;parameter.b=number&amp;parameter.b.description=Second
 
operand&amp;parameter.b.required=true&amp;parameter.operation=string&amp;parameter.operation.description=The
 operation to 
perform&amp;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" +
+    "&parameter.city=string&parameter.city.description=The city name")
+    .to("bean:weatherService");
+
+from("ai-tool:queryUser?tags=users&description=Query database by user ID" +
+    "&parameter.id=integer&parameter.id.description=The user 
ID&parameter.id.required=true")
+    .to("sql:SELECT name FROM users WHERE id = :#id");

Review Comment:
   This example won't bind as written: camel-sql resolves `:#id` from the 
body-as-Map, the headers, or an exchange **variable named `id`** (see 
`SqlHelper.lookupParameter`), but with this design the arguments live only 
inside the single `AiToolArguments` variable — there is no `id` variable or 
header, and the body is not the arguments map. (The pattern worked with the old 
header-based tools components, which is probably where the example stems from.)
   
   Options: use a simple-expression binding (e.g. 
`:#${variable.AiToolArguments.parameters[id]}` — worth a quick verification), 
switch the example to a processor/bean that reads `AiToolArguments`, or — if 
`:#name` ergonomics are a goal for tool routes — consider having the executor 
additionally expose declared arguments individually. The same applies to the 
XML tab (line 188) and the YAML tab (line 226).



##########
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()) {

Review Comment:
   Edge case: a `tags` value that is non-blank but contains only blank items 
(e.g. `tags=,`) passes this check, but `splitTags` then returns an empty array 
— so the loop below registers the tool **nowhere** (neither under a tag nor in 
the default pool), silently. Similarly, `tags=,research` registers the tool 
under the empty-string tag `""`.
   
   Suggestion: filter blank entries in `splitTags`, and if nothing remains fall 
back to the default pool here, matching the documented "when omitted" semantics.



##########
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();

Review Comment:
   The warn above says the undeclared argument is being *ignored*, but 
`argsCopy` copies **all** entries — undeclared arguments included — so they 
remain accessible to the route via `AiToolArguments`. The generated schema also 
advertises `additionalProperties: false` to the model.
   
   Two ways to make this consistent:
   
   1. Actually filter undeclared arguments out of `argsCopy` when the tool 
declares parameters (defense-in-depth: extra parameters an LLM hallucinates — 
or is prompted into sending — never reach the route), or
   2. Keep the pass-through behavior and reword the log message/Javadoc (the 
design doc says "warned about but not rejected").
   
   Option 1 seems preferable given the schema already tells the model 
`additionalProperties: false`. Either way, a test asserting the chosen behavior 
would pin it down — `testExecuteIgnoresUndeclaredArguments` currently passes 
under both semantics (it never checks whether `extraParam` is present in 
`AiToolArguments`).



##########
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:
   Question: this silently drops anything after the first `/` 
(`ai-tool:foo/bar` → tool name `foo`). Since the syntax is just 
`ai-tool:toolName`, silently truncating user input seems surprising — is the 
`/` handling intentional (future-proofing?), or would rejecting / keeping the 
full `remaining` be clearer? A short comment would help either way.



##########
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" 
+
+    "&parameter.city=string&parameter.city.description=The city name")
+    .to("bean:weatherService");
+----
+
+XML::
++
+[source,xml]
+----
+<route>
+  <from uri="ai-tool:weather?tags=weather&amp;description=Get current weather 
for a city&amp;parameter.city=string&amp;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" +
+    "&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("direct:calculator");
+----
+
+XML::
++
+[source,xml]
+----
+<route>
+  <from uri="ai-tool:calculator?tags=math&amp;description=Calculate a math 
expression&amp;parameter.a=number&amp;parameter.a.description=First 
operand&amp;parameter.a.required=true&amp;parameter.b=number&amp;parameter.b.description=Second
 
operand&amp;parameter.b.required=true&amp;parameter.operation=string&amp;parameter.operation.description=The
 operation to 
perform&amp;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" +
+    "&parameter.city=string&parameter.city.description=The city name")
+    .to("bean:weatherService");
+
+from("ai-tool:queryUser?tags=users&description=Query database by user ID" +
+    "&parameter.id=integer&parameter.id.description=The user 
ID&parameter.id.required=true")
+    .to("sql:SELECT name FROM users WHERE id = :#id");
+
+// LangChain4j agent uses only weather-tagged tools
+from("direct:chat")
+    .to("langchain4j-agent:assistant?agent=#myAgent&tags=weather");
+----

Review Comment:
   Minor: this section shows the `langchain4j-agent` producer consuming 
registry tools, but that wiring only lands in Step 2 — a reader of the nightly 
docs could reasonably expect it to work today. The See Also section already 
says "future release"; a short NOTE admonition here as well would avoid 
confusion until Steps 2–4 land.



-- 
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]

Reply via email to